mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-31 14:33:50 -04:00
Migrate to expo only (no next) PART 1 (#763)
This commit is contained in:
commit
1ea83848f3
@ -1,6 +1,3 @@
|
||||
{
|
||||
"extends": ["../biome.json"],
|
||||
"formatter": {
|
||||
"lineWidth": 80
|
||||
}
|
||||
"extends": "//"
|
||||
}
|
||||
|
33
api/bun.lock
33
api/bun.lock
@ -6,17 +6,18 @@
|
||||
"dependencies": {
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"blurhash": "^2.0.5",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"elysia": "^1.3.0",
|
||||
"jose": "^6.0.10",
|
||||
"elysia": "^1.3.1",
|
||||
"jose": "^6.0.11",
|
||||
"parjs": "^1.3.9",
|
||||
"pg": "^8.15.6",
|
||||
"sharp": "^0.34.1",
|
||||
"pg": "^8.16.0",
|
||||
"sharp": "^0.34.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.14",
|
||||
"bun-types": "^1.2.11",
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"@types/pg": "^8.15.2",
|
||||
"bun-types": "^1.2.14",
|
||||
"node-addon-api": "^8.3.1",
|
||||
},
|
||||
},
|
||||
@ -25,6 +26,24 @@
|
||||
"drizzle-orm@0.43.1": "patches/drizzle-orm@0.43.1.patch",
|
||||
},
|
||||
"packages": {
|
||||
"@biomejs/biome": ["@biomejs/biome@2.0.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.0", "@biomejs/cli-darwin-x64": "2.0.0", "@biomejs/cli-linux-arm64": "2.0.0", "@biomejs/cli-linux-arm64-musl": "2.0.0", "@biomejs/cli-linux-x64": "2.0.0", "@biomejs/cli-linux-x64-musl": "2.0.0", "@biomejs/cli-win32-arm64": "2.0.0", "@biomejs/cli-win32-x64": "2.0.0" }, "bin": { "biome": "bin/biome" } }, "sha512-BlUoXEOI/UQTDEj/pVfnkMo8SrZw3oOWBDrXYFT43V7HTkIUDkBRY53IC5Jx1QkZbaB+0ai1wJIfYwp9+qaJTQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QvqWYtFFhhxdf8jMAdJzXW+Frc7X8XsnHQLY+TBM1fnT1TfeV/v9vsFI5L2J7GH6qN1+QEEJ19jHibCY2Ypplw=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-5JFhls1EfmuIH4QGFPlNpxJQFC6ic3X1ltcoLN+eSRRIPr6H/lUS1ttuD0Fj7rPgPhZqopK/jfH8UVj/1hIsQw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BAH4QVi06TzAbVchXdJPsL0Z/P87jOfes15rI+p3EX9/EGTfIjaQ9lBVlHunxcmoptaA5y1Hdb9UYojIhmnjIw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bxsz8ki8+b3PytMnS5SgrGV+mbAWwIxI3ydChb/d1rURlJTMdxTTq5LTebUnlsUWAX6OvJuFeiVq9Gjn1YbCyA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-09PcOGYTtkopWRm6mZ/B6Mr6UHdkniUgIG/jLBv+2J8Z61ezRE+xQmpi3yNgUrFIAU4lPA9atg7mhvE/5Bo7Wg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tiQ0ABxMJb9I6GlfNp0ulrTiQSFacJRJO8245FFwE3ty3bfsfxlU/miblzDIi+qNrgGsLq5wIZcVYGp4c+HXZA=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vrTtuGu91xNTEQ5ZcMJBZuDlqr32DWU1r14UfePIGndF//s2WUAmer4FmgoPgruo76rprk37e8S2A2c0psXdxw=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2USVQ0hklNsph/KIR72ZdeptyXNnQ3JdzPn3NbjI4Sna34CnxeiYAaZcZzXPDl5PYNFBivV4xmvT3Z3rTmyDBg=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@elysiajs/swagger": ["@elysiajs/swagger@github:zoriya/elysia-swagger#f88fbc7", { "dependencies": { "@scalar/themes": "^0.9.81", "@scalar/types": "^0.1.3", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "zoriya-elysia-swagger-f88fbc7"],
|
||||
|
@ -20,9 +20,10 @@
|
||||
"sharp": "^0.34.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"@types/pg": "^8.15.2",
|
||||
"node-addon-api": "^8.3.1",
|
||||
"bun-types": "^1.2.14"
|
||||
"bun-types": "^1.2.14",
|
||||
"node-addon-api": "^8.3.1"
|
||||
},
|
||||
"module": "src/index.js",
|
||||
"patchedDependencies": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type SQL, and, desc, eq, isNotNull, ne, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, isNotNull, ne, type SQL, sql } from "drizzle-orm";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { auth } from "~/auth";
|
||||
import { db } from "~/db";
|
||||
@ -31,14 +31,14 @@ import { KError } from "~/models/error";
|
||||
import { madeInAbyss } from "~/models/examples";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
Filter,
|
||||
type FilterDef,
|
||||
Page,
|
||||
Sort,
|
||||
createPage,
|
||||
isUuid,
|
||||
keysetPaginate,
|
||||
Page,
|
||||
processLanguages,
|
||||
Sort,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import { desc as description } from "~/models/utils/descriptions";
|
||||
@ -146,7 +146,7 @@ export const mapProgress = ({ aliased }: { aliased: boolean }) => {
|
||||
const ret = {
|
||||
time: coalesce(time, sql<number>`0`),
|
||||
percent: coalesce(percent, sql<number>`0`),
|
||||
playedDate: sql<string>`${playedDate}`,
|
||||
playedDate: sql`to_char(${playedDate}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
videoId: sql<string>`${videoId}`,
|
||||
};
|
||||
if (!aliased) return ret;
|
||||
|
@ -1,28 +1,24 @@
|
||||
import type { Stats } from "node:fs";
|
||||
import type { BunFile, S3File, S3Stats } from "bun";
|
||||
import { type SQL, and, eq, sql } from "drizzle-orm";
|
||||
import type { S3Stats } from "bun";
|
||||
import { and, eq, type SQL, sql } from "drizzle-orm";
|
||||
import Elysia, { type Context, t } from "elysia";
|
||||
import { prefix } from "~/base";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
showTranslations,
|
||||
shows,
|
||||
showTranslations,
|
||||
staff,
|
||||
studioTranslations,
|
||||
studios,
|
||||
studioTranslations,
|
||||
} from "~/db/schema";
|
||||
import { sqlarr } from "~/db/utils";
|
||||
import { KError } from "~/models/error";
|
||||
import { bubble } from "~/models/examples";
|
||||
import { AcceptLanguage, isUuid, processLanguages } from "~/models/utils";
|
||||
import { getFile } from "~/utils";
|
||||
import { comment, getFile } from "~/utils";
|
||||
import { imageDir } from "./seed/images";
|
||||
|
||||
function getRedirectToImageHandler({
|
||||
filter,
|
||||
}: {
|
||||
filter?: SQL;
|
||||
}) {
|
||||
function getRedirectToImageHandler({ filter }: { filter?: SQL }) {
|
||||
return async function Handler({
|
||||
params: { id, image },
|
||||
headers: { "accept-language": languages },
|
||||
@ -97,11 +93,30 @@ function getRedirectToImageHandler({
|
||||
export const imagesH = new Elysia({ tags: ["images"] })
|
||||
.get(
|
||||
"/images/:id",
|
||||
async ({ params: { id }, query: { quality }, headers: reqHeaders }) => {
|
||||
async ({
|
||||
params: { id },
|
||||
query: { quality },
|
||||
headers: reqHeaders,
|
||||
status,
|
||||
}) => {
|
||||
const path = `${imageDir}/${id}.${quality}.jpg`;
|
||||
const file = getFile(path);
|
||||
|
||||
const etag = await generateETag(file);
|
||||
const stat = await file.stat().catch(() => undefined);
|
||||
if (!stat) {
|
||||
return status(404, {
|
||||
status: 404,
|
||||
message: comment`
|
||||
No image available with this ID.
|
||||
Either the id is invalid or the image has not been downloaded yet.
|
||||
`,
|
||||
});
|
||||
}
|
||||
const etag =
|
||||
"etag" in stat
|
||||
? stat.etag
|
||||
: Buffer.from(stat.mtime.toISOString(), "utf8").toString("base64");
|
||||
|
||||
if (await isCached(reqHeaders, etag, path))
|
||||
return new Response(null, { status: 304 });
|
||||
|
||||
@ -387,10 +402,3 @@ export async function isCached(
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function generateETag(file: BunFile | S3File) {
|
||||
const hash = new Bun.CryptoHasher("md5");
|
||||
hash.update(await file.arrayBuffer());
|
||||
|
||||
return hash.digest("base64");
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ import { KError } from "~/models/error";
|
||||
import { SeedHistory } from "~/models/history";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
Filter,
|
||||
isUuid,
|
||||
Page,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -8,13 +8,13 @@ import { getColumns, sqlarr } from "~/db/utils";
|
||||
import { Entry } from "~/models/entry";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
Filter,
|
||||
type FilterDef,
|
||||
Page,
|
||||
Sort,
|
||||
createPage,
|
||||
keysetPaginate,
|
||||
Page,
|
||||
processLanguages,
|
||||
Sort,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -18,11 +18,11 @@ import { Movie } from "~/models/movie";
|
||||
import { Serie } from "~/models/serie";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
DbMetadata,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
isUuid,
|
||||
Page,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { db } from "~/db";
|
||||
import { seasonTranslations, seasons, shows } from "~/db/schema";
|
||||
import { seasons, seasonTranslations, shows } from "~/db/schema";
|
||||
import { getColumns, sqlarr } from "~/db/utils";
|
||||
import { KError } from "~/models/error";
|
||||
import { madeInAbyss } from "~/models/examples";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
Filter,
|
||||
type FilterDef,
|
||||
Page,
|
||||
Sort,
|
||||
createPage,
|
||||
isUuid,
|
||||
keysetPaginate,
|
||||
Page,
|
||||
processLanguages,
|
||||
Sort,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -1,11 +1,11 @@
|
||||
import path from "node:path";
|
||||
import { encode } from "blurhash";
|
||||
import { type SQL, and, eq, is, lt, sql } from "drizzle-orm";
|
||||
import { and, eq, is, lt, type SQL, sql } from "drizzle-orm";
|
||||
import { PgColumn, type PgTable } from "drizzle-orm/pg-core";
|
||||
import { version } from "package.json";
|
||||
import type { PoolClient } from "pg";
|
||||
import sharp from "sharp";
|
||||
import { type Transaction, db } from "~/db";
|
||||
import { db, type Transaction } from "~/db";
|
||||
import { mqueue } from "~/db/schema/mqueue";
|
||||
import type { Image } from "~/models/utils";
|
||||
import { getFile } from "~/utils";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import { showTranslations, shows } from "~/db/schema";
|
||||
import { shows, showTranslations } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import type { SeedCollection } from "~/models/collections";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type Column, type SQL, eq, sql } from "drizzle-orm";
|
||||
import { type Column, eq, type SQL, sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
entries,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { db } from "~/db";
|
||||
import { seasonTranslations, seasons } from "~/db/schema";
|
||||
import { seasons, seasonTranslations } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import type { SeedSeason } from "~/models/season";
|
||||
import { enqueueOptImage } from "../images";
|
||||
|
@ -1,15 +1,15 @@
|
||||
import {
|
||||
type SQLWrapper,
|
||||
and,
|
||||
count,
|
||||
eq,
|
||||
exists,
|
||||
isNull,
|
||||
ne,
|
||||
type SQLWrapper,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { type Transaction, db } from "~/db";
|
||||
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
|
||||
import { db, type Transaction } from "~/db";
|
||||
import { entries, entryVideoJoin, shows, showTranslations } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept, sqlarr } from "~/db/utils";
|
||||
import type { SeedCollection } from "~/models/collections";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { db } from "~/db";
|
||||
import { showStudioJoin, studioTranslations, studios } from "~/db/schema";
|
||||
import { showStudioJoin, studios, studioTranslations } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import type { SeedStudio } from "~/models/studio";
|
||||
import { enqueueOptImage } from "../images";
|
||||
|
@ -1,7 +1,7 @@
|
||||
// oh i hate js dates so much.
|
||||
export const guessNextRefresh = (airDate: Date | string) => {
|
||||
if (typeof airDate === "string") airDate = new Date(airDate);
|
||||
const diff = new Date().getTime() - airDate.getTime();
|
||||
const diff = Date.now() - airDate.getTime();
|
||||
const days = diff / (24 * 60 * 60 * 1000);
|
||||
|
||||
const ret = new Date();
|
||||
|
@ -16,10 +16,10 @@ import { Serie } from "~/models/serie";
|
||||
import { Show } from "~/models/show";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
Filter,
|
||||
isUuid,
|
||||
Page,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type SQL, and, eq, exists, gt, ne, sql } from "drizzle-orm";
|
||||
import { and, eq, exists, ne, type SQL, sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
entries,
|
||||
@ -6,10 +6,10 @@ import {
|
||||
entryVideoJoin,
|
||||
profiles,
|
||||
showStudioJoin,
|
||||
showTranslations,
|
||||
shows,
|
||||
studioTranslations,
|
||||
showTranslations,
|
||||
studios,
|
||||
studioTranslations,
|
||||
videos,
|
||||
} from "~/db/schema";
|
||||
import { watchlist } from "~/db/schema/watchlist";
|
||||
@ -26,12 +26,12 @@ import type { MovieStatus } from "~/models/movie";
|
||||
import { SerieStatus, type SerieTranslation } from "~/models/serie";
|
||||
import type { Studio } from "~/models/studio";
|
||||
import {
|
||||
buildRelations,
|
||||
type FilterDef,
|
||||
Genre,
|
||||
type Image,
|
||||
Sort,
|
||||
buildRelations,
|
||||
keysetPaginate,
|
||||
Sort,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import type { EmbeddedVideo } from "~/models/video";
|
||||
@ -49,6 +49,11 @@ export const watchStatusQ = db
|
||||
.as("watchstatus");
|
||||
|
||||
export const showFilters: FilterDef = {
|
||||
kind: {
|
||||
column: shows.kind,
|
||||
type: "enum",
|
||||
values: ["serie", "movie", "collection"],
|
||||
},
|
||||
genres: {
|
||||
column: shows.genres,
|
||||
type: "enum",
|
||||
@ -81,6 +86,11 @@ export const showFilters: FilterDef = {
|
||||
export const showSort = Sort(
|
||||
{
|
||||
slug: shows.slug,
|
||||
name: {
|
||||
sql: sql.raw(`t.${showTranslations.name.name}`),
|
||||
isNullable: false,
|
||||
accessor: (x) => x.name,
|
||||
},
|
||||
rating: shows.rating,
|
||||
airDate: shows.startAir,
|
||||
startAir: shows.startAir,
|
||||
@ -111,7 +121,7 @@ const showRelations = {
|
||||
.as("translations");
|
||||
},
|
||||
studios: ({ languages }: { languages: string[] }) => {
|
||||
const { pk: _, ...studioCol } = getColumns(studios);
|
||||
const { pk: _, createdAt, updatedAt, ...studioCol } = getColumns(studios);
|
||||
const studioTransQ = db
|
||||
.selectDistinctOn([studioTranslations.pk])
|
||||
.from(studioTranslations)
|
||||
@ -125,7 +135,14 @@ const showRelations = {
|
||||
return db
|
||||
.select({
|
||||
json: coalesce(
|
||||
jsonbAgg(jsonbBuildObject<Studio>({ ...studioTrans, ...studioCol })),
|
||||
jsonbAgg(
|
||||
jsonbBuildObject<Studio>({
|
||||
...studioTrans,
|
||||
...studioCol,
|
||||
createdAt: sql`to_char(${createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
updatedAt: sql`to_char(${updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
}),
|
||||
),
|
||||
sql`'[]'::jsonb`,
|
||||
).as("json"),
|
||||
})
|
||||
@ -261,6 +278,8 @@ export async function getShows({
|
||||
.orderBy(
|
||||
showTranslations.pk,
|
||||
sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
|
||||
// ensure a stable sort to prevent future pages to contains the same element again
|
||||
showTranslations.language,
|
||||
)
|
||||
.as("t");
|
||||
|
||||
|
@ -9,10 +9,10 @@ import { bubble } from "~/models/examples";
|
||||
import { FullMovie, Movie, MovieTranslation } from "~/models/movie";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
Filter,
|
||||
isUuid,
|
||||
Page,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -9,10 +9,10 @@ import { madeInAbyss } from "~/models/examples";
|
||||
import { FullSerie, Serie, SerieTranslation } from "~/models/serie";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
Filter,
|
||||
isUuid,
|
||||
Page,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -8,9 +8,9 @@ import { KError } from "~/models/error";
|
||||
import { Show } from "~/models/show";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { type SQL, and, eq, sql } from "drizzle-orm";
|
||||
import { and, eq, type SQL, sql } from "drizzle-orm";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { auth } from "~/auth";
|
||||
import { prefix } from "~/base";
|
||||
import { db } from "~/db";
|
||||
import { profiles, showTranslations, shows } from "~/db/schema";
|
||||
import { profiles, shows, showTranslations } from "~/db/schema";
|
||||
import { roles, staff } from "~/db/schema/staff";
|
||||
import { watchlist } from "~/db/schema/watchlist";
|
||||
import { getColumns, jsonbBuildObject, sqlarr } from "~/db/utils";
|
||||
@ -13,15 +13,15 @@ import { Role, Staff } from "~/models/staff";
|
||||
import { RoleWShow, RoleWStaff } from "~/models/staff-roles";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
Filter,
|
||||
type FilterDef,
|
||||
type Image,
|
||||
Page,
|
||||
Sort,
|
||||
createPage,
|
||||
isUuid,
|
||||
keysetPaginate,
|
||||
Page,
|
||||
processLanguages,
|
||||
Sort,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
|
||||
import { and, eq, exists, type SQL, sql } from "drizzle-orm";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { auth } from "~/auth";
|
||||
import { prefix } from "~/base";
|
||||
@ -6,8 +6,8 @@ import { db } from "~/db";
|
||||
import {
|
||||
showStudioJoin,
|
||||
shows,
|
||||
studioTranslations,
|
||||
studios,
|
||||
studioTranslations,
|
||||
} from "~/db/schema";
|
||||
import {
|
||||
getColumns,
|
||||
@ -22,14 +22,14 @@ import { Show } from "~/models/show";
|
||||
import { Studio, StudioTranslation } from "~/models/studio";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
Filter,
|
||||
Page,
|
||||
Sort,
|
||||
buildRelations,
|
||||
createPage,
|
||||
Filter,
|
||||
isUuid,
|
||||
keysetPaginate,
|
||||
Page,
|
||||
processLanguages,
|
||||
Sort,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { and, eq, notExists, or, sql } from "drizzle-orm";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { type Transaction, db } from "~/db";
|
||||
import { db, type Transaction } from "~/db";
|
||||
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||
import {
|
||||
conflictUpdateAllExcept,
|
||||
@ -13,12 +13,12 @@ import {
|
||||
import { KError } from "~/models/error";
|
||||
import { bubbleVideo } from "~/models/examples";
|
||||
import {
|
||||
Page,
|
||||
type Resource,
|
||||
Sort,
|
||||
createPage,
|
||||
isUuid,
|
||||
keysetPaginate,
|
||||
Page,
|
||||
type Resource,
|
||||
Sort,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import { desc as description } from "~/models/utils/descriptions";
|
||||
|
@ -8,11 +8,11 @@ import {
|
||||
primaryKey,
|
||||
real,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { image, language, schema } from "./utils";
|
||||
import { entryVideoJoin } from "./videos";
|
||||
@ -67,14 +67,14 @@ export const entries = schema.table(
|
||||
|
||||
externalId: entry_extid(),
|
||||
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
.default(sql`now()`),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.$onUpdate(() => sql`now()`),
|
||||
availableSince: timestamp({ withTimezone: true, mode: "string" }),
|
||||
nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(),
|
||||
availableSince: timestamp({ withTimezone: true, mode: "iso" }),
|
||||
nextRefresh: timestamp({ withTimezone: true, mode: "iso" }).notNull(),
|
||||
},
|
||||
(t) => [
|
||||
unique().on(t.showPk, t.seasonNumber, t.episodeNumber),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, index, integer, timestamp } from "drizzle-orm/pg-core";
|
||||
import { check, index, integer } from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { profiles } from "./profiles";
|
||||
import { schema } from "./utils";
|
||||
@ -18,9 +19,9 @@ export const history = schema.table(
|
||||
videoPk: integer().references(() => videos.pk, { onDelete: "set null" }),
|
||||
percent: integer().notNull().default(0),
|
||||
time: integer(),
|
||||
playedDate: timestamp({ withTimezone: true, mode: "string" })
|
||||
playedDate: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
.default(sql`now()`),
|
||||
},
|
||||
(t) => [
|
||||
index("history_play_date").on(t.playedDate.desc()),
|
||||
|
@ -1,9 +1,9 @@
|
||||
export * from "./entries";
|
||||
export * from "./seasons";
|
||||
export * from "./shows";
|
||||
export * from "./studios";
|
||||
export * from "./staff";
|
||||
export * from "./videos";
|
||||
export * from "./profiles";
|
||||
export * from "./history";
|
||||
export * from "./mqueue";
|
||||
export * from "./profiles";
|
||||
export * from "./seasons";
|
||||
export * from "./shows";
|
||||
export * from "./staff";
|
||||
export * from "./studios";
|
||||
export * from "./videos";
|
||||
|
@ -1,11 +1,6 @@
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { index, integer, jsonb, uuid, varchar } from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { schema } from "./utils";
|
||||
|
||||
export const mqueue = schema.table(
|
||||
@ -15,9 +10,9 @@ export const mqueue = schema.table(
|
||||
kind: varchar({ length: 255 }).notNull(),
|
||||
message: jsonb().notNull(),
|
||||
attempt: integer().notNull().default(0),
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
.default(sql`now()`),
|
||||
},
|
||||
(t) => [index("mqueue_created").on(t.createdAt)],
|
||||
);
|
||||
|
@ -6,11 +6,11 @@ import {
|
||||
jsonb,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { image, language, schema } from "./utils";
|
||||
|
||||
@ -42,13 +42,13 @@ export const seasons = schema.table(
|
||||
|
||||
externalId: season_extid(),
|
||||
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
.default(sql`now()`),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.$onUpdate(() => sql`now()`),
|
||||
nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(),
|
||||
nextRefresh: timestamp({ withTimezone: true, mode: "iso" }).notNull(),
|
||||
},
|
||||
(t) => [
|
||||
unique().on(t.showPk, t.seasonNumber),
|
||||
|
@ -9,11 +9,11 @@ import {
|
||||
primaryKey,
|
||||
smallint,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { Image, Original } from "~/models/utils";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { seasons } from "./seasons";
|
||||
import { roles } from "./staff";
|
||||
@ -87,13 +87,13 @@ export const shows = schema.table(
|
||||
|
||||
externalId: externalid(),
|
||||
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
.default(sql`now()`),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.$onUpdate(() => sql`now()`),
|
||||
nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(),
|
||||
nextRefresh: timestamp({ withTimezone: true, mode: "iso" }).notNull(),
|
||||
},
|
||||
(t) => [
|
||||
check("rating_valid", sql`${t.rating} between 0 and 100`),
|
||||
|
@ -3,13 +3,12 @@ import {
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { Character } from "~/models/staff";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { externalid, image, schema } from "./utils";
|
||||
|
||||
@ -32,10 +31,10 @@ export const staff = schema.table("staff", {
|
||||
image: image(),
|
||||
externalId: externalid(),
|
||||
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
.default(sql`now()`),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.$onUpdate(() => sql`now()`),
|
||||
});
|
||||
|
@ -4,10 +4,10 @@ import {
|
||||
integer,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { externalid, image, language, schema } from "./utils";
|
||||
|
||||
@ -17,10 +17,10 @@ export const studios = schema.table("studios", {
|
||||
slug: varchar({ length: 255 }).notNull().unique(),
|
||||
externalId: externalid(),
|
||||
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
.default(sql`now()`),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.$onUpdate(() => sql`now()`),
|
||||
});
|
||||
|
@ -5,12 +5,12 @@ import {
|
||||
jsonb,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { Guess } from "~/models/video";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { schema } from "./utils";
|
||||
|
||||
@ -25,10 +25,10 @@ export const videos = schema.table(
|
||||
version: integer().notNull().default(1),
|
||||
guess: jsonb().$type<Guess>().notNull(),
|
||||
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
.default(sql`now()`),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.$onUpdate(() => sql`now()`),
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, integer, primaryKey, timestamp } from "drizzle-orm/pg-core";
|
||||
import { check, integer, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { profiles } from "./profiles";
|
||||
import { shows } from "./shows";
|
||||
@ -29,14 +30,14 @@ export const watchlist = schema.table(
|
||||
|
||||
score: integer(),
|
||||
|
||||
startedAt: timestamp({ withTimezone: true, mode: "string" }),
|
||||
lastPlayedAt: timestamp({ withTimezone: true, mode: "string" }),
|
||||
completedAt: timestamp({ withTimezone: true, mode: "string" }),
|
||||
startedAt: timestamp({ withTimezone: true, mode: "iso" }),
|
||||
lastPlayedAt: timestamp({ withTimezone: true, mode: "iso" }),
|
||||
completedAt: timestamp({ withTimezone: true, mode: "iso" }),
|
||||
|
||||
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "string" })
|
||||
.default(sql`now()`),
|
||||
updatedAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.$onUpdate(() => sql`now()`),
|
||||
},
|
||||
|
@ -1,19 +1,23 @@
|
||||
import {
|
||||
type Column,
|
||||
type ColumnsSelection,
|
||||
getTableColumns,
|
||||
is,
|
||||
type SQL,
|
||||
type SQLWrapper,
|
||||
type Subquery,
|
||||
sql,
|
||||
Table,
|
||||
View,
|
||||
ViewBaseConfig,
|
||||
getTableColumns,
|
||||
is,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import type { CasingCache } from "drizzle-orm/casing";
|
||||
import type { AnyMySqlSelect } from "drizzle-orm/mysql-core";
|
||||
import type { AnyPgSelect, SelectedFieldsFlat } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
type AnyPgSelect,
|
||||
customType,
|
||||
type SelectedFieldsFlat,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { AnySQLiteSelect } from "drizzle-orm/sqlite-core";
|
||||
import type { WithSubquery } from "drizzle-orm/subquery";
|
||||
import { db } from "./index";
|
||||
@ -148,3 +152,19 @@ export const isUniqueConstraint = (e: unknown): boolean => {
|
||||
typeof e === "object" && e != null && "code" in e && e.code === "23505"
|
||||
);
|
||||
};
|
||||
|
||||
export const timestamp = customType<{
|
||||
data: string;
|
||||
driverData: string;
|
||||
config: { withTimezone: boolean; precision?: number; mode: "iso" };
|
||||
}>({
|
||||
dataType(config) {
|
||||
const precision = config?.precision ? ` (${config.precision})` : "";
|
||||
return `timestamp${precision}${config?.withTimezone ? " with time zone" : ""}`;
|
||||
},
|
||||
fromDriver(value: string): string {
|
||||
// postgres format: 2025-06-22 16:13:37.489301+00
|
||||
// what we want: 2025-06-22T16:13:37Z
|
||||
return `${value.substring(0, 10)}T${value.substring(11, 19)}Z`;
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { comment, type Prettify } from "~/utils";
|
||||
import { madeInAbyss, registerExamples } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import { DbMetadata, SeedImage } from "../utils";
|
||||
|
@ -13,6 +13,6 @@ export type SeedEntry = SeedEpisode | SeedMovieEntry | SeedSpecial;
|
||||
export type EntryKind = Entry["kind"] | Extra["kind"];
|
||||
|
||||
export * from "./episode";
|
||||
export * from "./extra";
|
||||
export * from "./movie-entry";
|
||||
export * from "./special";
|
||||
export * from "./extra";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { comment, type Prettify } from "~/utils";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { comment, type Prettify } from "~/utils";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import {
|
||||
|
@ -31,7 +31,7 @@ export const registerExamples = <T extends TSchema>(
|
||||
};
|
||||
|
||||
export * from "./bubble";
|
||||
export * from "./made-in-abyss";
|
||||
export * from "./dune-1984";
|
||||
export * from "./dune-2021";
|
||||
export * from "./dune-collection";
|
||||
export * from "./made-in-abyss";
|
||||
|
@ -23,7 +23,10 @@ export type FilterDef = {
|
||||
export const Filter = ({
|
||||
def,
|
||||
description = "Filters to apply to the query.",
|
||||
}: { def: FilterDef; description?: string }) =>
|
||||
}: {
|
||||
def: FilterDef;
|
||||
description?: string;
|
||||
}) =>
|
||||
t
|
||||
.Transform(
|
||||
t.String({
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {
|
||||
type Parjser,
|
||||
anyStringOf,
|
||||
digit,
|
||||
float,
|
||||
int,
|
||||
letter,
|
||||
noCharOf,
|
||||
type Parjser,
|
||||
string,
|
||||
} from "parjs";
|
||||
import {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {
|
||||
type BinaryOperator,
|
||||
type SQL,
|
||||
and,
|
||||
type BinaryOperator,
|
||||
eq,
|
||||
gt,
|
||||
gte,
|
||||
@ -10,6 +9,7 @@ import {
|
||||
ne,
|
||||
not,
|
||||
or,
|
||||
type SQL,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { KErrorT } from "~/models/error";
|
||||
|
@ -1,12 +1,12 @@
|
||||
export * from "./db-metadata";
|
||||
export * from "./external-id";
|
||||
export * from "./filters";
|
||||
export * from "./genres";
|
||||
export * from "./image";
|
||||
export * from "./language";
|
||||
export * from "./resource";
|
||||
export * from "./filters";
|
||||
export * from "./page";
|
||||
export * from "./sort";
|
||||
export * from "./keyset-paginate";
|
||||
export * from "./db-metadata";
|
||||
export * from "./language";
|
||||
export * from "./original";
|
||||
export * from "./page";
|
||||
export * from "./relations";
|
||||
export * from "./resource";
|
||||
export * from "./sort";
|
||||
|
@ -4,9 +4,7 @@ import {
|
||||
type TSchema,
|
||||
type TString,
|
||||
} from "@sinclair/typebox";
|
||||
import { type Column, type Table, eq, sql } from "drizzle-orm";
|
||||
import { t } from "elysia";
|
||||
import { sqlarr } from "~/db/utils";
|
||||
import { comment } from "../../utils";
|
||||
import { KErrorT } from "../error";
|
||||
|
||||
@ -74,7 +72,7 @@ export const TranslationRecord = <T extends TSchema>(
|
||||
.Encode((x) => x);
|
||||
|
||||
export const processLanguages = (languages?: string) => {
|
||||
if (!languages) return ["*"];
|
||||
if (!languages) return ["en", "*"];
|
||||
return languages
|
||||
.split(",")
|
||||
.map((x) => {
|
||||
@ -91,9 +89,11 @@ export const processLanguages = (languages?: string) => {
|
||||
|
||||
export const AcceptLanguage = ({
|
||||
autoFallback = false,
|
||||
}: { autoFallback?: boolean } = {}) =>
|
||||
}: {
|
||||
autoFallback?: boolean;
|
||||
} = {}) =>
|
||||
t.String({
|
||||
default: "*",
|
||||
default: "en, *",
|
||||
example: "en-us, ja;q=0.5",
|
||||
description:
|
||||
comment`
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PatternStringExact, type TSchema } from "@sinclair/typebox";
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { comment, type Prettify } from "~/utils";
|
||||
import { ExtraType } from "./entry/extra";
|
||||
import { bubble, bubbleVideo, registerExamples } from "./examples";
|
||||
import { DbMetadata, EpisodeId, ExternalId, Resource } from "./utils";
|
||||
|
@ -1,8 +1,7 @@
|
||||
export * from "~/base";
|
||||
export * from "./movies-helper";
|
||||
export * from "./series-helper";
|
||||
export * from "./shows-helper";
|
||||
export * from "./studio-helper";
|
||||
export * from "./staff-helper";
|
||||
export * from "./studio-helper";
|
||||
export * from "./videos-helper";
|
||||
|
||||
export * from "~/base";
|
||||
|
@ -45,7 +45,11 @@ export const getSerie = async (
|
||||
export const getSeries = async ({
|
||||
langs,
|
||||
...query
|
||||
}: { langs?: string; preferOriginal?: boolean; with?: string[] }) => {
|
||||
}: {
|
||||
langs?: string;
|
||||
preferOriginal?: boolean;
|
||||
with?: string[];
|
||||
}) => {
|
||||
const resp = await handlers.handle(
|
||||
new Request(buildUrl("series", query), {
|
||||
method: "GET",
|
||||
|
@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { showTranslations, shows, videos } from "~/db/schema";
|
||||
import { shows, showTranslations, videos } from "~/db/schema";
|
||||
import { bubble } from "~/models/examples";
|
||||
import { dune, duneVideo } from "~/models/examples/dune-2021";
|
||||
import { createMovie, createVideo } from "../helpers";
|
||||
|
@ -36,7 +36,7 @@ describe("Set & get watch status", () => {
|
||||
expect(body.items[0].slug).toBe(bubble.slug);
|
||||
expect(body.items[0].watchStatus).toMatchObject({
|
||||
status: "completed",
|
||||
completedAt: "2024-12-21 00:00:00+00",
|
||||
completedAt: "2024-12-21T00:00:00Z",
|
||||
score: 85,
|
||||
percent: 100,
|
||||
});
|
||||
@ -61,7 +61,7 @@ describe("Set & get watch status", () => {
|
||||
expect(body.items[0].slug).toBe(bubble.slug);
|
||||
expect(body.items[0].watchStatus).toMatchObject({
|
||||
status: "rewatching",
|
||||
completedAt: "2024-12-21 00:00:00+00",
|
||||
completedAt: "2024-12-21T00:00:00Z",
|
||||
score: 85,
|
||||
percent: 0,
|
||||
});
|
||||
@ -89,7 +89,7 @@ describe("Set & get watch status", () => {
|
||||
expect(body.items[0].slug).toBe(bubble.slug);
|
||||
expect(body.items[0].watchStatus).toMatchObject({
|
||||
status: "rewatching",
|
||||
completedAt: "2024-12-21 00:00:00+00",
|
||||
completedAt: "2024-12-21T00:00:00Z",
|
||||
score: 85,
|
||||
percent: 0,
|
||||
});
|
||||
@ -109,7 +109,7 @@ describe("Set & get watch status", () => {
|
||||
expect(body.slug).toBe(bubble.slug);
|
||||
expect(body.watchStatus).toMatchObject({
|
||||
status: "rewatching",
|
||||
completedAt: "2024-12-21 00:00:00+00",
|
||||
completedAt: "2024-12-21T00:00:00Z",
|
||||
score: 85,
|
||||
percent: 0,
|
||||
});
|
||||
|
@ -109,7 +109,7 @@ describe("Set & get history", () => {
|
||||
percent: 100,
|
||||
time: 38 * 60,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
playedDate: "2025-02-03 00:00:00+00",
|
||||
playedDate: "2025-02-03T00:00:00Z",
|
||||
});
|
||||
});
|
||||
|
||||
@ -122,7 +122,7 @@ describe("Set & get history", () => {
|
||||
percent: 100,
|
||||
time: 38 * 60,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
playedDate: "2025-02-03 00:00:00+00",
|
||||
playedDate: "2025-02-03T00:00:00Z",
|
||||
});
|
||||
});
|
||||
|
||||
@ -137,13 +137,13 @@ describe("Set & get history", () => {
|
||||
expect(body.items[0].watchStatus).toMatchObject({
|
||||
status: "watching",
|
||||
seenCount: 1,
|
||||
startedAt: "2025-02-01 00:00:00+00",
|
||||
startedAt: "2025-02-01T00:00:00Z",
|
||||
});
|
||||
expect(body.items[1].slug).toBe(bubble.slug);
|
||||
expect(body.items[1].watchStatus).toMatchObject({
|
||||
status: "completed",
|
||||
percent: 100,
|
||||
completedAt: "2025-02-02 00:00:00+00",
|
||||
completedAt: "2025-02-02T00:00:00Z",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -113,7 +113,7 @@ describe("nextup", () => {
|
||||
percent: 58,
|
||||
time: 28 * 60 + 12,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
playedDate: "2025-02-01T00:00:00+00:00",
|
||||
playedDate: "2025-02-01T00:00:00Z",
|
||||
});
|
||||
|
||||
[resp, body] = await getMovie(bubble.slug, {});
|
||||
@ -121,7 +121,7 @@ describe("nextup", () => {
|
||||
expect(body.watchStatus).toMatchObject({
|
||||
percent: 100,
|
||||
status: "completed",
|
||||
completedAt: "2025-02-02 00:00:00+00",
|
||||
completedAt: "2025-02-02T00:00:00Z",
|
||||
});
|
||||
|
||||
[resp, body] = await getNextup("me", {});
|
||||
@ -132,7 +132,7 @@ describe("nextup", () => {
|
||||
percent: 58,
|
||||
time: 28 * 60 + 12,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
playedDate: "2025-02-01 00:00:00+00",
|
||||
playedDate: "2025-02-01T00:00:00Z",
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from "tests/helpers";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||
import { entries, shows, videos } from "~/db/schema";
|
||||
import { bubble, madeInAbyss } from "~/models/examples";
|
||||
|
||||
beforeAll(async () => {
|
||||
|
@ -10,6 +10,6 @@ pkgs.mkShell {
|
||||
postgresql_15
|
||||
pgformatter
|
||||
# to run tests
|
||||
hurl
|
||||
# hurl
|
||||
];
|
||||
}
|
||||
|
23
biome.json
23
biome.json
@ -1,18 +1,15 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts", "**/back/**"]
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
@ -24,16 +21,20 @@
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
"noArrayIndexKey": "off",
|
||||
"noTemplateCurlyInString": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": "off"
|
||||
"noBannedTypes": "off",
|
||||
"noUselessUndefinedInitialization": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "off"
|
||||
}
|
||||
},
|
||||
"ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts", "**/back/**"]
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
|
@ -25,26 +25,25 @@ x-transcoder: &transcoder-base
|
||||
target: /app
|
||||
|
||||
services:
|
||||
# front:
|
||||
# build:
|
||||
# context: ./front
|
||||
# dockerfile: Dockerfile.dev
|
||||
# volumes:
|
||||
# - ./front:/app
|
||||
# - /app/.yarn
|
||||
# - /app/node_modules
|
||||
# - /app/apps/mobile/node_modules
|
||||
# - /app/apps/web/.next/
|
||||
# - /app/apps/mobile/.expo/
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
# - "8081:8081"
|
||||
# restart: unless-stopped
|
||||
# environment:
|
||||
# - KYOO_URL=${KYOO_URL:-http://api:5000/api}
|
||||
# labels:
|
||||
# - "traefik.enable=true"
|
||||
# - "traefik.http.routers.front.rule=PathPrefix(`/`)"
|
||||
front:
|
||||
build:
|
||||
context: ./front
|
||||
dockerfile: Dockerfile.dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:8081"
|
||||
environment:
|
||||
- KYOO_URL=${KYOO_URL:-http://api:5000/api}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
|
||||
develop:
|
||||
watch:
|
||||
- action: sync
|
||||
path: ./front
|
||||
target: /app
|
||||
- action: rebuild
|
||||
path: ./front/bun.lock
|
||||
|
||||
auth:
|
||||
build:
|
||||
@ -82,6 +81,8 @@ services:
|
||||
- JWT_ISSUER=${PUBLIC_URL}
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- images:/app/images
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.api.rule=PathPrefix(`/api/`) || PathPrefix(`/swagger`)"
|
||||
@ -95,7 +96,7 @@ services:
|
||||
path: ./api
|
||||
target: /app
|
||||
- action: rebuild
|
||||
path: ./api/packages.json
|
||||
path: ./api/bun.lock
|
||||
|
||||
scanner:
|
||||
build: ./scanner
|
||||
@ -206,4 +207,5 @@ services:
|
||||
|
||||
volumes:
|
||||
db:
|
||||
images:
|
||||
transcoder_metadata:
|
||||
|
@ -1,14 +1,9 @@
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
||||
.dockerignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.expo
|
||||
.git
|
||||
.yarn
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
**
|
||||
!/package.json
|
||||
!/bun.lock
|
||||
!/tsconfig.json
|
||||
!/metro.config.js
|
||||
!/app.config.ts
|
||||
!/src
|
||||
!/app
|
||||
!/public
|
||||
|
2
front/.gitattributes
vendored
2
front/.gitattributes
vendored
@ -1,2 +0,0 @@
|
||||
/.yarn/releases/** binary
|
||||
/.yarn/plugins/** binary
|
8
front/.gitignore
vendored
8
front/.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
dist/
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
@ -45,3 +45,9 @@ yarn-error.log*
|
||||
|
||||
|
||||
apps/web/next-env.d.ts
|
||||
|
||||
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
|
||||
# The following patterns were generated by expo-cli
|
||||
|
||||
expo-env.d.ts
|
||||
# @end expo-cli
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
801
front/.yarn/releases/yarn-3.2.4.cjs
vendored
801
front/.yarn/releases/yarn-3.2.4.cjs
vendored
File diff suppressed because one or more lines are too long
20
front/.yarn/sdks/eslint/bin/eslint.js
vendored
20
front/.yarn/sdks/eslint/bin/eslint.js
vendored
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint/bin/eslint.js your application uses
|
||||
module.exports = absRequire(`eslint/bin/eslint.js`);
|
20
front/.yarn/sdks/eslint/lib/api.js
vendored
20
front/.yarn/sdks/eslint/lib/api.js
vendored
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint your application uses
|
||||
module.exports = absRequire(`eslint`);
|
6
front/.yarn/sdks/eslint/package.json
vendored
6
front/.yarn/sdks/eslint/package.json
vendored
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "eslint",
|
||||
"version": "8.19.0-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs"
|
||||
}
|
3
front/.yarn/sdks/integrations.yml
vendored
3
front/.yarn/sdks/integrations.yml
vendored
@ -1,3 +0,0 @@
|
||||
# This file is automatically generated by @yarnpkg/sdks.
|
||||
# Manual changes might be lost!
|
||||
|
20
front/.yarn/sdks/prettier/index.js
vendored
20
front/.yarn/sdks/prettier/index.js
vendored
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier/index.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real prettier/index.js your application uses
|
||||
module.exports = absRequire(`prettier/index.js`);
|
6
front/.yarn/sdks/prettier/package.json
vendored
6
front/.yarn/sdks/prettier/package.json
vendored
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "prettier",
|
||||
"version": "2.7.1-sdk",
|
||||
"main": "./index.js",
|
||||
"type": "commonjs"
|
||||
}
|
20
front/.yarn/sdks/typescript/bin/tsc
vendored
20
front/.yarn/sdks/typescript/bin/tsc
vendored
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsc
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsc your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsc`);
|
20
front/.yarn/sdks/typescript/bin/tsserver
vendored
20
front/.yarn/sdks/typescript/bin/tsserver
vendored
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsserver
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsserver your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsserver`);
|
20
front/.yarn/sdks/typescript/lib/tsc.js
vendored
20
front/.yarn/sdks/typescript/lib/tsc.js
vendored
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsc.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/tsc.js`);
|
223
front/.yarn/sdks/typescript/lib/tsserver.js
vendored
223
front/.yarn/sdks/typescript/lib/tsserver.js
vendored
@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
223
front/.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
223
front/.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
20
front/.yarn/sdks/typescript/lib/typescript.js
vendored
20
front/.yarn/sdks/typescript/lib/typescript.js
vendored
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/typescript.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/typescript.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/typescript.js`);
|
6
front/.yarn/sdks/typescript/package.json
vendored
6
front/.yarn/sdks/typescript/package.json
vendored
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "4.7.4-sdk",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs"
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
packageExtensions:
|
||||
"@expo/cli@*":
|
||||
dependencies:
|
||||
expo-modules-autolinking: "*"
|
||||
babel-preset-expo@*:
|
||||
dependencies:
|
||||
"@babel/core": "*"
|
||||
expo-asset@*:
|
||||
dependencies:
|
||||
expo: "*"
|
||||
react-native-codegen@*:
|
||||
peerDependenciesMeta:
|
||||
"@babel/preset-env":
|
||||
optional: true
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.4.cjs
|
@ -1,32 +1,12 @@
|
||||
FROM node:18-alpine AS builder
|
||||
FROM oven/bun AS builder
|
||||
WORKDIR /app
|
||||
COPY .yarn ./.yarn
|
||||
COPY .yarnrc.yml ./
|
||||
COPY package.json yarn.lock ./
|
||||
COPY apps/web/package.json apps/web/package.json
|
||||
COPY apps/mobile/package.json apps/mobile/package.json
|
||||
COPY packages/ui/package.json packages/ui/package.json
|
||||
COPY packages/primitives/package.json packages/primitives/package.json
|
||||
COPY packages/models/package.json packages/models/package.json
|
||||
RUN yarn --immutable
|
||||
|
||||
COPY package.json bun.lock .
|
||||
RUN bun install --production
|
||||
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
RUN yarn build:web
|
||||
|
||||
EXPOSE 8081
|
||||
CMD ["bun", "dev"]
|
||||
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/apps/web/.next/standalone/apps/web .
|
||||
COPY --from=builder /app/apps/web/.next/standalone/node_modules ./node_modules
|
||||
COPY --from=builder /app/apps/web/.next/static ./.next/static/
|
||||
COPY --from=builder /app/apps/web/public ./public
|
||||
|
||||
EXPOSE 8901
|
||||
ENV PORT=8901
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
CMD ["node", "server.js"]
|
||||
# TODO: Actually do something there, either nginx or preferably an ssr bun serv
|
||||
|
@ -1,17 +1,10 @@
|
||||
FROM node:18-alpine
|
||||
RUN apk add git bash
|
||||
FROM oven/bun
|
||||
WORKDIR /app
|
||||
COPY .yarn ./.yarn
|
||||
COPY .yarnrc.yml ./
|
||||
COPY package.json yarn.lock ./
|
||||
COPY apps/web/package.json apps/web/package.json
|
||||
COPY apps/mobile/package.json apps/mobile/package.json
|
||||
COPY packages/ui/package.json packages/ui/package.json
|
||||
COPY packages/primitives/package.json packages/primitives/package.json
|
||||
COPY packages/models/package.json packages/models/package.json
|
||||
RUN yarn --immutable
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
EXPOSE 3000
|
||||
COPY package.json bun.lock .
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8081
|
||||
ENTRYPOINT ["yarn", "dev"]
|
||||
CMD ["bun", "dev"]
|
||||
|
76
front/app.config.ts
Normal file
76
front/app.config.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { ExpoConfig } from "expo/config";
|
||||
|
||||
const IS_DEV = process.env.APP_VARIANT === "development";
|
||||
|
||||
export const expo: ExpoConfig = {
|
||||
name: IS_DEV ? "Kyoo Development" : "Kyoo",
|
||||
slug: "kyoo",
|
||||
scheme: "kyoo",
|
||||
version: "1.0.0",
|
||||
newArchEnabled: true,
|
||||
platforms: ["web", "ios", "android"],
|
||||
orientation: "default",
|
||||
icon: "./public/icon-256x256.png",
|
||||
userInterfaceStyle: "automatic",
|
||||
ios: {
|
||||
supportsTablet: true,
|
||||
},
|
||||
android: {
|
||||
package: IS_DEV ? "dev.zoriya.kyoo.dev" : "dev.zoriya.kyoo",
|
||||
adaptiveIcon: {
|
||||
foregroundImage: "./public/icon-256x256.png",
|
||||
backgroundColor: "#eff1f5",
|
||||
},
|
||||
edgeToEdgeEnabled: true,
|
||||
},
|
||||
web: {
|
||||
favicon: "./public/icon-256x256.png",
|
||||
bundler: "metro",
|
||||
},
|
||||
updates: {
|
||||
url: "https://u.expo.dev/55de6b52-c649-4a15-9a45-569ff5ed036c",
|
||||
fallbackToCacheTimeout: 0,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: "sdkVersion",
|
||||
},
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: "55de6b52-c649-4a15-9a45-569ff5ed036c",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
android: {
|
||||
usesCleartextTraffic: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"expo-localization",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
image: "./public/icon-256x256.png",
|
||||
resizeMode: "contain",
|
||||
backgroundColor: "#eff1f5",
|
||||
dark: {
|
||||
image: "./public/icon-256x256.png",
|
||||
resizeMode: "contain",
|
||||
backgroundColor: "#1e1e2e",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
enableNotificationControls: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
experiments: {
|
||||
typedRoutes: true,
|
||||
},
|
||||
};
|
@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// import "ts-node/register"; // Add this to import TypeScript files
|
||||
import type { ExpoConfig } from "expo/config";
|
||||
import { withAndroidManifest } from "expo/config-plugins";
|
||||
|
||||
const IS_DEV = process.env.APP_VARIANT === "development";
|
||||
|
||||
// Defined outside the config because dark splashscreen needs to be platform specific.
|
||||
const splash = {
|
||||
image: "./assets/icon.png",
|
||||
resizeMode: "contain",
|
||||
backgroundColor: "#eff1f5",
|
||||
dark: {
|
||||
image: "./assets/icon.png",
|
||||
resizeMode: "contain",
|
||||
backgroundColor: "#1e1e2e",
|
||||
},
|
||||
} as const;
|
||||
|
||||
const config: ExpoConfig = {
|
||||
name: IS_DEV ? "Kyoo Development" : "Kyoo",
|
||||
slug: "kyoo",
|
||||
scheme: "kyoo",
|
||||
version: "1.0.0",
|
||||
orientation: "default",
|
||||
icon: "./assets/icon.png",
|
||||
userInterfaceStyle: "automatic",
|
||||
splash,
|
||||
assetBundlePatterns: ["**/*"],
|
||||
ios: {
|
||||
supportsTablet: true,
|
||||
},
|
||||
android: {
|
||||
package: IS_DEV ? "dev.zoriya.kyoo.dev" : "dev.zoriya.kyoo",
|
||||
adaptiveIcon: {
|
||||
foregroundImage: "./assets/icon.png",
|
||||
backgroundColor: "#eff1f5",
|
||||
},
|
||||
splash,
|
||||
},
|
||||
updates: {
|
||||
url: "https://u.expo.dev/55de6b52-c649-4a15-9a45-569ff5ed036c",
|
||||
fallbackToCacheTimeout: 0,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: "sdkVersion",
|
||||
},
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: "55de6b52-c649-4a15-9a45-569ff5ed036c",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
android: {
|
||||
usesCleartextTraffic: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"expo-localization",
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
enableNotificationControls: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Icon } from "@kyoo/primitives";
|
||||
import Browse from "@material-symbols/svg-400/rounded/browse-fill.svg";
|
||||
import Downloading from "@material-symbols/svg-400/rounded/downloading-fill.svg";
|
||||
import Home from "@material-symbols/svg-400/rounded/home-fill.svg";
|
||||
import { Tabs } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function TabsLayout() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
tabBarLabel: t("navbar.home"),
|
||||
tabBarIcon: ({ color, size }) => <Icon icon={Home} color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="browse"
|
||||
options={{
|
||||
tabBarLabel: t("navbar.browse"),
|
||||
tabBarIcon: ({ color, size }) => <Icon icon={Browse} color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="downloads"
|
||||
options={{
|
||||
tabBarLabel: t("navbar.download"),
|
||||
tabBarIcon: ({ color, size }) => <Icon icon={Downloading} color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BrowsePage } from "@kyoo/ui";
|
||||
|
||||
export default BrowsePage;
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ConnectionErrorContext, useAccount } from "@kyoo/models";
|
||||
import { NavbarRight, NavbarTitle } from "@kyoo/ui";
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
import { useContext } from "react";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useTheme } from "yoshiki/native";
|
||||
|
||||
export default function SignGuard() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
// TODO: support guest accounts on mobile too.
|
||||
const account = useAccount();
|
||||
const { error } = useContext(ConnectionErrorContext);
|
||||
const netInfo = useNetInfo();
|
||||
|
||||
if (error && netInfo.isConnected) return <Redirect href={"/connection-error"} />;
|
||||
if (!account) return <Redirect href="/server-url" />;
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
navigationBarColor: "transparent",
|
||||
// @ts-expect-error Not yet available. Waiting for expo-router update.
|
||||
navigationBarTranslucent: true,
|
||||
headerTitle: () => <NavbarTitle />,
|
||||
headerRight: () => <NavbarRight />,
|
||||
contentStyle: {
|
||||
backgroundColor: theme.background,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: theme.accent,
|
||||
},
|
||||
headerTintColor: theme.colors.white,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -63,59 +63,6 @@ import { withTranslations } from "../i18n";
|
||||
|
||||
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
|
||||
|
||||
const GlobalCssTheme = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
overflow: "hidden";
|
||||
background-color: ${theme.background};
|
||||
font-family: ${font.style.fontFamily};
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #999;
|
||||
border-radius: 90px;
|
||||
}
|
||||
*:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(134, 127, 127);
|
||||
}
|
||||
|
||||
#__next {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.infinite-scroll-component__outerdiv {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::cue {
|
||||
background-color: transparent;
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
}
|
||||
`}</style>
|
||||
<WebTooltip theme={theme} />
|
||||
<SkeletonCss />
|
||||
<TouchOnlyCss />
|
||||
<HiddenIfNoJs />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const YoshikiDebug = ({ children }: { children: JSX.Element }) => {
|
||||
if (typeof window === "undefined") return children;
|
||||
const registry = useStyleRegistry();
|
||||
|
@ -1,3 +1,6 @@
|
||||
{
|
||||
"extends": ["../biome.json"]
|
||||
"extends": "//",
|
||||
"files": {
|
||||
"includes": ["src/**"]
|
||||
}
|
||||
}
|
||||
|
2269
front/bun.lock
Normal file
2269
front/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 3.0.0",
|
||||
"version": ">= 15.0.10",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
@ -20,7 +20,8 @@
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"channel": "prod"
|
||||
"channel": "prod",
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
18
front/metro.config.js
Normal file
18
front/metro.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
|
||||
module.exports = (() => {
|
||||
const config = getDefaultConfig(__dirname);
|
||||
const { transformer, resolver } = config;
|
||||
|
||||
config.transformer = {
|
||||
...transformer,
|
||||
babelTransformerPath: require.resolve("react-native-svg-transformer/expo"),
|
||||
};
|
||||
config.resolver = {
|
||||
...resolver,
|
||||
assetExts: resolver.assetExts.filter((ext) => ext !== "svg"),
|
||||
sourceExts: [...resolver.sourceExts, "svg"],
|
||||
};
|
||||
|
||||
return config;
|
||||
})();
|
@ -1,24 +1,68 @@
|
||||
{
|
||||
"name": "kyoo",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "yarn workspaces foreach -pi run dev",
|
||||
"web": "yarn workspace web dev",
|
||||
"mobile": "yarn workspace mobile dev",
|
||||
"build:web": "yarn workspace web build",
|
||||
"build:mobile": "yarn workspace mobile build",
|
||||
"build:mobile:apk": "yarn workspace mobile build:apk",
|
||||
"build:mobile:dev": "yarn workspace mobile build:dev",
|
||||
"update": "yarn workspace mobile update",
|
||||
"dev": "expo start",
|
||||
"apk": "eas build --profile preview --platform android --non-interactive --json",
|
||||
"apk:dev": "eas build --profile development --platform android --non-interactive",
|
||||
"lint": "biome lint .",
|
||||
"lint:fix": "biome lint . --write",
|
||||
"format": "biome format .",
|
||||
"format:fix": "biome format . --write"
|
||||
},
|
||||
"workspaces": ["apps/*", "packages/*"],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"typescript": "5.5.4"
|
||||
"dependencies": {
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@legendapp/list": "^1.0.20",
|
||||
"@material-symbols/svg-400": "^0.31.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@tanstack/react-query": "^5.80.6",
|
||||
"expo": "~53.0.10",
|
||||
"expo-build-properties": "^0.14.6",
|
||||
"expo-image": "^2.3.0",
|
||||
"expo-linear-gradient": "^14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "^16.1.5",
|
||||
"expo-router": "~5.1.0",
|
||||
"expo-splash-screen": "^0.30.9",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-updates": "~0.28.14",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jotai": "^2.12.5",
|
||||
"react": "19.0.0",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-native": "0.79.3",
|
||||
"react-native-mmkv": "^3.2.0",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-video": "^6.15.0",
|
||||
"react-native-web": "^0.20.0",
|
||||
"react-tooltip": "^5.29.1",
|
||||
"sweetalert2": "^11.22.0",
|
||||
"yoshiki": "1.2.14",
|
||||
"zod": "^3.25.56"
|
||||
},
|
||||
"packageManager": "yarn@3.2.4"
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"@tanstack/react-query-devtools": "^5.80.6",
|
||||
"@types/react": "~19.0.10",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"expo-dev-client": "^5.2.0",
|
||||
"react-native-svg-transformer": "^1.5.1",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"expo": {
|
||||
"doctor": {
|
||||
"reactNativeDirectoryCheck": {
|
||||
"listUnknownPackages": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,108 +17,3 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import { MMKV } from "react-native-mmkv";
|
||||
import { type ZodTypeAny, z } from "zod";
|
||||
import { type Account, AccountP } from "./accounts";
|
||||
|
||||
export const storage = new MMKV();
|
||||
|
||||
const readAccounts = () => {
|
||||
const acc = storage.getString("accounts");
|
||||
if (!acc) return [];
|
||||
return z.array(AccountP).parse(JSON.parse(acc));
|
||||
};
|
||||
|
||||
const writeAccounts = (accounts: Account[]) => {
|
||||
storage.set("accounts", JSON.stringify(accounts));
|
||||
if (Platform.OS === "web") {
|
||||
const selected = accounts.find((x) => x.selected);
|
||||
if (!selected) return;
|
||||
setCookie("account", selected);
|
||||
// cookie used for images and videos since we can't add Authorization headers in img or video tags.
|
||||
setCookie("X-Bearer", selected?.token.access_token);
|
||||
}
|
||||
};
|
||||
|
||||
export const setCookie = (key: string, val?: unknown) => {
|
||||
let value = typeof val !== "string" ? JSON.stringify(val) : val;
|
||||
// Remove illegal values from json. There should not be one in the account anyways.
|
||||
value = value?.replaceAll(";", "");
|
||||
const d = new Date();
|
||||
// A year
|
||||
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||
const expires = value ? `expires=${d.toUTCString()}` : "expires=Thu, 01 Jan 1970 00:00:01 GMT";
|
||||
document.cookie = `${key}=${value};${expires};path=/;samesite=strict`;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const readCookie = <T extends ZodTypeAny>(
|
||||
cookies: string | undefined,
|
||||
key: string,
|
||||
parser?: T,
|
||||
) => {
|
||||
if (!cookies) return null;
|
||||
const name = `${key}=`;
|
||||
const decodedCookie = decodeURIComponent(cookies);
|
||||
const ca = decodedCookie.split(";");
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === " ") {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) === 0) {
|
||||
const str = c.substring(name.length, c.length);
|
||||
return parser ? parser.parse(JSON.parse(str)) : str;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getCurrentAccount = () => {
|
||||
const accounts = readAccounts();
|
||||
return accounts.find((x) => x.selected);
|
||||
};
|
||||
|
||||
export const addAccount = (account: Account) => {
|
||||
const accounts = readAccounts();
|
||||
|
||||
// Prevent the user from adding the same account twice.
|
||||
if (accounts.find((x) => x.id === account.id)) {
|
||||
updateAccount(account.id, account);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const acc of accounts) acc.selected = false;
|
||||
accounts.push(account);
|
||||
writeAccounts(accounts);
|
||||
};
|
||||
|
||||
export const removeAccounts = (filter: (acc: Account) => boolean) => {
|
||||
let accounts = readAccounts();
|
||||
accounts = accounts.filter((x) => !filter(x));
|
||||
if (!accounts.find((x) => x.selected) && accounts.length > 0) {
|
||||
accounts[0].selected = true;
|
||||
}
|
||||
writeAccounts(accounts);
|
||||
};
|
||||
|
||||
export const updateAccount = (id: string, account: Account) => {
|
||||
const accounts = readAccounts();
|
||||
const idx = accounts.findIndex((x) => x.id === id);
|
||||
if (idx === -1) return;
|
||||
|
||||
const selected = account.selected;
|
||||
if (selected) {
|
||||
for (const acc of accounts) acc.selected = false;
|
||||
// if account was already on the accounts list, we keep it selected.
|
||||
account.selected = selected;
|
||||
} else if (accounts[idx].selected) {
|
||||
// we just unselected the current account, focus another one.
|
||||
if (accounts.length > 0) accounts[0].selected = true;
|
||||
}
|
||||
|
||||
accounts[idx] = account;
|
||||
writeAccounts(accounts);
|
||||
};
|
||||
|
@ -38,27 +38,6 @@ import { useFetch } from "./query";
|
||||
import { ServerInfoP, type User, UserP } from "./resources";
|
||||
import { zdate } from "./utils";
|
||||
|
||||
export const TokenP = z.object({
|
||||
token_type: z.literal("Bearer"),
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string(),
|
||||
expire_in: z.string(),
|
||||
expire_at: zdate(),
|
||||
});
|
||||
export type Token = z.infer<typeof TokenP>;
|
||||
|
||||
export const AccountP = UserP.and(
|
||||
z.object({
|
||||
// set it optional for accounts logged in before the kind was present
|
||||
kind: z.literal("user").optional(),
|
||||
token: TokenP,
|
||||
apiUrl: z.string(),
|
||||
selected: z.boolean(),
|
||||
}),
|
||||
);
|
||||
export type Account = z.infer<typeof AccountP>;
|
||||
|
||||
const defaultApiUrl = Platform.OS === "web" ? "/api" : null;
|
||||
const currentApiUrl = atom<string | null>(defaultApiUrl);
|
||||
export const getCurrentApiUrl = () => {
|
||||
const store = getDefaultStore();
|
||||
@ -72,138 +51,6 @@ export const setSsrApiUrl = () => {
|
||||
store.set(currentApiUrl, process.env.KYOO_URL ?? "http://localhost:5000");
|
||||
};
|
||||
|
||||
const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]);
|
||||
export const ConnectionErrorContext = createContext<{
|
||||
error: KyooErrors | null;
|
||||
loading: boolean;
|
||||
retry?: () => void;
|
||||
setError: (error: KyooErrors) => void;
|
||||
}>({ error: null, loading: true, setError: () => {} });
|
||||
|
||||
export const AccountProvider = ({
|
||||
children,
|
||||
ssrAccount,
|
||||
ssrError,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
ssrAccount?: Account;
|
||||
ssrError?: KyooErrors;
|
||||
}) => {
|
||||
const setApiUrl = useSetAtom(currentApiUrl);
|
||||
if (Platform.OS === "web" && typeof window === "undefined") {
|
||||
const accs = ssrAccount
|
||||
? [{ ...ssrAccount, selected: true, select: () => {}, remove: () => {} }]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<AccountContext.Provider value={accs}>
|
||||
<ConnectionErrorContext.Provider
|
||||
value={{
|
||||
error: ssrError || null,
|
||||
loading: false,
|
||||
retry: () => {
|
||||
queryClient.resetQueries({ queryKey: ["auth", "me"] });
|
||||
},
|
||||
setError: () => {},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConnectionErrorContext.Provider>
|
||||
</AccountContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const initialSsrError = useRef(ssrError);
|
||||
|
||||
const [accStr] = useMMKVString("accounts");
|
||||
const acc = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null;
|
||||
const accounts = useMemo(
|
||||
() =>
|
||||
acc?.map((account) => ({
|
||||
...account,
|
||||
select: () => updateAccount(account.id, { ...account, selected: true }),
|
||||
remove: () => removeAccounts((x) => x.id === account.id),
|
||||
})) ?? [],
|
||||
[acc],
|
||||
);
|
||||
|
||||
// update user's data from kyoo un startup, it could have changed.
|
||||
const selected = useMemo(() => accounts.find((x) => x.selected), [accounts]);
|
||||
useEffect(() => {
|
||||
setApiUrl(selected?.apiUrl ?? defaultApiUrl);
|
||||
}, [selected, setApiUrl]);
|
||||
|
||||
const {
|
||||
isSuccess: userIsSuccess,
|
||||
isError: userIsError,
|
||||
isLoading: userIsLoading,
|
||||
isPlaceholderData: userIsPlaceholder,
|
||||
data: user,
|
||||
error: userError,
|
||||
} = useFetch({
|
||||
path: ["auth", "me"],
|
||||
parser: UserP,
|
||||
placeholderData: selected as User,
|
||||
enabled: !!selected,
|
||||
});
|
||||
// Use a ref here because we don't want the effect to trigger when the selected
|
||||
// value has changed, only when the fetch result changed
|
||||
// If we trigger the effect when the selected value change, we enter an infinite render loop
|
||||
const selectedRef = useRef(selected);
|
||||
selectedRef.current = selected;
|
||||
useEffect(() => {
|
||||
if (!selectedRef.current || !userIsSuccess || userIsPlaceholder) return;
|
||||
// The id is different when user is stale data, we need to wait for the use effect to invalidate the query.
|
||||
if (user.id !== selectedRef.current.id) return;
|
||||
const nUser = { ...selectedRef.current, ...user };
|
||||
updateAccount(nUser.id, nUser);
|
||||
}, [user, userIsSuccess, userIsPlaceholder]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const oldSelected = useRef<{ id: string; token: string } | null>(
|
||||
selected ? { id: selected.id, token: selected.token.access_token } : null,
|
||||
);
|
||||
|
||||
const [permissionError, setPermissionError] = useState<KyooErrors | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// if the user change account (or connect/disconnect), reset query cache.
|
||||
if (
|
||||
// biome-ignore lint/suspicious/noDoubleEquals: id can be an id, null or undefined
|
||||
selected?.id != oldSelected.current?.id ||
|
||||
(userIsError && selected?.token.access_token !== oldSelected.current?.token)
|
||||
) {
|
||||
initialSsrError.current = undefined;
|
||||
setPermissionError(null);
|
||||
queryClient.resetQueries();
|
||||
}
|
||||
oldSelected.current = selected ? { id: selected.id, token: selected.token.access_token } : null;
|
||||
|
||||
// update cookies for ssr (needs to contains token, theme, language...)
|
||||
if (Platform.OS === "web") {
|
||||
setCookie("account", selected);
|
||||
// cookie used for images and videos since we can't add Authorization headers in img or video tags.
|
||||
setCookie("X-Bearer", selected?.token.access_token);
|
||||
}
|
||||
}, [selected, queryClient, userIsError]);
|
||||
|
||||
return (
|
||||
<AccountContext.Provider value={accounts}>
|
||||
<ConnectionErrorContext.Provider
|
||||
value={{
|
||||
error: (selected ? (initialSsrError.current ?? userError) : null) ?? permissionError,
|
||||
loading: userIsLoading,
|
||||
retry: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||
},
|
||||
setError: setPermissionError,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConnectionErrorContext.Provider>
|
||||
</AccountContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAccount = () => {
|
||||
const acc = useContext(AccountContext);
|
||||
@ -227,3 +74,4 @@ export const useHasPermission = (perms?: string[]) => {
|
||||
if (!available) return false;
|
||||
return perms.every((perm) => available.includes(perm));
|
||||
};
|
||||
|
||||
|
@ -21,12 +21,6 @@
|
||||
export * from "./accounts";
|
||||
export { storage } from "./account-internal";
|
||||
export * from "./theme";
|
||||
export * from "./resources";
|
||||
export * from "./traits";
|
||||
export * from "./page";
|
||||
export * from "./kyoo-errors";
|
||||
export * from "./utils";
|
||||
export * from "./login";
|
||||
export * from "./issue";
|
||||
|
||||
export * from "./query";
|
||||
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The list of errors that where made in the request.
|
||||
*/
|
||||
export interface KyooErrors {
|
||||
/**
|
||||
* The list of errors that where made in the request.
|
||||
*
|
||||
* @example `["InvalidFilter: no field 'startYear' on a collection"]`
|
||||
*/
|
||||
errors: string[];
|
||||
|
||||
status?: number | "aborted" | "parse" | "json";
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* A page of resource that contains information about the pagination of resources.
|
||||
*/
|
||||
export interface Page<T> {
|
||||
/**
|
||||
* The link of the current page.
|
||||
*
|
||||
* @format uri
|
||||
*/
|
||||
this: string;
|
||||
|
||||
/**
|
||||
* The link of the first page.
|
||||
*
|
||||
* @format uri
|
||||
*/
|
||||
first: string;
|
||||
|
||||
/**
|
||||
* The link of the next page.
|
||||
*
|
||||
* @format uri
|
||||
*/
|
||||
next: string | null;
|
||||
|
||||
/**
|
||||
* The number of items in the current page.
|
||||
*/
|
||||
count: number;
|
||||
|
||||
/**
|
||||
* The list of items in the page.
|
||||
*/
|
||||
items: T[];
|
||||
}
|
||||
|
||||
export const Paged = <Item>(item: z.ZodType<Item>): z.ZodSchema<Page<Item>> =>
|
||||
z.object({
|
||||
this: z.string(),
|
||||
first: z.string(),
|
||||
next: z.string().nullable(),
|
||||
count: z.number(),
|
||||
items: z.array(item),
|
||||
});
|
@ -1,282 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
QueryClient,
|
||||
type QueryFunctionContext,
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import type { ComponentType, ReactElement } from "react";
|
||||
import type { z } from "zod";
|
||||
import { getCurrentApiUrl } from ".";
|
||||
import type { KyooErrors } from "./kyoo-errors";
|
||||
import { getToken, getTokenWJ } from "./login";
|
||||
import { type Page, Paged } from "./page";
|
||||
|
||||
export let lastUsedUrl: string = null!;
|
||||
|
||||
const cleanSlash = (str: string | null, keepFirst = false) => {
|
||||
if (!str) return null;
|
||||
if (keepFirst) return str.replace(/\/$/g, "");
|
||||
return str.replace(/^\/|\/$/g, "");
|
||||
};
|
||||
|
||||
export const queryFn = async <Parser extends z.ZodTypeAny>(
|
||||
context: {
|
||||
apiUrl?: string | null;
|
||||
authenticated?: boolean;
|
||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
} & (
|
||||
| QueryFunctionContext
|
||||
| ({
|
||||
path: (string | false | undefined | null)[];
|
||||
body?: object;
|
||||
formData?: FormData;
|
||||
plainText?: boolean;
|
||||
} & Partial<QueryFunctionContext>)
|
||||
),
|
||||
type?: Parser,
|
||||
iToken?: string | null,
|
||||
): Promise<z.infer<Parser>> => {
|
||||
const url = context.apiUrl && context.apiUrl.length > 0 ? context.apiUrl : getCurrentApiUrl();
|
||||
lastUsedUrl = url!;
|
||||
|
||||
const token = iToken === undefined && context.authenticated !== false ? await getToken() : iToken;
|
||||
const path = [cleanSlash(url, true)]
|
||||
.concat(
|
||||
"path" in context
|
||||
? (context.path as string[])
|
||||
: "pageParam" in context && context.pageParam
|
||||
? [cleanSlash(context.pageParam as string)]
|
||||
: (context.queryKey as string[]),
|
||||
)
|
||||
.filter((x) => x)
|
||||
.join("/")
|
||||
.replace("/?", "?");
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(path, {
|
||||
method: context.method,
|
||||
body:
|
||||
"body" in context && context.body
|
||||
? JSON.stringify(context.body)
|
||||
: "formData" in context && context.formData
|
||||
? context.formData
|
||||
: undefined,
|
||||
headers: {
|
||||
...(token ? { Authorization: token } : {}),
|
||||
...("body" in context ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
signal: context.signal,
|
||||
});
|
||||
} catch (e) {
|
||||
if (typeof e === "object" && e && "name" in e && e.name === "AbortError")
|
||||
throw { errors: ["Aborted"] } as KyooErrors;
|
||||
console.log("Fetch error", e, path);
|
||||
throw { errors: ["Could not reach Kyoo's server."], status: "aborted" } as KyooErrors;
|
||||
}
|
||||
if (resp.status === 404) {
|
||||
throw { errors: ["Resource not found."], status: 404 } as KyooErrors;
|
||||
}
|
||||
// If we got a forbidden, try to refresh the token
|
||||
// if we got a token as an argument, it either means we already retried or we go one provided that's fresh
|
||||
// so we can't retry either ways.
|
||||
if (resp.status === 403 && iToken === undefined && token) {
|
||||
const [newToken, _, error] = await getTokenWJ(undefined, true);
|
||||
if (newToken) return await queryFn(context, type, newToken);
|
||||
console.error("refresh error while retrying a forbidden", error);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const error = await resp.text();
|
||||
let data: Record<string, any>;
|
||||
try {
|
||||
data = JSON.parse(error);
|
||||
} catch (e) {
|
||||
data = { errors: [error] } as KyooErrors;
|
||||
}
|
||||
data.status = resp.status;
|
||||
console.trace(
|
||||
`Invalid response (${
|
||||
"method" in context && context.method ? context.method : "GET"
|
||||
} ${path}):`,
|
||||
data,
|
||||
resp.status,
|
||||
);
|
||||
throw data as KyooErrors;
|
||||
}
|
||||
|
||||
if (resp.status === 204) return null;
|
||||
|
||||
if ("plainText" in context && context.plainText) return (await resp.text()) as unknown;
|
||||
|
||||
let data: Record<string, any>;
|
||||
try {
|
||||
data = await resp.json();
|
||||
} catch (e) {
|
||||
console.error("Invalid json from kyoo", e);
|
||||
throw { errors: ["Invalid response from kyoo"], status: "json" };
|
||||
}
|
||||
if (!type) return data;
|
||||
const parsed = await type.safeParseAsync(data);
|
||||
if (!parsed.success) {
|
||||
console.log("Path: ", path, " Response: ", resp.status, " Parse error: ", parsed.error);
|
||||
throw {
|
||||
errors: [
|
||||
"Invalid response from kyoo. Possible version mismatch between the server and the application.",
|
||||
],
|
||||
status: "parse",
|
||||
} as KyooErrors;
|
||||
}
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
export type MutationParam = {
|
||||
params?: Record<string, number | string>;
|
||||
body?: object;
|
||||
path: string[];
|
||||
method: "POST" | "DELETE";
|
||||
};
|
||||
|
||||
export const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// 5min
|
||||
staleTime: 300_000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
mutationFn: (({ method, path, body, params }: MutationParam) => {
|
||||
return queryFn({
|
||||
method,
|
||||
path: toQueryKey({ path, params }),
|
||||
body,
|
||||
});
|
||||
}) as any,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type QueryIdentifier<T = unknown, Ret = T> = {
|
||||
parser: z.ZodType<T, z.ZodTypeDef, any>;
|
||||
path: (string | undefined)[];
|
||||
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
||||
infinite?: boolean | { value: true; map?: (x: any[]) => Ret[] };
|
||||
|
||||
placeholderData?: T | (() => T);
|
||||
enabled?: boolean;
|
||||
options?: Partial<Parameters<typeof queryFn>[0]>;
|
||||
};
|
||||
|
||||
export type QueryPage<Props = {}, Items = unknown> = ComponentType<
|
||||
Props & { randomItems: Items[] }
|
||||
> & {
|
||||
getFetchUrls?: (route: { [key: string]: string }, randomItems: Items[]) => QueryIdentifier<any>[];
|
||||
getLayout?:
|
||||
| QueryPage<{ page: ReactElement }>
|
||||
| { Layout: QueryPage<{ page: ReactElement }>; props: object };
|
||||
requiredPermissions?: string[];
|
||||
randomItems?: Items[];
|
||||
isPublic?: boolean;
|
||||
};
|
||||
|
||||
export const toQueryKey = (query: {
|
||||
path: (string | undefined)[];
|
||||
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
||||
options?: { apiUrl?: string | null };
|
||||
}) => {
|
||||
return [
|
||||
query.options?.apiUrl,
|
||||
...query.path,
|
||||
query.params
|
||||
? `?${Object.entries(query.params)
|
||||
.filter(([_, v]) => v !== undefined)
|
||||
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
||||
.join("&")}`
|
||||
: null,
|
||||
].filter((x) => x);
|
||||
};
|
||||
|
||||
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
return useQuery<Data, KyooErrors>({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) =>
|
||||
queryFn(
|
||||
{
|
||||
...ctx,
|
||||
queryKey: toQueryKey({ ...query, options: {} }),
|
||||
...query.options,
|
||||
},
|
||||
query.parser,
|
||||
),
|
||||
placeholderData: query.placeholderData as any,
|
||||
enabled: query.enabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
||||
const ret = useInfiniteQuery<Page<Data>, KyooErrors>({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) =>
|
||||
queryFn(
|
||||
{ ...ctx, queryKey: toQueryKey({ ...query, options: {} }), ...query.options },
|
||||
Paged(query.parser),
|
||||
),
|
||||
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
|
||||
initialPageParam: undefined,
|
||||
placeholderData: query.placeholderData as any,
|
||||
enabled: query.enabled,
|
||||
});
|
||||
const items = ret.data?.pages.flatMap((x) => x.items);
|
||||
return {
|
||||
...ret,
|
||||
items:
|
||||
items && typeof query.infinite === "object" && query.infinite.map
|
||||
? query.infinite.map(items)
|
||||
: (items as unknown as Ret[] | undefined),
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchQuery = async (queries: QueryIdentifier[], authToken?: string | null) => {
|
||||
// we can't put this check in a function because we want build time optimizations
|
||||
// see https://github.com/vercel/next.js/issues/5354 for details
|
||||
if (typeof window !== "undefined") return null;
|
||||
|
||||
const client = createQueryClient();
|
||||
await Promise.all(
|
||||
queries.map((query) => {
|
||||
if (query.infinite) {
|
||||
return client.prefetchInfiniteQuery({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(ctx, Paged(query.parser), authToken),
|
||||
initialPageParam: undefined,
|
||||
});
|
||||
}
|
||||
return client.prefetchQuery({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(ctx, query.parser, authToken),
|
||||
});
|
||||
}),
|
||||
);
|
||||
return client;
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { ImagesP, ResourceP } from "../traits";
|
||||
|
||||
export const CollectionP = ResourceP("collection")
|
||||
.merge(ImagesP)
|
||||
.extend({
|
||||
/**
|
||||
* The title of this collection.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The summary of this show.
|
||||
*/
|
||||
overview: z.string().nullable(),
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
href: `/collection/${x.slug}`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* A class representing collections of show or movies.
|
||||
*/
|
||||
export type Collection = z.infer<typeof CollectionP>;
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export enum Genre {
|
||||
Action = "Action",
|
||||
Adventure = "Adventure",
|
||||
Animation = "Animation",
|
||||
Comedy = "Comedy",
|
||||
Crime = "Crime",
|
||||
Documentary = "Documentary",
|
||||
Drama = "Drama",
|
||||
Family = "Family",
|
||||
Fantasy = "Fantasy",
|
||||
History = "History",
|
||||
Horror = "Horror",
|
||||
Music = "Music",
|
||||
Mystery = "Mystery",
|
||||
Romance = "Romance",
|
||||
ScienceFiction = "ScienceFiction",
|
||||
Thriller = "Thriller",
|
||||
War = "War",
|
||||
Western = "Western",
|
||||
Kids = "Kids",
|
||||
News = "News",
|
||||
Reality = "Reality",
|
||||
Soap = "Soap",
|
||||
Talk = "Talk",
|
||||
Politics = "Politics",
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./library-item";
|
||||
export * from "./news";
|
||||
export * from "./show";
|
||||
export * from "./movie";
|
||||
export * from "./collection";
|
||||
export * from "./genre";
|
||||
export * from "./person";
|
||||
export * from "./studio";
|
||||
export * from "./episode";
|
||||
export * from "./season";
|
||||
export * from "./watch-info";
|
||||
export * from "./watch-status";
|
||||
export * from "./watchlist";
|
||||
export * from "./user";
|
||||
export * from "./server-info";
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { CollectionP } from "./collection";
|
||||
import { MovieP } from "./movie";
|
||||
import { ShowP } from "./show";
|
||||
|
||||
export const LibraryItemP = z.union([
|
||||
/*
|
||||
* Either a Show
|
||||
*/
|
||||
ShowP,
|
||||
/*
|
||||
* Or a Movie
|
||||
*/
|
||||
MovieP,
|
||||
/*
|
||||
* Or a Collection
|
||||
*/
|
||||
CollectionP,
|
||||
]);
|
||||
|
||||
/**
|
||||
* An item that can be contained by a Library (so a Show, a Movie or a Collection).
|
||||
*/
|
||||
export type LibraryItem = z.infer<typeof LibraryItemP>;
|
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const MetadataP = z.preprocess(
|
||||
(x) =>
|
||||
typeof x === "object" && x ? Object.fromEntries(Object.entries(x).filter(([_, v]) => v)) : x,
|
||||
z.record(
|
||||
z.object({
|
||||
/*
|
||||
* The ID of the resource on the external provider.
|
||||
*/
|
||||
dataId: z.string(),
|
||||
|
||||
/*
|
||||
* The URL of the resource on the external provider.
|
||||
*/
|
||||
link: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export type Metadata = z.infer<typeof MetadataP>;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user