soline init

This commit is contained in:
2026-07-28 19:10:12 +05:30
commit 22c9e2d1c4
54 changed files with 5532 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
FROM node:25-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lock* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
elif [ -f bun.lock ]; then npm install -g bun && bun install --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
ARG VITE_PROXY_PREFIX
ARG VITE_PROXY_PREFIX_VAMANA
ARG VITE_PROXY_PREFIX_BALARAMA
# 2. Assign ARGs to ENV so Vite/Vinxi can see them
ENV VITE_PROXY_PREFIX=$VITE_PROXY_PREFIX
ENV VITE_PROXY_PREFIX_VAMANA=$VITE_PROXY_PREFIX_VAMANA
ENV VITE_PROXY_PREFIX_BALARAMA=$VITE_PROXY_PREFIX_BALARAMA
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f bun.lock ]; then npm run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
COPY --from=builder /app/public ./public
COPY --from=builder --chown=appuser:nodejs /app/.output ./.output
COPY --from=builder --chown=appuser:nodejs /app/.vinxi ./.vinxi
COPY --from=builder /app/package.json ./package.json
USER appuser
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", ".output/server/index.mjs"]
+32
View File
@@ -0,0 +1,32 @@
# SolidStart
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
## Creating a project
```bash
# create a new project in the current directory
npm init solid@latest
# create a new project in my-app
npm init solid@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
+37
View File
@@ -0,0 +1,37 @@
import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { type InlineConfig } from "vite";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = defineConfig({
middleware: "src/middleware/index.ts",
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
chunkSizeWarningLimit: 1000,
assetsInlineLimit: 1024 * 1024,
},
dev: {
chunkSizeWarningLimit: 1000,
},
} as InlineConfig,
});
app.addRouter({
name: "hono",
type: "http",
base: "/api/hono",
handler: "./src/hono.ts",
worker: true,
});
export default app;
+62
View File
@@ -0,0 +1,62 @@
import { em, entity, text, boolean } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { RuntimeBkndConfig } from "bknd/adapter";
const local = registerLocalMediaAdapter();
// let html = await readFile("./index.html", "utf-8");
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
export default {
connection: {
url: "file:data.db",
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
},
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
alg: "HS256",
secret: "e79f40806aee76b95f0e2e5789eea",
},
},
media: {
enabled: true,
adapter: local({
path: "./public/uploads",
}),
},
},
adminOptions: {
adminBasepath: "/admin",
assetsPath: "/admin/",
},
} satisfies RuntimeBkndConfig;
+2154
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
// https://zaidan.carere.dev/ui/button
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "kobalte",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {
"@zaidan": "https://zaidan.carere.dev/r/{style}/{name}.json"
}
}
+26
View File
@@ -0,0 +1,26 @@
services:
redis:
image: redis:alpine
container_name: redis
ports:
- 6379:6379
restart: always
postgres:
image: postgres:18-alpine
container_name: postgres
ports:
- 5432:5432
restart: always
app:
build:
context: .
args:
- VITE_PROXY_PREFIX=${VITE_PROXY_PREFIX}
- VITE_PROXY_PREFIX_VAMANA=${VITE_PROXY_PREFIX_VAMANA}
- VITE_PROXY_PREFIX_BALARAMA=${VITE_PROXY_PREFIX_BALARAMA}
env_file:
- .env
ports:
- 3000:3000
depends_on:
- redis
+57
View File
@@ -0,0 +1,57 @@
{
"name": "example-basic",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "vite start",
"preview": "vite preview",
"typegen": "bknd types --outfile bknd-types.d.ts",
"prebuild": "bknd copy-assets --out public/admin"
},
"dependencies": {
"@fontsource/barlow-condensed": "^5.2.8",
"@solidjs/h": "^2.0.0-experimental.16",
"@solidjs/html": "^2.0.0-experimental.16",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.4",
"@solidjs/start": "2.0.0-alpha.2",
"@solidjs/web": "^2.0.0-experimental.16",
"@solidjs/universal": "^2.0.0-experimental.16",
"@solidjs/vite-plugin-nitro-2": "^0.1.0",
"babel-preset-solid": "^2.0.0-experimental.16",
"vite-plugin-solid": "3.0.0-next.1",
"@trpc/client": "11.10.0",
"@trpc/server": "11.10.0",
"@types/node": "^25.3.5",
"artplayer": "^5.3.0",
"artplayer-plugin-chapter": "^1.0.1",
"bknd": "^0.20.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dexie": "^4.3.0",
"hls.js": "^1.6.15",
"hono": "^4.12.5",
"ioredis": "^5.10.0",
"lucide-react": "^0.564.0",
"lucide-solid": "^0.563.0",
"radix-ui": "^1.4.3",
"simple-icons": "^16.10.0",
"solid-js": "^1.9.5",
"solid-toast": "^0.5.0",
"tailwind-merge": "^3.5.0",
"vite": "^7.3.1",
"zod": "^4.3.6"
},
"engines": {
"node": ">=22"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"shadcn": "^3.8.5",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"vite-plugin-environment": "^1.1.3",
"vite-plugin-rewrite-all": "^1.0.2"
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+44
View File
@@ -0,0 +1,44 @@
{
"src/ui/components/code/CodeEditor.tsx": {
"file": "assets/CodeEditor-BSbZC6_t.js",
"name": "CodeEditor",
"src": "src/ui/components/code/CodeEditor.tsx",
"isDynamicEntry": true,
"imports": [
"src/ui/main.tsx"
]
},
"src/ui/components/form/json-schema/JsonSchemaForm.tsx": {
"file": "assets/JsonSchemaForm-CE_Yijem.js",
"name": "JsonSchemaForm",
"src": "src/ui/components/form/json-schema/JsonSchemaForm.tsx",
"isDynamicEntry": true,
"imports": [
"src/ui/main.tsx"
]
},
"src/ui/main.tsx": {
"file": "assets/main-CfjI0j-e.js",
"name": "main",
"src": "src/ui/main.tsx",
"isEntry": true,
"dynamicImports": [
"src/ui/components/form/json-schema/JsonSchemaForm.tsx",
"src/ui/components/code/CodeEditor.tsx",
"src/ui/modules/data/components/canvas/DataSchemaCanvas.tsx",
"src/ui/components/code/CodeEditor.tsx"
],
"css": [
"assets/main-OPOTedc8.css"
]
},
"src/ui/modules/data/components/canvas/DataSchemaCanvas.tsx": {
"file": "assets/DataSchemaCanvas-Bidf4HWW.js",
"name": "DataSchemaCanvas",
"src": "src/ui/modules/data/components/canvas/DataSchemaCanvas.tsx",
"isDynamicEntry": true,
"imports": [
"src/ui/main.tsx"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+158
View File
@@ -0,0 +1,158 @@
/* @import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); */
@import '@fontsource/barlow-condensed';
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@theme {
--font-barlow-condensed: "Barlow Condensed", sans-serif;
--text-xs: 0.750rem;
--text-sm: 1rem;
--text-base: 1.5rem;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-barlow-condensed);
--font-mono: var(--font-barlow-condensed);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
body {
font-family: 'Barlow Condensed', sans-serif;
font-weight: 400;
}
@layer base {
* {
@apply border-border outline-ring/50;
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground font-normal;
@apply bg-background text-foreground;
}
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+50
View File
@@ -0,0 +1,50 @@
import { MetaProvider, Title } from "@solidjs/meta";
import { A, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Loading } from "solid-js";
import "./app.css";
import Button from "./components/button";
export default function App() {
return (
<Router
root={(props) => (
<MetaProvider>
<Title>Home</Title>
<div class="max-w-7xl mx-auto min-h-full p-2 md:p-4">
<Navbar />
<div class="mt-16">
<Loading>{props.children}</Loading>
</div>
</div>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}
function Navbar() {
return (
<nav class="fixed top-4 left-0 w-full ">
<div class=" max-w-7xl mx-auto z-50 backdrop-blur-xs px-2 md:px-4">
<div class="bg-black/5 flex justify-between p-1 ">
<div>
<Button asLink href="/" size="lg">
Home
</Button>
<Button asLink href="/genre" size="lg">
Genre
</Button>
</div>
<div>
<Button asLink href="/history" size="lg">
History
</Button>
</div>
</div>
</div>
</nav>
);
}
+77
View File
@@ -0,0 +1,77 @@
// Refrences:
// https://zaidan.carere.dev/ui/button
// https://shadcn-solid.com/docs/installation/solid-start
// https://www.solid-ui.com/docs/installation/solid-start
// https://kobalte.dev/
// https://corvu.dev/
// https://zagjs.com/
import { JSX } from "solid-js";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
import { A } from "@solidjs/router";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "hover:bg-primary text-black hover:text-white hover:bg-primary/90 font-bold",
active: "bg-primary text-white hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type ButtonAsButton = JSX.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & {
asLink?: false;
class?: string;
};
type ButtonAsLink =JSX.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & {
asLink: true;
href: string;
};
export default function Button(props: ButtonAsButton | ButtonAsLink) {
const { children, asLink, ...rest } = props;
if (asLink) {
const { href, variant, size, class: className } = props;
return (
<A href={href} class={cn(buttonVariants({ variant, size, className }))}>
{children}
</A>
);
}
const { variant, size, class: className, ...buttonProps } = rest as ButtonAsButton;
return (
<button class={cn(buttonVariants({ variant, size, className }))} {...buttonProps}>
{children}
</button>
);
}
+48
View File
@@ -0,0 +1,48 @@
import { cn } from "~/lib/utils";
import { createSignal, JSX, onMount, Show } from "solid-js";
export default function AnimatedImage(
props: JSX.ImgHTMLAttributes<HTMLImageElement>,
) {
const [loaded, setLoaded] = createSignal(false);
const [error, setError] = createSignal(false);
let imgRef!: HTMLImageElement;
onMount(() => {
if (imgRef && imgRef.complete && imgRef.naturalWidth !== 0) {
setLoaded(true);
}
});
return (
<div
class="grid transition-[grid-template-rows] duration-700 ease-in-out"
style={{
"grid-template-rows": loaded() ? "1fr" : "0fr",
}}
>
<div class="overflow-hidden">
<Show
when={!error()}
fallback={
<div class="p-4 bg-gray-100 text-sm text-red-500">
Failed to load image
</div>
}
>
<img
ref={imgRef}
{...props}
onload={() => setLoaded(true)}
onerror={() => setError(true)}
class={cn(
"w-full h-auto transition-opacity duration-500",
loaded() ? "opacity-100" : "opacity-0",
props.class,
)}
/>
</Show>
</div>
</div>
);
}
+29
View File
@@ -0,0 +1,29 @@
export default function Toogle({
setChecked,
checked,
label,
}: {
label: string;
checked: () => boolean;
setChecked: (val: boolean) => boolean;
}) {
return (
<div class="inline-flex cursor-pointer items-center gap-2 justify-between">
<label class=" text-sm font-medium">{label}</label>
<input
type="checkbox"
checked={checked()}
onChange={() => {
setChecked(!checked());
}}
class="peer sr-only"
/>
<div
role="switch"
aria-checked={checked()}
class="peer-focus:ring-red-700 peer relative h-[25px] w-11 border-none bg-gray-200 outline-hidden duration-200 after:absolute after:start-[2px] after:top-[2.5px] after:h-5 after:w-5 after:bg-white after:transition-all after:content-[''] peer-checked:bg-red-600 peer-checked:after:translate-x-[95%] peer-focus:outline-hidden peer-checked:rtl:after:-translate-x-full
dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-red-800"
/>
</div>
);
}
+4
View File
@@ -0,0 +1,4 @@
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";
mount(() => <StartClient />, document.getElementById("app")!);
+30
View File
@@ -0,0 +1,30 @@
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body>
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
));
if (typeof process !== "undefined") {
process.on("unhandledRejection", (reason, promise) => {
console.error("Caught Unhandled Rejection:", reason);
});
process.on("uncaughtException", (err) => {
console.error("Caught Uncaught Exception:", err);
});
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="@solidjs/start/env" />
+67
View File
@@ -0,0 +1,67 @@
import { Hono } from "hono";
import { getWebRequest } from "@solidjs/start/http";
const app = new Hono().basePath("/api/hono/");
app.get("/", (c) => c.text("ok"));
app.get("/proxy/*", async (c) => {
const incomingUrl = c.req.path;
const parts = incomingUrl.split("/").slice(4);
const completeUrl = parts
.map((part) => (part === "https:" ? part + "//" : part + "/"))
.join("");
const sanitizedUrl = new URL(decodeURIComponent(completeUrl));
if (sanitizedUrl.host === "thunderstrike77.online") {
sanitizedUrl.host = "haildrop77.pro";
}
// const origin = c.req.header("Origin");
// const userAgent = c.req.header("User-Agent");
// const accept = c.req.header("Accept");
const res = await fetch(sanitizedUrl, {
method: "GET",
keepalive: true,
redirect: "follow",
headers: {
origin: "localhost",
accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng",
"user-agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
Referer: "https://megacloud.blog/",
},
});
if (!res.ok) {
return new Response(
JSON.stringify({ error: res.status, message: res.statusText }),
{
status: res.status,
headers: { "Content-Type": "application/json" },
},
);
}
if (!res.body) {
return new Response("No body in response", { status: 500 });
}
return new Response(res.body, {
status: res.status,
headers: {
"Content-Type":
res.headers.get("Content-Type") || "application/octet-stream",
"Cache-Control": "private, max-age=3600",
},
});
});
// export default eventHandler(async (event) => {
// const request = getWebRequest(event);
// return await app.fetch(request);
// });
+18
View File
@@ -0,0 +1,18 @@
import {
createTRPCProxyClient,
httpBatchLink,
loggerLink,
} from '@trpc/client';
import { AppRouter } from "~/trpc/api/root";
import { getBaseUrl } from '~/utils/url';
// create the client, export it
export const api = createTRPCProxyClient<AppRouter>({
links: [
// will print out helpful logs when using client
// loggerLink(),
// identifies what url will handle trpc requests
httpBatchLink({ url: `${getBaseUrl()}/api/trpc` })
],
});
+73
View File
@@ -0,0 +1,73 @@
import { Redis } from 'ioredis'
export const cache = new Redis(process.env.REDIS_URL!)
export async function cached<T>(
key: string,
fetcher: () => Promise<T>,
ttl = 3600 // 1 hour default
): Promise<T> {
try {
const cachedData = await cache.get(key)
if (cachedData) {
return JSON.parse(cachedData) as T
}
} catch (error) {
console.error(`Cache get error for key ${key}:`, error)
}
const data = await fetcher()
const isAnimeInfo = key.startsWith('anime:info:')
try {
await cache.set(key, JSON.stringify(data), 'EX', isAnimeInfo ? shouldCacheLonger(data as AniwatchInfo, key) ? 86400 : ttl : ttl)
} catch (error) {
console.error(`Cache set error for key ${key}:`, error)
}
return data
}
export const shouldCacheLonger = (src: AniwatchInfo, key: string) => {
const { moreInfo, info } = src.data.anime;
if (moreInfo.status === "Currently Airing") return false
const endedAt = moreInfo.aired.split(" to ")[1]; // ? if anime hasn't ended
if (!endedAt || endedAt === "?") {
return false;
}
const endedAtDate = new Date(endedAt);
// check if older than 6 months
const isOld = endedAtDate.getTime() < Date.now() - 6 * 30 * 24 * 60 * 60 * 1000;
const isVeryOld = endedAtDate.getTime() < Date.now() - 24 * 30 * 24 * 60 * 60 * 1000;
if (
isOld &&
moreInfo.status === "Finished Airing" &&
info.stats.episodes.sub === info.stats.episodes.dub
) {
console.log(`[CACHE] ${key} cached longer has dub, sub and finished airing`);
return true
};
if (
isOld &&
isVeryOld &&
moreInfo.status === "Finished Airing" &&
info.stats.episodes.dub === 0 &&
info.stats.episodes.sub
) {
console.log(`[CACHE] ${key} cached longer has sub and finished airing 2 years ago`);
return true
};
return false;
}
+52
View File
@@ -0,0 +1,52 @@
import Dexie, { type EntityTable } from "dexie";
interface SearchHistory {
id: number;
type?: string;
term: string;
}
export interface UserPreference {
id: number;
hideWatchedShows: boolean;
hideWatchedShowsInSearch: boolean;
disableFloatingNavbar: boolean;
centerContent: boolean;
lang: "en" | "jp" | "all";
}
export interface WatchedShows {
show: Anime;
id: string;
}
export interface WatchLater {
show: Anime;
id: string;
}
export interface WatchHistory {
show: Anime;
id: string;
time: number;
updatedAt: Date;
duration: number;
ep: string;
epNum: number | string;
lang: "en" | "jp";
}
export const indexDB = new Dexie("BunflixDB") as Dexie & {
searches: EntityTable<SearchHistory, "id">;
userPreferences: EntityTable<UserPreference, "id">;
watchLater: EntityTable<WatchLater, "id">;
watchedShows: EntityTable<WatchedShows, "id">;
watchHistory: EntityTable<WatchHistory, "id">;
};
indexDB.version(1).stores({
searches: "++id",
userPreferences: "id",
watchHistory: "++id",
watchedShows: "++id",
watchLater: "++id",
});
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+52
View File
@@ -0,0 +1,52 @@
import { createMiddleware } from "@solidjs/start/middleware";
import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter";
import config from "../../bknd.config";
async function getApp<Env = NodeJS.ProcessEnv>(
config: RuntimeBkndConfig<Env>,
args: Env = process.env as Env,
) {
return await createRuntimeApp(config, args);
}
function serve(config: RuntimeBkndConfig) {
return async (request: Request) => {
const app = await getApp(config, process.env);
return app.fetch(request);
};
}
const handler = serve(config);
export default createMiddleware({
onRequest: async (event) => {
const url = new URL(event.request.url);
const isMethodWithBody = ["POST", "PUT", "PATCH", "DELETE"].includes(
event.request.method!,
);
const adminBasepath = config.adminOptions.adminBasepath;
if (
url.pathname.startsWith("/api") ||
url.pathname.startsWith(adminBasepath)
) {
if (url.pathname === adminBasepath + "/") {
url.pathname = url.pathname.replace(adminBasepath + "/", adminBasepath);
} else {
url.pathname = url.pathname.replaceAll("//", "/");
}
const modifiedRequest = new Request(url.toString(), {
method: event.request.method,
headers: event.request.headers as HeadersInit,
// @ts-expect-error - 'duplex' is required for streaming bodies in Node.js
duplex: isMethodWithBody ? "half" : undefined,
body: isMethodWithBody ? event.request.body : undefined,
});
const res = await handler(modifiedRequest);
if (res && res.status !== 404) {
return res;
}
}
},
});
+11
View File
@@ -0,0 +1,11 @@
import { JSX } from "solid-js";
import { Toaster } from "solid-toast";
export default function Layout({ children }: { children: JSX.Element }) {
return (
<div>
{children}
<Toaster />
</div>
);
}
+28
View File
@@ -0,0 +1,28 @@
import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
import Button from "~/components/button";
export default function NotFound() {
return (
<main>
<Title>Not Found</Title>
<HttpStatusCode code={404} />
<main class="grid place-items-center px-6 py-24 sm:py-32 lg:px-8 min-h-[80vh]">
<div class="text-center">
<p class="text-base font-semibold text-red-600">404</p>
<h1 class="mt-4 text-5xl font-semibold tracking-tight text-balance sm:text-7xl">
Page not found
</h1>
<p class="mt-6 text-lg font-medium text-pretty opacity-60 sm:text-xl/8">
Sorry, we couldn&apos;t find the page you&apos;re looking for.
</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<Button asLink variant="outline" href="/" class="">
Go Home
</Button>
</div>
</div>
</main>
</main>
);
}
View File
+26
View File
@@ -0,0 +1,26 @@
import Button from "~/components/button";
const GENRES = [
'Action', 'Adventure', 'Cars',
'Comedy', 'Dementia', 'Demons',
'Drama', 'Ecchi', 'Fantasy',
'Game', 'Harem', 'Historical',
'Horror', 'Isekai', 'Josei',
'Kids', 'Magic', 'Martial Arts',
'Mecha', 'Military', 'Music',
'Mystery', 'Parody', 'Police',
'Psychological', 'Romance', 'Samurai',
'School', 'Sci-Fi', 'Seinen',
'Shoujo', 'Shoujo Ai', 'Shounen',
'Shounen Ai', 'Slice of Life', 'Space',
'Sports', 'Super Power', 'Supernatural',
'Thriller', 'Vampire'
] as const;
export default function Genre(){
return(
<div class="my-4">
{GENRES.map((genre)=><Button asLink href={`/genre/${genre}`}>{genre}</Button>)}
</div>
)
}
+146
View File
@@ -0,0 +1,146 @@
import { Title } from "@solidjs/meta";
import { api } from "~/lib/api";
import Button from "~/components/button";
import {
For,
Loading,
} from "@solidjs/web";
import BookmarkIcon from "lucide-solid/icons/bookmark";
import Genre from "./genre";
import Image from "~/components/image";
import toast from "solid-toast";
import { cn } from "~/lib/utils";
import { createMemo, Errored } from "solid-js";
export default function Home() {
const anime = createMemo(async () => await api.anime.home.query());
return (
<main>
<Title>Home</Title>
<div>
<Errored
fallback={(error, reset) => (
<div>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
>
<Loading fallback="loading...">
<h1 class="font-bold text-2xl my-8 pl-4">Spotlight Animes</h1>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
<For each={anime()?.data.spotlightAnimes}>
{(show) => (
<ShowCard {...show()} href={show().id} fetchpriority="high" />
)}
</For>
</div>
<h1 class="font-bold text-2xl my-8 pl-4">Latest Episode Animes</h1>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
<For
each={anime()?.data.latestEpisodeAnimes.sort(
(a, b) => a.name.length - b.name.length,
)}
>
{(show) => (
<ShowCard
{...show()}
href={show().id}
class="[&_h1]:text-sm [&_img]:aspect-9/16"
/>
)}
</For>
</div>
<h1 class="font-bold text-2xl my-8 pl-4">Upcoming Animes</h1>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
<For
each={anime()?.data.topUpcomingAnimes.sort(
(a, b) => a.name.length - b.name.length,
)}
>
{(show) => (
<ShowCard
{...show()}
href={show().id}
class="[&_h1]:text-sm [&_img]:aspect-9/16"
/>
)}
</For>
</div>
<Genre />
</Loading>
</Errored>
</div>
</main>
);
}
function ShowCard(props: {
name: string;
description?: string;
poster: string;
href: string;
id: string;
class?: string;
fetchpriority?: "high" | "low" | "auto" | undefined;
episodes: {
sub: number;
dub: number;
};
}) {
const { episodes, href, name, poster, description, fetchpriority, id } = props;
const hasEpisodes = () => episodes.dub !== null || episodes.sub !== null;
return (
<div
class={cn(
"border-2 border-transparent hover:border-dashed hover:border-current p-2 flex flex-col justify-between",
"[&_h1]:text-2xl [&_h1]:font-bold [&_h1]:mt-4",
props.class,
)}
>
<div>
<Image
src={poster}
alt={name}
class="w-full transition-all object-cover"
fetchpriority={fetchpriority}
/>
<h1>{name}</h1>
{description && (
<h4 class={"opacity-40 text-sm"}>
{description.length > 140
? `${description.slice(0, 140)}...`
: description}
</h4>
)}
</div>
<div
class={cn(
"mt-4 flex justify-between items-center",
!hasEpisodes() && "w-full",
)}
>
{hasEpisodes() && (
<Button asLink variant="active" href={`/watch/${href}`}>
Play
</Button>
)}
<Button
variant="outline"
class={cn(!hasEpisodes() && "w-full")}
onClick={async () => {
toast.success("Saved to My List");
}}
>
<BookmarkIcon /> Save
</Button>
</div>
</div>
);
}
+508
View File
@@ -0,0 +1,508 @@
import {
AccessorWithLatest,
createAsync,
CustomResponse,
redirect,
revalidate,
RouteDefinition,
RouteSectionProps,
useNavigate,
useParams,
useSearchParams,
} from "@solidjs/router";
import {
createEffect,
onCleanup,
For,
Loading,
Errored
} from "solid-js";
import { api } from "~/lib/api";
import { getLinkParams } from "~/utils/url";
import { query } from "@solidjs/router";
import { withBackoff } from "~/utils/backoff";
import Artplayer from "artplayer";
import Hls from "hls.js/dist/hls.light.js";
import type { HlsConfig } from "hls.js";
import Button from "~/components/button";
interface WatchArgs {
t?: number;
id: string;
ep: number;
num: number;
lang?: "en" | "jp";
}
interface WatchReturn {
show: AniwatchInfo;
episode: AniwatchEpisodeData;
nextEpTiming: NextEpisodeTiming;
id: string;
epNum: number;
ep: number;
sources: AniwatchEpisodeSrc;
}
async function watchPageData({
lang,
num,
ep,
id,
}: WatchArgs): Promise<WatchReturn | CustomResponse<never>> {
const episode = await api.anime.episode_info.query(id);
if (episode.data.episodes.length === 0 || episode.data.totalEpisodes === 0) {
return redirect("/error/No%20Dub%20Available");
}
const epNum = num - 1;
const epId =
episode.data?.episodes[epNum] ||
episode.data?.episodes[epNum - 1] ||
episode.data?.episodes[0];
const {
params: { id: episodeId },
search,
} = getLinkParams(epId.episodeId);
if (!ep) {
return redirect(`/watch/${episodeId}${search.toString({ num })}`);
}
const server = await api.anime.episode_server.query({
id,
ep,
});
if (server.data?.dub?.length === 0 && lang === "en") {
return redirect("/error/No%20Dub%20Available");
}
const serverName =
lang === "en"
? server.data?.dub[0].serverName
: !server.data?.sub[0]
? server.data?.raw[0].serverName
: server.data?.sub[0].serverName;
const sources = await api.anime.episode_src.query({
id,
ep,
server: serverName,
category: lang === "en" ? "dub" : !server.data?.sub[0] ? "raw" : "sub",
});
if (sources.status !== 200 || sources.data.sources.length === 0) {
return redirect(`/error/No%20Source%20Available`);
}
if ((sources.status !== 200 || !sources.data.sources) && lang === "en") {
return redirect("/error/No%20Dub%20Available");
}
if (sources.data.sources.length === 0 && lang === "jp") {
return redirect("/error/No%20Source%20Available");
}
const show = (await api.anime.anime_info.query(id)) as AniwatchInfo;
const nextEpTiming = await api.anime.next_episode_time.query(id);
return { episode, nextEpTiming, id, epNum, ep, sources, show };
}
const getWatchData = query(async (args: WatchArgs) => {
const res = await withBackoff(async () => await watchPageData(args));
if (res.success) {
return res.data;
} else throw res.error;
}, "watchPage");
export const route = {
async preload({ params, location }) {
const { id } = params as { id: string };
const searchParams = new URLSearchParams(location.search);
const ep = Number(searchParams.get("ep"));
const lang = searchParams.get("lang") as WatchArgs["lang"];
const t = Number(searchParams.get("t") ?? 0);
const num = Number(searchParams.get("num") ?? 0);
void watchPageData({
ep,
id,
num,
lang,
t,
});
},
} satisfies RouteDefinition;
export default function Watch({}: RouteSectionProps) {
const params = useParams();
const [searchParams] = useSearchParams();
const anime = createAsync(
async () =>
await getWatchData({
id: params.id ?? "",
ep: Number(searchParams.ep),
num: Number(searchParams.num ?? 0),
t: Number(searchParams.t ?? 0),
lang: searchParams.lang,
} as WatchArgs),
);
return (
<div>
<Errored
fallback={(error, reset) => (
<div>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
>
<Loading fallback={<p>Loading video...</p>}>
<Player anime={anime} />
<Episodes anime={anime} />
</Loading>
</Errored>
</div>
);
}
export function Player({
anime,
}: {
anime: AccessorWithLatest<WatchReturn | undefined>;
}) {
let containerRef!: HTMLDivElement;
let videoRef!: HTMLVideoElement;
let artRef!: Artplayer;
let hls!: Hls;
class loader extends Hls.DefaultConfig.loader {
constructor(config: HlsConfig) {
super(config);
const baseLoad = this.load.bind(this);
this.load = (context, config, callbacks) => {
const proxiedContext = {
...context,
url: getProxiedUrl(context.url),
};
baseLoad(proxiedContext, config, callbacks);
};
function getProxiedUrl(originalUrl: string) {
const proxies = [
import.meta.env.VITE_PROXY_PREFIX,
import.meta.env.VITE_PROXY_PREFIX_VAMANA,
import.meta.env.VITE_PROXY_PREFIX_BALARAMA,
];
const prefix: string[] = proxies
.filter((x) => x && x.trim() !== "")
.filter((x) => x !== undefined);
const chance = Math.floor(Math.random() * prefix.length);
const pick = prefix[chance];
const cleanUrl = originalUrl.replaceAll("//", "/");
let hasProxyPrefix = false;
prefix.forEach((t) => {
if (cleanUrl.startsWith(t)) {
hasProxyPrefix = true;
}
});
return hasProxyPrefix ? originalUrl : `${pick}${cleanUrl}`;
}
}
}
const hlsConfig: Partial<HlsConfig> = {
fragLoadingMaxRetry: 200,
fragLoadingRetryDelay: 500,
fragLoadingTimeOut: 30_000,
fragLoadingMaxRetryTimeout: 1000,
maxBufferLength: 60 * 30,
maxMaxBufferLength: 60 * 30,
maxBufferHole: 0.5,
enableSoftwareAES: true,
autoStartLoad: true,
progressive: true,
loader: loader,
};
createEffect(() => {
const nextEpUrl = () =>
anime()?.episode.data?.episodes[(anime()?.epNum ?? 0) + 1]?.episodeId;
const prevEpUrl = () =>
anime()?.episode.data?.episodes[(anime()?.epNum ?? 0) - 1]?.episodeId;
// if (artRef) {
// artRef.destroy();
// }
if (hls) {
console.log("updating");
hls.loadSource(anime()?.sources.data.sources[0].url ?? "");
// hls.destroy();
return;
}
console.log("entering");
const sources = anime()?.sources.data;
if (!sources) {
revalidate(getWatchData.key);
return;
}
const voiceTracks = sources?.tracks.filter(
(item) => item.lang !== "thumbnails",
);
const src = sources.sources[0].url;
artRef = new Artplayer({
url: src,
container: containerRef,
setting: true,
fullscreen: true,
fullscreenWeb: true,
playbackRate: true,
autoPlayback: true,
screenshot: true,
gesture: true,
backdrop: true,
hotkey: true,
volume: 0.5,
muted: false,
autoplay: true,
moreVideoAttr: {
crossOrigin: "anonymous",
},
plugins: [
// artplayerPluginChapter({
// chapters: [
// {
// title: "Opening Song",
// start: intro.start,
// end: intro.end,
// },
// {
// title: "Ending Song",
// start: outro.start,
// end: outro.end,
// },
// ],
// }),
// artplayerPluginHlsControl({
// quality: {
// control: true,
// setting: true,
// getName: (level: { height: string }) => level.height,
// title: "Quality",
// auto: "Auto",
// },
// audio: {
// control: true,
// setting: true,
// getName: (track: { name: string }) => track.name,
// title: "Audio",
// auto: "Auto",
// },
// }),
],
settings: [
{
width: 200,
html: "Subtitle",
tooltip: "Subtitle",
selector: [
{
html: "Display",
tooltip: "Show",
switch: true,
onSwitch: (item) => {
item.tooltip = item.switch ? "Hide" : "Show";
if (artRef) {
artRef.subtitle.show = !item.switch;
}
return !item.switch;
},
},
...(voiceTracks ?? []).map((sub) => {
return {
html: sub.lang,
url: sub.url,
default: sub.lang === "English",
};
}),
],
onSelect: (item) => {
if (!artRef) return;
artRef.subtitle.switch(item.url, {
name: item.html,
});
return item.html;
},
},
],
controls: [
{
name: "prev",
html: "<svg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 22 22' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='lucide lucide-skip-back-icon lucide-skip-back'><path d='M17.971 4.285A2 2 0 0 1 21 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z'/><path d='M3 20V4'/></svg>",
tooltip: "Previous",
position: "left",
disable: !prevEpUrl(),
click: () => {
if (!artRef) return;
if (!prevEpUrl()) return;
artRef.notice.show = "Playing previous episode";
// playPrevious();
},
},
{
name: "next",
html: "<svg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 22 22' fill='none' stroke=Color' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='lucide lucide-skip-forward-icon lucide-skip-forward'><path d='M21 4v16'/><path d='M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z'/></svg>",
tooltip: "Next",
position: "right",
disable: !nextEpUrl(),
click: () => {
if (!artRef) return;
if (!nextEpUrl()) return;
artRef.notice.show = "Playing next episode";
// playNext();
},
},
{
name: "speed",
position: "right",
html: "Speed",
selector: [
{
html: "1x",
},
{
html: "2x",
},
],
onSelect: (item) => {
if (!artRef) return;
const speed = Number((item.html as string).charAt(0));
artRef.storage.set("speed", speed);
// setPlayerOpts({ ...playerOpts, speed });
artRef.video.playbackRate = speed;
artRef.notice.show = `Speed set to ${speed}x`;
return item.html;
},
},
],
subtitle: {
url:
(voiceTracks ?? []).filter((sub) => sub.lang === "English")[0]?.url ??
"",
escape: true,
name: "English",
onVttLoad: (vtt) => {
return vtt.replace(/<[^>]+>/g, "");
},
type: "vtt",
encoding: "utf-8",
style: {
fontWeight: "600",
fontSize: "28px",
},
},
customType: {
m3u8: function playM3u8(video, url, art) {
if (Hls.isSupported()) {
if (!hls) {
hls = new Hls(hlsConfig);
}
hls?.loadSource(url); // this load for the initial video
hls?.attachMedia(video);
(art as any).hls = hls;
// if (time) {
// videoTime = time;
// }
const speed = Number(art.storage.get("speed")) ?? 1;
if (speed) {
video.playbackRate = speed;
}
videoRef = video;
art.on("destroy", () => hls?.destroy());
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
} else {
art.notice.show = "Unsupported playback format: m3u8";
}
},
},
});
},(pop)=>{
});
// onCleanup(() => {
// console.log("leaving");
// if (artRef) {
// artRef.destroy(false);
// }
// if (hls) {
// hls?.destroy();
// }
// });
return (
<div
ref={containerRef}
tabindex={1}
class="mt-4 w-full h-[300px] sm:h-[350px] md:h-[450px] lg:h-[550px] xl:h-[600px] focus:outline-0 focus-within:outline-0"
/>
);
}
export function Episodes({
anime,
}: {
anime: AccessorWithLatest<WatchReturn | undefined>;
}) {
const [searchParams] = useSearchParams();
// const naviagte = useNavigate();
return (
<div class="grid grid-cols-2 mt-4">
<For each={anime()?.episode.data.episodes}>
{(episode, i) => (
<Button
variant="outline"
asLink
href={
`/watch/${episode().episodeId}&num=${episode().number}&lang=${searchParams.lang ?? "jp"}`
}
onClick={(e) => {
// e.preventDefault();
// naviagte(
// `/watch/${episode.episodeId}&num=${episode.number}&lang=${searchParams.lang ?? "jp"}`,
// );
revalidate(getWatchData.key);
}}
>
{i() + 1}. {episode().title}
</Button>
)}
</For>
</div>
);
}
export function AnimeInfo({
anime,
}: {
anime: AccessorWithLatest<WatchReturn | undefined>;
}){
return(
<div>
</div>
)
}
+70
View File
@@ -0,0 +1,70 @@
import type { APIEvent } from "@solidjs/start/server";
export async function GET(req: APIEvent) {
const reqUrl = req.params.all
// const reqUrl: string[] = req.params.all.split("/").slice(3); // removes http : //
const completeUrl = reqUrl
.split("/")
.map((part) => (part === "https:" ? part + "//" : part + "/"))
.join("");
const sanitizedUrl = new URL(decodeURIComponent(completeUrl));
if(sanitizedUrl.host === "thunderstrike77.online"){
sanitizedUrl.host = "haildrop77.pro"
}
const res = await fetch((sanitizedUrl), {
cache: "no-store",
priority: "high",
redirect: "follow",
keepalive: true,
headers: {
...req.request.headers,
Referer: "https://megacloud.club/",
},
});
if (!res.ok) {
return Response.json({
error: res.status,
message: res.statusText,
});
}
const reader: ReadableStreamDefaultReader | null =
res.body?.getReader() || null;
function iteratorToStream(
iterator: ReadableStreamDefaultReader
): ReadableStream {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.read();
if (done) {
controller.close();
} else if (value) {
controller.enqueue(value);
}
},
});
}
if (!reader) {
return new Response("No body in response", { status: 500 });
}
const stream = iteratorToStream(reader);
return new Response(stream, {
status: 200,
headers: {
"Content-Type":
res.headers.get("Content-Type") || "application/octet-stream",
"Cache-Control": "private, max-age=3600",
},
});
}
+4
View File
@@ -0,0 +1,4 @@
export async function GET (){
return Response.json({oallk:true})
}
+4
View File
@@ -0,0 +1,4 @@
export async function GET (){
return Response.json({rest:true})
}
+4
View File
@@ -0,0 +1,4 @@
export async function GET (){
return Response.json({hello:true})
}
+4
View File
@@ -0,0 +1,4 @@
export async function GET (){
return Response.json({ok:true})
}
+27
View File
@@ -0,0 +1,27 @@
import type { APIEvent } from "@solidjs/start/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "~/trpc/api/root";
const handler = (event: APIEvent) =>
// adapts tRPC to fetch API style requests
fetchRequestHandler({
// the endpoint handling the requests
endpoint: "/api/trpc",
// the request object
req: event.request,
// the router for handling the requests
router: appRouter,
// any arbitrary data that should be available to all actions
createContext: () => event,
onError(opts) {
const { error, type, path, input, ctx, req } = opts;
console.error('Error:', error);
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
}
},
});
export const GET = handler;
export const POST = handler;
+9
View File
@@ -0,0 +1,9 @@
import { anime, tmdb } from "./routers/anime";
import { createTRPCRouter } from "./utils";
export const appRouter = createTRPCRouter({
anime,
tmdb
});
export type AppRouter = typeof appRouter;
+302
View File
@@ -0,0 +1,302 @@
import { TRPCError, type TRPCRouterRecord } from "@trpc/server";
import { z } from "zod";
import { cached } from "~/lib/cache";
import { publicProcedure } from "../utils";
const baseUrl = "https://api.themoviedb.org/3";
const key = process.env.TMDB_KEY;
const anime_url = process.env.ANIWATCH_API;
export const tmdb = {
home: publicProcedure.query(async () => {
return cached("tmdb:home", async () => {
const response = await fetch(`${baseUrl}/movie/popular?api_key=${key}`);
return (await response.json()) as TMDBMovie;
});
}),
popularMovies: publicProcedure.query(async () => {
return cached("tmdb:popular", async () => {
const response = await fetch(`${baseUrl}/movie/popular?api_key=${key}`);
return (await response.json()) as TMDBMovie;
});
}),
trendingMovies: publicProcedure.query(async () => {
return cached("tmdb:trending:week", async () => {
const response = await fetch(
`${baseUrl}/trending/movie/week?api_key=${key}&region=IN&with_original_language=hi`,
);
return (await response.json()) as TMDBMovie;
});
}),
upcomingMovies: publicProcedure.query(async () => {
return cached("tmdb:upcoming", async () => {
const response = await fetch(`${baseUrl}/movie/upcoming?api_key=${key}`);
return (await response.json()) as TMDBMovie;
});
}),
topRatedMovies: publicProcedure.query(async () => {
return cached("tmdb:top-rated", async () => {
const response = await fetch(`${baseUrl}/movie/top_rated?api_key=${key}`);
return (await response.json()) as TMDBMovie;
});
}),
nowPlayingMovies: publicProcedure.query(async () => {
return cached("tmdb:now-playing", async () => {
const response = await fetch(`${baseUrl}/movie/now_playing?api_key=${key}`);
return (await response.json()) as TMDBMovie;
});
}),
topRatedTvShows: publicProcedure.query(async () => {
return cached("tmdb:tv:top-rated", async () => {
const response = await fetch(
`${baseUrl}/tv/top_rated?api_key=${key}&region=US`,
);
return (await response.json()) as TMDBMovie;
});
}),
search: publicProcedure
.input(
z.object({
query: z.string(),
page: z.number().optional().default(1),
}),
)
.query(async ({ input }) => {
return cached(`tmdb:search:${input.query}:${input.page}`, async () => {
const response = await fetch(
`${baseUrl}/search/multi?api_key=${key}&query=${input.query}`,
);
return (await response.json()) as TMDBMultiSearch;
});
}),
info: publicProcedure
.input(
z.object({
type: z.enum(["movie", "tv"]),
id: z.number(),
}),
)
.query(async ({ input }) => {
return cached(`tmdb:info:${input.type}:${input.id}`, async () => {
const response = await fetch(
`${baseUrl}/${input.type}/${input.id}?api_key=${key}`,
);
return (await response.json()) as MovieResults;
});
}),
season_info: publicProcedure
.input(
z.object({
id: z.number(),
season: z.number(),
}),
)
.query(async ({ input }) => {
return cached(`tmdb:season:${input.id}:${input.season}`, async () => {
const response = await fetch(
`${baseUrl}/tv/${input.id}/season/${input.season}?api_key=${key}`,
);
return (await response.json()) as TMDBEpisodesInfo;
});
}),
} satisfies TRPCRouterRecord;
export const anime = {
home: publicProcedure.query(async () => {
return cached("anime:home", async () => {
const response = await fetch(
`${anime_url}/api/v2/hianime/home`);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch anime home",
})
}
const data = (await response.json()) as AniwatchHome;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch anime home",
})
}
return data;
},);
}),
anime_info: publicProcedure
.input(z.string())
.query(async ({ input }) => {
return cached(`anime:info:${input}`, async () => {
const response = await fetch(
`${anime_url}/api/v2/hianime/anime/${input}`,
);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch anime info",
})
}
const data = await response.json() as AniwatchInfo;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch anime info",
})
}
return data;
});
}),
category: publicProcedure
.input(z.object({
type: z.string(),
pageToFetch: z.number(),
}))
.query(async ({ input }) => {
return cached(`anime:category:${input.type}:${input.pageToFetch}`, async () => {
const response = await fetch(
`${anime_url}/api/v2/hianime/category/${input.type}?page=${input.pageToFetch}`,
);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch anime category",
});
}
const data = (await response.json()) as AniwatchSearch;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch anime category",
});
}
return data;
});
}),
episode_info: publicProcedure
.input(z.string())
.query(async ({ input }) => {
return cached(`anime:episode-info:${input}`, async () => {
const response = await fetch(
`${anime_url}/api/v2/hianime/anime/${input}/episodes`,
{ cache: "no-store" }
);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch episode info",
});
}
const data = await response.json() as AniwatchEpisodeData;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch episode info",
});
}
return data;
});
}),
episode_server: publicProcedure
.input(z.object({
id: z.string(),
ep: z.number(),
}))
.query(async ({ input }) => {
return cached(`anime:episode-server:${input.id}:${input.ep}`, async () => {
const response = await fetch(
`${anime_url}/api/v2/hianime/episode/servers?animeEpisodeId=${input.id}?ep=${input.ep}`,
{ cache: "no-store" }
);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch episode server info",
});
}
const data = await response.json() as AniwatchServer;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch episode server info",
});
}
return data;
});
}),
next_episode_time: publicProcedure
.input(z.string())
.query(async ({ input }) => {
return cached(`anime:next-ep:${input}`, async () => {
const response = await fetch(`${anime_url}/api/v2/hianime/anime/${input}/next-episode-schedule`);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch next episode time",
});
}
const data = await response.json() as NextEpisodeTiming;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch next episode time",
});
}
return data;
});
}),
episode_src: publicProcedure
.input(z.object({
id: z.string(),
ep: z.number(),
server: z.string(),
category: z.string(),
}))
.query(async ({ input }) => {
return cached(`anime:episode-src:${input.id}:${input.ep}:${input.server}:${input.category}`, async () => {
const response = await fetch(
`${anime_url
}/api/v2/hianime/episode/sources?animeEpisodeId=${input.id}?ep=${input.ep}&server=${input.server ? input.server : "vidstreaming"
}&category=${input.category}`,
{ cache: "no-cache" }
);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch episode source",
});
}
const data = await response.json() as AniwatchEpisodeSrc;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch episode source",
});
}
return data;
});
}),
search: publicProcedure
.input(z.object({
query: z.string(),
page: z.number().optional()
}))
.query(async ({ input }) => {
return cached(`anime:search:${input.query}:${input.page ?? 1}`, async () => {
const response = await fetch(
`${anime_url}/api/v2/hianime/search?q=${input.query}&page=${input.page ?? 1}`);
if (!response.ok) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to search anime",
});
}
const data = await response.json() as AniwatchSearch;
if (data.status !== 200) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to search anime",
});
}
return data;
});
})
} satisfies TRPCRouterRecord;
+6
View File
@@ -0,0 +1,6 @@
import { initTRPC } from "@trpc/server";
export const t = initTRPC.create();
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
+309
View File
@@ -0,0 +1,309 @@
interface AniwatchHome {
status: number,
data: {
spotlightAnimes: SpotlightAnimes[];
trendingAnimes: TrendingAnimes[];
latestEpisodeAnimes: LatestEpisodeAnimes[];
topUpcomingAnimes: TopUpcomingAnimes[];
top10Animes: Top10Animes;
topAiringAnimes: TopAiringAnimes[];
genres: string[];
};
}
interface TopAiringAnimes {
id: string;
name: string;
jname?: string;
type?: string;
description: string;
poster: string;
otherInfo: string[];
}
interface Top10Animes {
today: Top10AnimesResult[];
week: Top10AnimesResult[];
month: Top10AnimesResult[];
}
interface Top10AnimesResult {
id: string;
name: string;
description: string;
poster: string;
episodes: { sub: number; dub: number };
}
interface TopUpcomingAnimes {
id: string;
name: string;
duration: string;
poster: string;
type: string;
rating: string | null;
episodes: { sub: number; dub: number };
}
interface LatestEpisodeAnimes {
id: string;
name: string;
description: string;
poster: string;
type: string;
rating: string | null;
episodes: { sub: number; dub: number };
}
interface NextEpisodeTiming {
status: number,
data: {
airingISOTimestamp: string | null;
airingTimestamp: number | null;
secondsUntilAiring: number | null;
};
}
interface TrendingAnimes {
rank: number;
id: string;
name: string;
poster: string;
}
interface SpotlightAnimes {
rank: number;
id: string;
name: string;
description: string;
poster: string;
jname: string;
episodes: { sub: number; dub: number };
otherInfo: [string, string, string, string];
}
interface AniwatchInfo {
status: number;
data: {
anime: {
info: {
id: string;
anilistId: number;
malId: number;
name: string;
poster: string;
description: string;
stats: {
rating: string;
quality: string;
episodes: { sub: number; dub: number };
type: string;
duration: string;
promotionalVideos: {
title: string | undefined,
source: string | undefined,
thumbnail: string | undefined
}[],
characterVoiceActor: {
character: {
id: string,
poster: string,
name: string,
cast: string
},
voiceActor: {
id: string,
poster: string,
name: string,
cast: string
}
}[]
};
};
moreInfo: {
japanese: string;
synonyms: string;
aired: string;
premiered: string;
duration: string;
status: "Finished Airing" | "Currently Airing" | ({} & string);
malscore: string;
genres: string[];
studios: string;
producers: string[];
};
};
seasons: {
id: string;
name: string;
title: string;
poster: string;
isCurrent: boolean;
}[];
mostPopularAnimes: {
id: string;
name: string;
poster: string;
jname: string;
episodes: {
sub: number;
dub: number;
};
type: string;
}[];
relatedAnimes: {
id: string;
name: string;
poster: string;
jname: string;
episodes: {
sub: number;
dub: number;
};
type: '';
}[];
recommendedAnimes: {
id: string;
name: string;
jname?: string;
poster: string;
duration: string;
type: string;
rating: string;
episodes: {
sub: number;
dub: number;
};
}[];
};
}
interface AniwatchEpisodeData {
status: number;
data: {
totalEpisodes: number;
episodes: {
title: string;
episodeId: string;
number: string;
isFiller: boolean;
}[];
};
}
interface AniwatchSearch {
status: number,
data: {
animes: Anime[];
mostPopularAnimes: {
id: string;
name: string;
poster: string;
jname: string;
episodes: { sub: number; dub: number };
type: string;
}[];
currentPage: number;
hasNextPage: boolean;
totalPages: number;
searchQuery: string;
searchFilters: object;
};
}
interface Anime {
id: string;
name: string;
poster: string;
duration: string;
type: string;
jname?: string;
rating: string;
episodes: { sub: number; dub: number };
}
interface AniwatchEpisodeSrc {
status: number,
data: {
headers: {
Referer: string,
"User-Agent": string,
},
tracks: { url: string; lang: string; default?: boolean }[];
intro: { start: number; end: number };
outro: { start: number; end: number };
sources: { url: string; type: string }[];
anilistID: number | null,
malID: number | null
};
}
interface AniwatchGenre {
status: number,
data: {
genreName: string;
animes: Anime[];
genres: string[];
topAiringAnimes: TopAiringAnimes[];
totalPages: number;
hasNextPage: boolean;
currentPage: number;
};
}
interface AniwatchStudio {
producerName: string;
animes: Anime[];
genres: string[];
topAiringAnimes: TopAiringAnimes[];
totalPages: number;
hasNextPage: boolean;
currentPage: number;
}
interface AniwatchCategories {
status: number,
data: {
genres: string[];
animes: Anime[];
top10Animes: Top10Animes;
category: string;
totalPages: number;
hasNextPage: boolean;
currentPage: number;
};
}
type AniwatchCategoriesName =
| "most-favorite"
| "most-popular"
| "subbed-anime"
| "dubbed-anime"
| "recently-updated"
| "recently-added"
| "top-upcoming"
| "top-airing"
| "movie"
| "special"
| "ova"
| "ona"
| "tv"
| "completed";
interface AniwatchServer {
status: number;
data: {
dub: {
serverName: string, serverId: number
}[],
sub: {
serverName: string, serverId: number
}[]
raw: {
serverName: string, serverId: number
}[]
episodeId: string,
episodeNo: number
}
}
+16
View File
@@ -0,0 +1,16 @@
interface ImportMetaEnv {
readonly VITE_USER_ID: string;
readonly VITE_PUBLIC_ENDPOINT: string;
readonly APP_URL:string
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare namespace NodeJS {
interface ProcessEnv {
readonly DB_URL: string
readonly DB_PASSWORD: string
}
}
+4
View File
@@ -0,0 +1,4 @@
declare module "hls.js/dist/hls.light.js" {
import Hls from "hls.js";
export default Hls;
}
+211
View File
@@ -0,0 +1,211 @@
type MovieResults = {
adult: boolean
genre_ids: number[]
backdrop_path: string
id: number
original_language: string
original_title: string
overview: string
popularity: number
poster_path: string
release_date: string
title: string
vote_average: number
vote_count: number
first_air_date: string
title_english: string
synopsis: string
imdb_id: number
name: string
image: string
known_for?: string[]
profile_path?: string
media_type: "movie" | "tv" | "person" | "collection"
seasons: TMDBMultiSeason[]
}
type TMDBMovie = {
page: number
results: MovieResults[]
total_pages: number
total_results: number
}
type TMDBMultiSearch = {
results: TMDBMultiResult[]
total_pages: number
total_results: number
page: number
}
type TMDBMultiResult = {
id: number
genre_ids: string[]
video: boolean
adult: boolean
backdrop_path: string
original_language: string
original_title: string
overview: string
popularity: number
poster_path: string
release_date: string
title: string
vote_average: number
vote_count: number
first_air_date: string
title_english: string
synopsis: string
imdb_id: number
name: string
image: string
media_type: "movie" | "tv" | "person" | "collection" | & (string)
seasons: TMDBMultiSeason[]
}
type TMDBMultiSeason = {
air_date: string
episode_count: number
id: number
name: string
overview: string
poster_path: string
season_number: string
vote_average: number
}
type TMDBMovieInfo = {
adult: boolean
backdrop_path: string
belongs_to_collection: {
id: number
name: string
poster_path: string
backdrop_path: string
}
budget: number
genres: { id: number; name: string }[]
homepage: string
id: number
imdb_id: number
origin_country: string[]
original_language: string
original_title: string
overview: string
popularity: number
poster_path: string
production_companies: {
id: number
logo_path: string | null
name: string
origin_country: string
}[]
production_countries: { iso_3166_1: string; name: string }[]
release_date: string
revenue: number
runtime: number
spoken_languages: { english_name: string; iso_639_1: string; name: string }[]
status: string
tagline: string
title: string
video: boolean
vote_average: boolean
vote_count: boolean
}
type TMDBTvInfo = {
adult: boolean
title: string
synopsis?: string
overview?: string
media_type?: string
backdrop_path: string
created_by: []
episode_run_time: number[]
first_air_date: string
genres: { id: string; name: string }[]
homepage: string
id: number
in_production: boolean
languages: string[]
last_air_date: string
last_episode_to_air: {
id: number
overview: string
name: string
vote_average: number
vote_count: number
air_date: string
episode_number: number
episode_type: string
production_code: ""
runtime: number | string
season_number: number
show_id: number
still_path: string
}
name: string
next_episode_to_air: null
networks: []
number_of_episodes: number
number_of_seasons: number
origin_country: string[]
original_language: string
original_name: string
popularity: 85.99
poster_path: string
production_companies: {
id: number
logo_path: string
name: string
origin_country: string
}[]
production_countries: { iso_3166_1: string; name: string }[]
seasons: {
air_date: string
episode_count: number
id: number
name: string
overview: string
poster_path: string
season_number: number
vote_average: number
}[]
spoken_languages: { english_name: string; iso_639_1: string; name: string }[]
status: string
tagline: string
type: string
vote_average: string
vote_count: number
}
type TMDBEpisodesInfo = {
air_date: string
episodes: TMDBEpisodeResult[]
name: string
overview: string
id: number
poster_path?: string
season_number: number
vote_average: number
}
type TMDBEpisodeResult = {
air_date: string
season: number | string
episode: number | string
episode_number: string
episode_type: string
id: number
name: string
overview: string
production_code: string
runtime: number
season_number: number
show_id: number
still_path: string
vote_average: number
vote_count: number
crew: []
guest_stars: []
}
+49
View File
@@ -0,0 +1,49 @@
export interface BackoffOptions {
shouldNotThrow?: boolean;
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
factor?: number;
}
export type BackoffResult<T> =
| { success: true; data: T; attempts: number }
| { success: false; error: Error; attempts: number };
export async function withBackoff<T>(
fn: () => Promise<T>,
options: BackoffOptions = {},
): Promise<BackoffResult<T>> {
const {
maxRetries = 5,
initialDelay = 1000,
maxDelay = 30000,
factor = 2,
shouldNotThrow = false
} = options;
let lastError!: Error;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const data = await fn();
return { success: true, data, attempts: attempt + 1 };
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxRetries - 1) {
return { success: false, error: lastError, attempts: attempt + 1 };
}
const delay = Math.min(
initialDelay * Math.pow(factor, attempt),
maxDelay,
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
if(!shouldNotThrow) throw lastError;
return { success: false, error: lastError, attempts: maxRetries };
}
+40
View File
@@ -0,0 +1,40 @@
export const shouldCacheLonger = (src: AniwatchInfo, key: string) => {
const { moreInfo, info } = src.data.anime;
if (moreInfo.status === "Currently Airing") return false
const endedAt = moreInfo.aired.split(" to ")[1]; // ? if anime hasn't ended
if (!endedAt || endedAt === "?") {
return false;
}
const endedAtDate = new Date(endedAt);
// check if older than 6 months
const isOld = endedAtDate.getTime() < Date.now() - 6 * 30 * 24 * 60 * 60 * 1000;
const isVeryOld = endedAtDate.getTime() < Date.now() - 24 * 30 * 24 * 60 * 60 * 1000;
if (
isOld &&
moreInfo.status === "Finished Airing" &&
info.stats.episodes.sub === info.stats.episodes.dub
) {
console.log(`[CACHE] ${key} cached longer has dub, sub and finished airing`);
return true
};
if (
isOld &&
isVeryOld &&
moreInfo.status === "Finished Airing" &&
info.stats.episodes.dub === 0 &&
info.stats.episodes.sub
) {
console.log(`[CACHE] ${key} cached longer has sub and finished airing 2 years ago`);
return true
};
return false;
}
+58
View File
@@ -0,0 +1,58 @@
type AnyRecord = Record<string, number | boolean | null | undefined | string>;
interface SearchParams {
ep?: number;
lang?: "jp" | "en";
toString: (args: AnyRecord) => string;
}
export function createQueryString(
options: Record<string, string | number | boolean | undefined | null>,
args: AnyRecord
): string {
const params = new URLSearchParams();
Object.entries(options).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.append(key, String(value));
}
});
Object.entries(args)?.forEach(([key, value]) => {
params.append(key, String(value));
});
const queryString = params.toString();
return queryString ? `?${queryString}` : "";
}
export function getLinkParams(id: string): {
params: { id: string };
search: SearchParams;
} {
if (!id.includes("?")) return { params: { id: id }, search: {} };
const items = {
id: id?.split("?")[0],
ep: Number(id.split("?")[1].split("=")[1]) || 1,
lang: (id.split("?")[1].split("=")[2] as SearchParams["lang"]) || "jp",
};
return {
params: {
id: items.id,
},
search: {
ep: items.ep,
lang: items.lang,
toString(opt) {
return createQueryString({ ep: this.ep, lang: this.lang }, opt);
},
},
};
}
export const getBaseUrl = () => {
const app_url = import.meta.env.APP_URL;
if (app_url) return app_url;
if (typeof window !== "undefined") return "";
return `http://localhost:${import.meta.env.PORT ?? 3000}`;
};
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": [
"@solidjs/start/env"
],
"isolatedModules": true,
"paths": {
"~/*": [
"./src/*"
]
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import { solidStart } from "@solidjs/start/config";
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
export default defineConfig(() => {
return {
plugins: [
// solid(),
solidStart({
middleware: "./src/middleware/index.ts",
ssr:true,
}),
],
};
});