├── .npmrc ├── static ├── favicon.png └── Inter-roman.var.woff2 ├── src ├── lib │ ├── components │ │ ├── player │ │ │ ├── index.ts │ │ │ └── Player.svelte │ │ ├── ui │ │ │ ├── aspect-ratio │ │ │ │ ├── index.ts │ │ │ │ └── aspect-ratio.svelte │ │ │ ├── label │ │ │ │ ├── index.ts │ │ │ │ ├── Label.svelte │ │ │ │ └── label.svelte │ │ │ ├── checkbox │ │ │ │ ├── index.ts │ │ │ │ ├── Checkbox.svelte │ │ │ │ └── checkbox.svelte │ │ │ ├── slider │ │ │ │ ├── index.ts │ │ │ │ └── slider.svelte │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ ├── Switch.svelte │ │ │ │ └── switch.svelte │ │ │ ├── progress │ │ │ │ ├── index.ts │ │ │ │ ├── Progress.svelte │ │ │ │ └── progress.svelte │ │ │ ├── skeleton │ │ │ │ ├── index.ts │ │ │ │ ├── Skeleton.svelte │ │ │ │ └── skeleton.svelte │ │ │ ├── separator │ │ │ │ ├── index.ts │ │ │ │ ├── Separator.svelte │ │ │ │ └── separator.svelte │ │ │ ├── dialog │ │ │ │ ├── dialog-portal.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── index.ts │ │ │ │ └── dialog-content.svelte │ │ │ ├── tooltip │ │ │ │ ├── index.ts │ │ │ │ └── tooltip-content.svelte │ │ │ ├── select │ │ │ │ ├── select.svelte │ │ │ │ ├── select-separator.svelte │ │ │ │ ├── select-label.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── select-trigger.svelte │ │ │ │ ├── select-content.svelte │ │ │ │ └── select-item.svelte │ │ │ ├── card │ │ │ │ ├── card-content.svelte │ │ │ │ ├── card-footer.svelte │ │ │ │ ├── card-header.svelte │ │ │ │ ├── card-description.svelte │ │ │ │ ├── Card.svelte │ │ │ │ ├── card.svelte │ │ │ │ ├── card-title.svelte │ │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ │ ├── dropdown-menu-radio-group.svelte │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ │ └── index.ts │ │ │ ├── table │ │ │ │ ├── table-header.svelte │ │ │ │ ├── table-body.svelte │ │ │ │ ├── table-cell.svelte │ │ │ │ ├── table-caption.svelte │ │ │ │ ├── table-footer.svelte │ │ │ │ ├── Table.svelte │ │ │ │ ├── table.svelte │ │ │ │ ├── table-head.svelte │ │ │ │ ├── table-row.svelte │ │ │ │ └── index.ts │ │ │ ├── alert │ │ │ │ ├── alert-description.svelte │ │ │ │ ├── Alert.svelte │ │ │ │ ├── alert.svelte │ │ │ │ ├── alert-title.svelte │ │ │ │ └── index.ts │ │ │ ├── tabs │ │ │ │ ├── index.ts │ │ │ │ ├── tabs-list.svelte │ │ │ │ ├── tabs-content.svelte │ │ │ │ └── tabs-trigger.svelte │ │ │ ├── badge │ │ │ │ ├── Badge.svelte │ │ │ │ ├── badge.svelte │ │ │ │ └── index.ts │ │ │ ├── button │ │ │ │ ├── Button.svelte │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ ├── Input.svelte │ │ │ │ └── input.svelte │ │ │ └── toggle │ │ │ │ ├── Toggle.svelte │ │ │ │ ├── toggle.svelte │ │ │ │ └── index.ts │ │ ├── episodes │ │ │ ├── index.ts │ │ │ ├── stores.ts │ │ │ └── Episodes.svelte │ │ ├── cards │ │ │ ├── index.ts │ │ │ ├── SkeletonCard.svelte │ │ │ └── AnimeCard.svelte │ │ └── light-switch │ │ │ ├── local-storage-store.ts │ │ │ └── light-switch.ts │ ├── settings.ts │ ├── icons │ │ ├── index.ts │ │ ├── Discord.svelte │ │ ├── AniList.svelte │ │ ├── Simkl.svelte │ │ └── Kitsu.svelte │ ├── server │ │ ├── prisma.ts │ │ └── lucia.ts │ ├── api.ts │ ├── utils.ts │ └── types.ts ├── routes │ ├── +layout.server.ts │ ├── (auth) │ │ ├── oauth │ │ │ ├── +server.ts │ │ │ └── discord │ │ │ │ └── +server.ts │ │ ├── logout │ │ │ └── +server.ts │ │ ├── create │ │ │ ├── +page.svelte │ │ │ └── +page.server.ts │ │ ├── login │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ └── history │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── +page.server.ts │ ├── api │ │ └── history │ │ │ └── +server.ts │ ├── (anime) │ │ ├── search │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ │ ├── [animeId] │ │ │ ├── +page.ts │ │ │ ├── [providerId] │ │ │ │ └── [watchId] │ │ │ │ │ └── [episode] │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ ├── +page.ts │ │ │ │ │ └── +page.svelte │ │ │ └── +page.svelte │ │ └── dmca │ │ │ └── +page.svelte │ ├── +error.svelte │ ├── (admin) │ │ └── whitelist │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── +page.ts │ ├── +page.svelte │ └── +layout.svelte ├── app.html ├── hooks.server.ts ├── app.d.ts └── app.postcss ├── vite.config.ts ├── .gitignore ├── .prettierignore ├── components.json ├── postcss.config.cjs ├── .prettierrc ├── tsconfig.json ├── svelte.config.js ├── package.json ├── prisma └── schema.prisma ├── tailwind.config.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skearya/sift/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/components/player/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Player } from './Player.svelte'; 2 | -------------------------------------------------------------------------------- /static/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skearya/sift/HEAD/static/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /src/lib/components/ui/aspect-ratio/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./aspect-ratio.svelte"; 2 | 3 | export { Root, Root as AspectRatio }; 4 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./label.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./checkbox.svelte"; 2 | export { 3 | Root, 4 | // 5 | Root as Checkbox 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/slider/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./slider.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Slider 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./switch.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Switch 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./progress.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Progress 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./skeleton.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Skeleton 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/episodes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Episodes } from './Episodes.svelte'; 2 | export { animeId, toastState } from './stores'; 3 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./separator.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Separator 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/cards/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AnimeCard } from './AnimeCard.svelte'; 2 | export { default as SkeletonCard } from './SkeletonCard.svelte'; 3 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /src/lib/components/episodes/stores.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export let toastState = writable(false); 4 | export let animeId = writable(21); 5 | -------------------------------------------------------------------------------- /src/lib/settings.ts: -------------------------------------------------------------------------------- 1 | import { persisted } from 'svelte-persisted-store'; 2 | 3 | export const preferences = persisted('preferences', { 4 | type: 'romaji' as 'romaji' | 'native' | 'english' 5 | }); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | dev.db 12 | tempCodeRunnerFile.ts -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /src/lib/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Discord } from './Discord.svelte'; 2 | export { default as AniList } from './AniList.svelte'; 3 | export { default as Kitsu } from './Kitsu.svelte'; 4 | export { default as Simkl } from './Simkl.svelte'; 5 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { loadFlash } from 'sveltekit-flash-message/server'; 2 | 3 | export const load = loadFlash(async ({ url, locals }) => { 4 | const session = await locals.auth.validate(); 5 | 6 | return { 7 | user: session?.user, 8 | url: url.pathname 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /src/lib/server/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import { env } from '$env/dynamic/private'; 3 | 4 | const prisma = global.__prisma || new PrismaClient(); 5 | 6 | if (env.NODE_ENV === 'development') { 7 | global.__prisma = prisma; 8 | } 9 | 10 | export { prisma }; 11 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/app.postcss", 7 | "baseColor": "slate" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils" 12 | } 13 | } -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/aspect-ratio/aspect-ratio.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | const config = { 5 | plugins: [ 6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 7 | tailwindcss(), 8 | //But others, like autoprefixer, need to run after, 9 | autoprefixer 10 | ] 11 | }; 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": [ 7 | "prettier-plugin-svelte", 8 | "prettier-plugin-tailwindcss" 9 | ], 10 | "pluginSearchDirs": [ 11 | "." 12 | ], 13 | "overrides": [ 14 | { 15 | "files": "*.svelte", 16 | "options": { 17 | "parser": "svelte" 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from "bits-ui"; 2 | import Content from "./tooltip-content.svelte"; 3 | 4 | const Root = TooltipPrimitive.Root; 5 | const Trigger = TooltipPrimitive.Trigger; 6 | 7 | export { 8 | Root, 9 | Trigger, 10 | Content, 11 | // 12 | Root as Tooltip, 13 | Content as TooltipContent, 14 | Trigger as TooltipTrigger 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/Skeleton.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/skeleton.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |

12 | 13 |

14 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-separator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-body.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import { Tabs as TabsPrimitive } from "bits-ui"; 2 | import Content from "./tabs-content.svelte"; 3 | import List from "./tabs-list.svelte"; 4 | import Trigger from "./tabs-trigger.svelte"; 5 | 6 | const Root = TabsPrimitive.Root; 7 | 8 | export { 9 | Root, 10 | Content, 11 | List, 12 | Trigger, 13 | // 14 | Root as Tabs, 15 | Content as TabsContent, 16 | List as TabsList, 17 | Trigger as TabsTrigger 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-cell.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes/(auth)/oauth/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { discordAuth } from '$lib/server/lucia'; 3 | import { redirect } from '@sveltejs/kit'; 4 | 5 | export const GET: RequestHandler = async ({ cookies }) => { 6 | const [url, state] = await discordAuth.getAuthorizationUrl(); 7 | 8 | cookies.set('discord_oauth_state', state, { 9 | path: '/', 10 | maxAge: 60 * 60 11 | }); 12 | 13 | throw redirect(302, url.toString()); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-caption.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/Card.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-label.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/Table.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-head.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-list.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/Badge.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/badge.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { prisma } from '$lib/server/prisma'; 3 | 4 | export const load = (async ({ locals }) => { 5 | const session = await locals.auth.validate(); 6 | 7 | async function fetchHistory() { 8 | return await prisma.episode.findMany({ 9 | where: { 10 | UserData: { user_id: session!.user?.userId } 11 | }, 12 | orderBy: { createdAt: 'desc' }, 13 | take: 6 14 | }); 15 | } 16 | 17 | return { 18 | history: fetchHistory() 19 | }; 20 | }) satisfies PageServerLoad; 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-row.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/routes/(auth)/logout/+server.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '$lib/server/lucia'; 2 | import { redirect } from '@sveltejs/kit'; 3 | import type { RequestHandler } from './$types'; 4 | 5 | export const GET: RequestHandler = async ({ locals }) => { 6 | const session = await locals.auth.validate(); 7 | 8 | if (session) { 9 | await auth.invalidateSession(session.sessionId); 10 | 11 | locals.auth.setSession(null); 12 | 13 | await auth.deleteDeadUserSessions(session.user.userId); 14 | 15 | throw redirect(303, '/login'); 16 | } 17 | 18 | return new Response(); 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/Alert.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/Label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./card.svelte"; 2 | import Content from "./card-content.svelte"; 3 | import Description from "./card-description.svelte"; 4 | import Footer from "./card-footer.svelte"; 5 | import Header from "./card-header.svelte"; 6 | import Title from "./card-title.svelte"; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle 22 | }; 23 | 24 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "types": ["vidstack/svelte"] 14 | } 15 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/Separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '$lib/server/lucia'; 2 | import { redirect, type Handle } from '@sveltejs/kit'; 3 | 4 | export const handle = (async ({ event, resolve }) => { 5 | event.locals.auth = auth.handleRequest(event); 6 | 7 | const session = await event.locals.auth.validate(); 8 | const { pathname } = event.url; 9 | 10 | if ( 11 | pathname.startsWith('/') && 12 | pathname != '/create' && 13 | pathname != '/login' && 14 | pathname != '/logout' && 15 | pathname != '/dmca' && 16 | !pathname.startsWith('/oauth') 17 | ) { 18 | if (!session || !session.user.authorized) { 19 | throw redirect(303, '/login'); 20 | } 21 | } 22 | 23 | return await resolve(event); 24 | }) satisfies Handle; 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./table.svelte"; 2 | import Body from "./table-body.svelte"; 3 | import Caption from "./table-caption.svelte"; 4 | import Cell from "./table-cell.svelte"; 5 | import Footer from "./table-footer.svelte"; 6 | import Head from "./table-head.svelte"; 7 | import Header from "./table-header.svelte"; 8 | import Row from "./table-row.svelte"; 9 | 10 | export { 11 | Root, 12 | Body, 13 | Caption, 14 | Cell, 15 | Footer, 16 | Head, 17 | Header, 18 | Row, 19 | // 20 | Root as Table, 21 | Body as TableBody, 22 | Caption as TableCaption, 23 | Cell as TableCell, 24 | Footer as TableFooter, 25 | Head as TableHead, 26 | Header as TableHeader, 27 | Row as TableRow 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/Button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaClient } from '@prisma/client'; 2 | 3 | /// 4 | declare global { 5 | namespace App { 6 | interface Error { 7 | message: string; 8 | info?: string; 9 | } 10 | interface Locals { 11 | auth: import('lucia').AuthRequest; 12 | } 13 | interface PageData { 14 | flash?: { type: 'success' | 'error'; message: string }; 15 | } 16 | // interface Platform {} 17 | } 18 | 19 | var __prisma: PrismaClient; 20 | 21 | namespace Lucia { 22 | type Auth = import('$lib/server/lucia').Auth; 23 | type DatabaseUserAttributes = { 24 | discordId: string; 25 | username: string; 26 | authorized: boolean; 27 | }; 28 | type DatabaseSessionAttributes = {}; 29 | } 30 | } 31 | 32 | export {}; 33 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/Progress.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 |
26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/progress.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 |
26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./input.svelte"; 2 | 3 | type FormInputEvent = T & { 4 | currentTarget: EventTarget & HTMLInputElement; 5 | }; 6 | export type InputEvents = { 7 | blur: FormInputEvent; 8 | change: FormInputEvent; 9 | click: FormInputEvent; 10 | focus: FormInputEvent; 11 | keydown: FormInputEvent; 12 | keypress: FormInputEvent; 13 | keyup: FormInputEvent; 14 | mouseover: FormInputEvent; 15 | mouseenter: FormInputEvent; 16 | mouseleave: FormInputEvent; 17 | paste: FormInputEvent; 18 | input: FormInputEvent; 19 | }; 20 | 21 | export { 22 | Root, 23 | // 24 | Root as Input 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle/Toggle.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle/toggle.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | import { Select as SelectPrimitive } from "bits-ui"; 2 | 3 | import Root from "./select.svelte"; 4 | import Label from "./select-label.svelte"; 5 | import Item from "./select-item.svelte"; 6 | import Content from "./select-content.svelte"; 7 | import Trigger from "./select-trigger.svelte"; 8 | import Separator from "./select-separator.svelte"; 9 | 10 | const Group = SelectPrimitive.Group; 11 | const Input = SelectPrimitive.Input; 12 | const Value = SelectPrimitive.Value; 13 | export { 14 | Root, 15 | Group, 16 | Input, 17 | Label, 18 | Item, 19 | Value, 20 | Content, 21 | Trigger, 22 | Separator, 23 | // 24 | Root as Select, 25 | Group as SelectGroup, 26 | Input as SelectInput, 27 | Label as SelectLabel, 28 | Item as SelectItem, 29 | Value as SelectValue, 30 | Content as SelectContent, 31 | Trigger as SelectTrigger, 32 | Separator as SelectSeparator 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { tv, type VariantProps } from "tailwind-variants"; 2 | export { default as Badge } from "./badge.svelte"; 3 | 4 | export const badgeVariants = tv({ 5 | base: "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none select-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 6 | variants: { 7 | variant: { 8 | default: 9 | "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground", 10 | secondary: 11 | "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground", 12 | destructive: 13 | "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", 14 | outline: "text-foreground" 15 | } 16 | }, 17 | defaultVariants: { 18 | variant: "default" 19 | } 20 | }); 21 | 22 | export type Variant = VariantProps["variant"]; 23 | -------------------------------------------------------------------------------- /src/lib/components/cards/SkeletonCard.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess'; 2 | import adapter from '@sveltejs/adapter-auto'; 3 | import { vitePreprocess } from '@sveltejs/kit/vite'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 8 | // for more information about preprocessors 9 | preprocess: [ 10 | vitePreprocess(), 11 | preprocess({ 12 | postcss: true 13 | }) 14 | ], 15 | kit: { 16 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 17 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 18 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 19 | adapter: adapter(), 20 | alias: { 21 | $components: 'src/lib/components', 22 | '$components/*': 'src/lib/components/*' 23 | } 24 | } 25 | }; 26 | export default config; 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from "bits-ui"; 2 | 3 | const Root = DialogPrimitive.Root; 4 | const Trigger = DialogPrimitive.Trigger; 5 | 6 | import Title from "./dialog-title.svelte"; 7 | import Portal from "./dialog-portal.svelte"; 8 | import Footer from "./dialog-footer.svelte"; 9 | import Header from "./dialog-header.svelte"; 10 | import Overlay from "./dialog-overlay.svelte"; 11 | import Content from "./dialog-content.svelte"; 12 | import Description from "./dialog-description.svelte"; 13 | 14 | export { 15 | Root, 16 | Title, 17 | Portal, 18 | Footer, 19 | Header, 20 | Trigger, 21 | Overlay, 22 | Content, 23 | Description, 24 | // 25 | Root as Dialog, 26 | Title as DialogTitle, 27 | Portal as DialogPortal, 28 | Footer as DialogFooter, 29 | Header as DialogHeader, 30 | Trigger as DialogTrigger, 31 | Overlay as DialogOverlay, 32 | Content as DialogContent, 33 | Description as DialogDescription 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-trigger.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 |
25 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/slider/slider.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 23 | 24 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/routes/(auth)/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 |

Create Account

12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import ky from 'ky'; 2 | 3 | export const api = ky.create({ prefixUrl: 'https://api.anify.tv/', timeout: 15000 }); 4 | 5 | export const validCover = (url: string | undefined) => 6 | url && 7 | url !== 'https://simkl.in/episodes/null_c.jpg' && 8 | url !== 'https://image.tmdb.org/t/p/w500null'; 9 | 10 | export function bestFallback(artwork: Artwork[]): string { 11 | let newImg = ''; 12 | 13 | for (let i = 0; i < artwork.length; i++) { 14 | if (artwork[i].providerId == 'mal') { 15 | newImg = artwork[i].img; 16 | break; 17 | } 18 | if (artwork[i].providerId == 'anilist') { 19 | newImg = artwork[i].img; 20 | break; 21 | } 22 | if (artwork[i].providerId == 'anilist' && artwork[i].img.includes('large')) { 23 | newImg = artwork[i].img; 24 | break; 25 | } 26 | if (artwork[i].providerId == 'kitsu' && artwork[i].type == 'poster') { 27 | newImg = artwork[i].img; 28 | break; 29 | } 30 | } 31 | 32 | return newImg; 33 | } 34 | 35 | interface Artwork { 36 | img: string; 37 | type: string; 38 | providerId: string; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import { tv, type VariantProps } from "tailwind-variants"; 2 | 3 | import Root from "./alert.svelte"; 4 | import Description from "./alert-description.svelte"; 5 | import Title from "./alert-title.svelte"; 6 | 7 | export const alertVariants = tv({ 8 | base: "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", 9 | 10 | variants: { 11 | variant: { 12 | default: "bg-background text-foreground", 13 | destructive: 14 | "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive" 15 | } 16 | }, 17 | defaultVariants: { 18 | variant: "default" 19 | } 20 | }); 21 | 22 | export type Variant = VariantProps["variant"]; 23 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 24 | 25 | export { 26 | Root, 27 | Description, 28 | Title, 29 | // 30 | Root as Alert, 31 | Description as AlertDescription, 32 | Title as AlertTitle 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/Input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | -------------------------------------------------------------------------------- /src/lib/server/lucia.ts: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private'; 2 | import { lucia } from 'lucia'; 3 | import { sveltekit } from 'lucia/middleware'; 4 | import { discord } from '@lucia-auth/oauth/providers'; 5 | import { prisma } from '@lucia-auth/adapter-prisma'; 6 | import { prisma as PrismaClient } from '$lib/server/prisma'; 7 | import { dev } from '$app/environment'; 8 | 9 | export const auth = lucia({ 10 | adapter: prisma(PrismaClient, { 11 | user: 'authUser', 12 | key: 'authKey', 13 | session: 'authSession' 14 | }), 15 | env: dev ? 'DEV' : 'PROD', 16 | middleware: sveltekit(), 17 | sessionExpiresIn: { 18 | activePeriod: 1000 * 60 * 60 * 24 * 7, 19 | idlePeriod: 1000 * 60 * 60 * 24 * 14 20 | }, 21 | getUserAttributes: (userData) => { 22 | return { 23 | discordId: userData.discordId, 24 | username: userData.username, 25 | authorized: userData.authorized 26 | }; 27 | } 28 | }); 29 | 30 | export const discordAuth = discord(auth, { 31 | clientId: env.CLIENT_ID!, 32 | clientSecret: env.CLIENT_SECRET!, 33 | redirectUri: env.REDIRECT_URI! 34 | }); 35 | 36 | export type Auth = typeof auth; 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/switch/Switch.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/switch/switch.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /src/routes/api/history/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { prisma } from '$lib/server/prisma'; 3 | 4 | export const POST: RequestHandler = async ({ request, locals }) => { 5 | const session = await locals.auth.validate(); 6 | 7 | let { animeName, animeId, providerId, watchId, episode, length, time } = await request.json(); 8 | 9 | let userData = await prisma.userData.findUnique({ 10 | where: { user_id: session?.user!.userId }, 11 | select: { 12 | id: true 13 | } 14 | }); 15 | 16 | await prisma.userData.update({ 17 | where: { 18 | user_id: session?.user!.userId 19 | }, 20 | data: { 21 | watchHistory: { 22 | update: { 23 | where: { 24 | animeId_userDataId: { 25 | animeId, 26 | userDataId: userData!.id 27 | } 28 | }, 29 | data: { 30 | animeId, 31 | episodeNumber: Number(episode), 32 | animeName, 33 | providerId, 34 | watchId, 35 | progress: time, 36 | totalLength: length, 37 | createdAt: new Date() 38 | } 39 | } 40 | } 41 | } 42 | }); 43 | 44 | return new Response(); 45 | }; 46 | -------------------------------------------------------------------------------- /src/routes/(anime)/search/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types'; 2 | import { error, redirect } from '@sveltejs/kit'; 3 | import { api, bestFallback } from '$lib/api'; 4 | import type { Anime, MinifiedAnime } from '$lib/types'; 5 | 6 | export const load = (async ({ fetch, url }) => { 7 | if (!url.searchParams.get('query')) throw redirect(303, '/'); 8 | 9 | async function fetchResults() { 10 | try { 11 | let response = await api( 12 | `search-advanced?type=anime&page=${ 13 | url.searchParams.get('page') || 1 14 | }&query=${encodeURIComponent(url.searchParams.get('query')!)}`, 15 | { fetch } 16 | ).json(); 17 | 18 | let minifiedResponse: MinifiedAnime[] = response.map((anime: Anime) => ({ 19 | id: anime.id, 20 | coverImage: anime.coverImage, 21 | title: anime.title, 22 | year: anime.year, 23 | fallback: bestFallback(anime.artwork) 24 | })); 25 | 26 | return minifiedResponse; 27 | } catch (e: any) { 28 | throw error(500, e.message); 29 | } 30 | } 31 | 32 | return { 33 | streamed: { 34 | response: fetchResults() 35 | } 36 | }; 37 | }) satisfies PageLoad; 38 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./toggle.svelte"; 2 | import { tv, type VariantProps } from "tailwind-variants"; 3 | 4 | export const toggleVariants = tv({ 5 | base: "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background hover:bg-muted hover:text-muted-foreground", 6 | variants: { 7 | variant: { 8 | default: "bg-transparent", 9 | outline: 10 | "bg-transparent border border-input hover:bg-accent hover:text-accent-foreground" 11 | }, 12 | size: { 13 | default: "h-10 px-3", 14 | sm: "h-9 px-2.5", 15 | lg: "h-11 px-5" 16 | } 17 | }, 18 | defaultVariants: { 19 | variant: "default", 20 | size: "default" 21 | } 22 | }); 23 | 24 | export type Variant = VariantProps["variant"]; 25 | export type Size = VariantProps["size"]; 26 | 27 | export { 28 | Root, 29 | // 30 | Root as Toggle 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/icons/Discord.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-content.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 |
34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 |

{page.status}

15 |

{page.error?.message || 'An error occurred'}

16 | 17 | {#if animeId && providerId && watchId && episode} 18 | 28 | {/if} 29 | 30 | {#if page.error?.info} 31 | 32 |

More Info:

33 |
{page.error?.info}
34 | {/if} 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-item.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/Checkbox.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 28 | {#if isChecked} 29 | 30 | {:else if isIndeterminate} 31 | 32 | {/if} 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 28 | {#if isChecked} 29 | 30 | {:else if isIndeterminate} 31 | 32 | {/if} 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/lib/icons/AniList.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 26 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/routes/(auth)/login/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad, Actions } from './$types'; 2 | import { setFlash, redirect } from 'sveltekit-flash-message/server'; 3 | import { fail } from '@sveltejs/kit'; 4 | import { auth } from '$lib/server/lucia'; 5 | import { prisma } from '$lib/server/prisma'; 6 | 7 | export const load = (async ({ locals }) => { 8 | const session = await locals.auth.validate(); 9 | 10 | if (session?.user?.authorized) throw redirect(303, '/'); 11 | }) satisfies PageServerLoad; 12 | 13 | export const actions: Actions = { 14 | default: async (event) => { 15 | let { request, locals } = event; 16 | 17 | const form = await request.formData(); 18 | const username = form.get('username'); 19 | const password = form.get('password'); 20 | 21 | if (typeof username !== 'string' || typeof password !== 'string') { 22 | return setFlash({ type: 'error', message: 'Missing values' }, event); 23 | } 24 | 25 | try { 26 | const key = await auth.useKey('username', username, password); 27 | 28 | const session = await auth.createSession({ 29 | userId: key.userId, 30 | attributes: {} 31 | }); 32 | 33 | locals.auth.setSession(session); 34 | } catch { 35 | return setFlash({ type: 'error', message: 'Invalid username/password' }, event); 36 | } 37 | 38 | throw redirect(302, '/', { type: 'success', message: 'Logged in' }, event); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 28 | 29 | 32 | 33 | Close 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/routes/(auth)/history/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad, Actions } from './$types'; 2 | import { prisma } from '$lib/server/prisma'; 3 | import { fail } from '@sveltejs/kit'; 4 | import { setFlash } from 'sveltekit-flash-message/server'; 5 | 6 | export const load = (async ({ locals }) => { 7 | const session = await locals.auth.validate(); 8 | 9 | async function fetchHistory() { 10 | return await prisma.episode.findMany({ 11 | where: { 12 | UserData: { user_id: session!.user?.userId } 13 | }, 14 | orderBy: { createdAt: 'desc' } 15 | }); 16 | } 17 | 18 | return { 19 | history: fetchHistory() 20 | }; 21 | }) satisfies PageServerLoad; 22 | 23 | export const actions: Actions = { 24 | default: async (event) => { 25 | const { locals, request } = event; 26 | const session = await locals.auth.validate(); 27 | 28 | let form = await request.formData(); 29 | let id = form.get('id'); 30 | 31 | if (typeof id !== 'string') return fail(400); 32 | 33 | try { 34 | await prisma.userData.update({ 35 | where: { 36 | user_id: session?.user.userId 37 | }, 38 | data: { 39 | watchHistory: { 40 | delete: { 41 | id: id 42 | } 43 | } 44 | } 45 | }); 46 | } catch { 47 | return setFlash({ type: 'error', message: 'Something went wrong' }, event); 48 | } 49 | 50 | setFlash({ type: 'success', message: 'Episode removed' }, event); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/lib/icons/Simkl.svelte: -------------------------------------------------------------------------------- 1 | 9 | Simkl 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/routes/(auth)/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {#if !data.user} 15 |
16 |

Log In

17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | {:else} 33 | 34 | Error 35 | Not (yet?) whitelisted! 36 | 37 | {/if} 38 |
39 | -------------------------------------------------------------------------------- /src/routes/(anime)/[animeId]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types'; 2 | import type { Anime, ContentMetadata, EpisodeData } from '$lib/types'; 3 | import { api } from '$lib/api'; 4 | import { error, redirect } from '@sveltejs/kit'; 5 | 6 | export const load = (async ({ fetch, params }) => { 7 | let { animeId } = params; 8 | 9 | if (!animeId) throw redirect(303, `/search`); 10 | 11 | async function fetchInfo() { 12 | try { 13 | return await api(`info/${animeId}`, { fetch }).json(); 14 | } catch (e: any) { 15 | throw error(404, { 16 | message: 'Error fetching anime info', 17 | info: e.message 18 | }); 19 | } 20 | } 21 | 22 | async function fetchEpisodes() { 23 | try { 24 | let response = await api(`episodes/${animeId}`, { fetch }).json(); 25 | 26 | for (let i = 0; i < response.length; i++) { 27 | let firstItem = response[i].episodes[0].number; 28 | let lastItem = response[i].episodes[response[i].episodes.length - 1].number; 29 | 30 | if (firstItem > lastItem) { 31 | response[i].episodes.reverse(); 32 | } 33 | } 34 | 35 | return response; 36 | } catch (e: any) { 37 | throw error(404, { 38 | message: 'Error fetching episode info', 39 | info: e.message 40 | }); 41 | } 42 | } 43 | 44 | async function fetchCovers() { 45 | try { 46 | return await api(`content-metadata/${animeId}`, { fetch }).json(); 47 | } catch (e: any) { 48 | return {} as ContentMetadata[]; 49 | } 50 | } 51 | 52 | return { 53 | info: fetchInfo(), 54 | episodes: fetchEpisodes(), 55 | covers: fetchCovers() 56 | }; 57 | }) satisfies PageLoad; 58 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; 2 | import Item from "./dropdown-menu-item.svelte"; 3 | import Label from "./dropdown-menu-label.svelte"; 4 | import Content from "./dropdown-menu-content.svelte"; 5 | import Shortcut from "./dropdown-menu-shortcut.svelte"; 6 | import RadioItem from "./dropdown-menu-radio-item.svelte"; 7 | import Separator from "./dropdown-menu-separator.svelte"; 8 | import RadioGroup from "./dropdown-menu-radio-group.svelte"; 9 | import SubContent from "./dropdown-menu-sub-content.svelte"; 10 | import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; 11 | import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; 12 | 13 | const Sub = DropdownMenuPrimitive.Sub; 14 | const Root = DropdownMenuPrimitive.Root; 15 | const Trigger = DropdownMenuPrimitive.Trigger; 16 | const Group = DropdownMenuPrimitive.Group; 17 | 18 | export { 19 | Sub, 20 | Root, 21 | Item, 22 | Label, 23 | Group, 24 | Trigger, 25 | Content, 26 | Shortcut, 27 | Separator, 28 | RadioItem, 29 | SubContent, 30 | SubTrigger, 31 | RadioGroup, 32 | CheckboxItem, 33 | // 34 | Root as DropdownMenu, 35 | Sub as DropdownMenuSub, 36 | Item as DropdownMenuItem, 37 | Label as DropdownMenuLabel, 38 | Group as DropdownMenuGroup, 39 | Content as DropdownMenuContent, 40 | Trigger as DropdownMenuTrigger, 41 | Shortcut as DropdownMenuShortcut, 42 | RadioItem as DropdownMenuRadioItem, 43 | Separator as DropdownMenuSeparator, 44 | RadioGroup as DropdownMenuRadioGroup, 45 | SubContent as DropdownMenuSubContent, 46 | SubTrigger as DropdownMenuSubTrigger, 47 | CheckboxItem as DropdownMenuCheckboxItem 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sift", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "prisma db push && prisma generate && vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check .", 12 | "format": "prettier --plugin-search-dir . --write ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "^2.1.0", 16 | "@sveltejs/kit": "^1.26.0", 17 | "@tailwindcss/typography": "^0.5.10", 18 | "@types/node": "^20.8.7", 19 | "autoprefixer": "^10.4.16", 20 | "postcss": "^8.4.31", 21 | "postcss-load-config": "^4.0.1", 22 | "prettier": "^3.0.3", 23 | "prettier-plugin-svelte": "^3.0.3", 24 | "prettier-plugin-tailwindcss": "^0.5.6", 25 | "prisma": "^5.4.2", 26 | "svelte": "^4.2.2", 27 | "svelte-check": "^3.5.2", 28 | "svelte-preprocess": "^5.0.4", 29 | "tailwindcss": "^3.3.3", 30 | "tslib": "^2.6.2", 31 | "typescript": "^5.2.2", 32 | "vite": "^4.5.0" 33 | }, 34 | "type": "module", 35 | "dependencies": { 36 | "@lucia-auth/adapter-prisma": "^3.0.2", 37 | "@lucia-auth/oauth": "^3.3.1", 38 | "@melt-ui/svelte": "^0.55.3", 39 | "@prisma/client": "^5.4.2", 40 | "bits-ui": "^0.6.3", 41 | "clsx": "^2.0.0", 42 | "hls.js": "^1.4.12", 43 | "ky": "^1.1.0", 44 | "lucia": "^2.7.1", 45 | "lucide-svelte": "^0.288.0", 46 | "svelte-french-toast": "^1.2.0", 47 | "svelte-persisted-store": "^0.7.0", 48 | "sveltekit-flash-message": "^2.2.1", 49 | "tailwind-merge": "^1.14.0", 50 | "tailwind-variants": "^0.1.14", 51 | "vidstack": "^1.4.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./button.svelte"; 2 | import { tv, type VariantProps } from "tailwind-variants"; 3 | import type { Button as ButtonPrimitive } from "bits-ui"; 4 | 5 | const buttonVariants = tv({ 6 | base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 7 | variants: { 8 | variant: { 9 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 10 | destructive: 11 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 12 | outline: 13 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 14 | secondary: 15 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline" 18 | }, 19 | size: { 20 | default: "h-10 px-4 py-2", 21 | sm: "h-9 rounded-md px-3", 22 | lg: "h-11 rounded-md px-8", 23 | icon: "h-10 w-10" 24 | } 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default" 29 | } 30 | }); 31 | 32 | type Variant = VariantProps["variant"]; 33 | type Size = VariantProps["size"]; 34 | 35 | type Props = ButtonPrimitive.Props & { 36 | variant?: Variant; 37 | size?: Size; 38 | }; 39 | 40 | type Events = ButtonPrimitive.Events; 41 | 42 | export { 43 | Root, 44 | type Props, 45 | type Events, 46 | // 47 | Root as Button, 48 | type Props as ButtonProps, 49 | type Events as ButtonEvents, 50 | buttonVariants 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/components/cards/AnimeCard.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | Anime cover art= 6 ? 'lazy' : 'eager'} 22 | class="h-96 w-full rounded-md object-cover" 23 | data-fallback={anime.fallback} 24 | on:error={(event) => { 25 | // @ts-ignore 26 | if (event.target.src !== anime.fallback) { 27 | // @ts-ignore 28 | event.target.src = anime.fallback; 29 | } 30 | }} 31 | /> 32 | 33 |
34 | {anime.title[nameType] || anime.id} 35 | 36 |

{anime.year || ''}

37 |
38 |
39 | 40 | 49 | 50 | 51 |
52 | -------------------------------------------------------------------------------- /src/routes/(auth)/create/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad, Actions } from './$types'; 2 | import { setFlash, redirect } from 'sveltekit-flash-message/server'; 3 | import { auth } from '$lib/server/lucia'; 4 | import { env } from '$env/dynamic/private'; 5 | import { prisma } from '$lib/server/prisma'; 6 | 7 | export const load = (async ({ locals }) => { 8 | const session = await locals.auth.validate(); 9 | 10 | if (session) throw redirect(303, '/'); 11 | }) satisfies PageServerLoad; 12 | 13 | export const actions: Actions = { 14 | default: async (event) => { 15 | let { request, locals } = event; 16 | 17 | const formData = await request.formData(); 18 | const username = formData.get('username'); 19 | const password = formData.get('password'); 20 | 21 | if (typeof username !== 'string' || username.length < 4 || username.length > 31) { 22 | return setFlash({ type: 'error', message: 'Invalid username' }, event); 23 | } 24 | if (typeof password !== 'string' || password.length < 6 || password.length > 255) { 25 | return setFlash({ type: 'error', message: 'Invalid password' }, event); 26 | } 27 | 28 | const user = await auth.createUser({ 29 | key: { 30 | providerId: 'username', 31 | providerUserId: username.toLowerCase(), 32 | password 33 | }, 34 | attributes: { 35 | username, 36 | discordId: 'none', 37 | authorized: username == env?.OWNER_USERNAME ? true : false 38 | } 39 | }); 40 | 41 | const session = await auth.createSession({ 42 | userId: user.userId, 43 | attributes: {} 44 | }); 45 | 46 | locals.auth.setSession(session); 47 | 48 | await prisma.userData.create({ 49 | data: { 50 | user: { 51 | connect: { 52 | id: user.userId 53 | } 54 | } 55 | } 56 | }); 57 | 58 | throw redirect(302, '/', { type: 'success', message: 'Created user' }, event); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/routes/(auth)/oauth/discord/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { auth, discordAuth } from '$lib/server/lucia'; 3 | import { redirect } from 'sveltekit-flash-message/server'; 4 | import { error } from '@sveltejs/kit'; 5 | import { env } from '$env/dynamic/private'; 6 | import { prisma } from '$lib/server/prisma'; 7 | 8 | export const GET: RequestHandler = async (event) => { 9 | let { cookies, url, locals } = event; 10 | 11 | const code = url.searchParams.get('code')!; 12 | const state = url.searchParams.get('state'); 13 | 14 | const storedState = cookies.get('discord_oauth_state'); 15 | 16 | if (!code) { 17 | throw error(404, 'code missing'); 18 | } 19 | 20 | if (!state || !storedState || state !== storedState) { 21 | throw error(404, 'state missing/invalid'); 22 | } 23 | 24 | try { 25 | const { getExistingUser, discordUser, createUser } = await discordAuth.validateCallback(code); 26 | 27 | const existingUser = await getExistingUser(); 28 | const getUser = async () => { 29 | if (existingUser) return existingUser; 30 | return await createUser({ 31 | attributes: { 32 | discordId: discordUser.id, 33 | username: discordUser.username, 34 | authorized: discordUser.id == env?.OWNER_ID ? true : false 35 | } 36 | }); 37 | }; 38 | 39 | const user = await getUser(); 40 | const session = await auth.createSession({ 41 | userId: user.userId, 42 | attributes: {} 43 | }); 44 | 45 | locals.auth.setSession(session); 46 | 47 | if (!existingUser) { 48 | await prisma.userData.create({ 49 | data: { 50 | user: { 51 | connect: { 52 | id: user!.userId 53 | } 54 | } 55 | } 56 | }); 57 | } 58 | } catch (e) { 59 | throw error(404, 'invalid code'); 60 | } 61 | 62 | throw redirect(302, '/', { type: 'success', message: 'Logged in' }, event); 63 | }; 64 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import { cubicOut } from 'svelte/easing'; 4 | import type { TransitionConfig } from 'svelte/transition'; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | 10 | type FlyAndScaleParams = { 11 | y?: number; 12 | x?: number; 13 | start?: number; 14 | duration?: number; 15 | }; 16 | 17 | export const flyAndScale = ( 18 | node: Element, 19 | params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } 20 | ): TransitionConfig => { 21 | const style = getComputedStyle(node); 22 | const transform = style.transform === 'none' ? '' : style.transform; 23 | 24 | const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => { 25 | const [minA, maxA] = scaleA; 26 | const [minB, maxB] = scaleB; 27 | 28 | const percentage = (valueA - minA) / (maxA - minA); 29 | const valueB = percentage * (maxB - minB) + minB; 30 | 31 | return valueB; 32 | }; 33 | 34 | const styleToString = (style: Record): string => { 35 | return Object.keys(style).reduce((str, key) => { 36 | if (style[key] === undefined) return str; 37 | return str + `${key}:${style[key]};`; 38 | }, ''); 39 | }; 40 | 41 | return { 42 | duration: params.duration ?? 200, 43 | delay: 0, 44 | css: (t) => { 45 | const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); 46 | const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); 47 | const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); 48 | 49 | return styleToString({ 50 | transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, 51 | opacity: t 52 | }); 53 | }, 54 | easing: cubicOut 55 | }; 56 | }; 57 | 58 | export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 59 | -------------------------------------------------------------------------------- /src/routes/(admin)/whitelist/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad, Actions } from './$types'; 2 | import { prisma } from '$lib/server/prisma'; 3 | import { error } from '@sveltejs/kit'; 4 | import { env } from '$env/dynamic/private'; 5 | import { redirect, setFlash } from 'sveltekit-flash-message/server'; 6 | 7 | export const load = (async ({ locals }) => { 8 | const session = await locals.auth.validate(); 9 | 10 | if ( 11 | session!.user?.discordId !== env?.OWNER_ID && 12 | session!.user?.username !== env?.OWNER_USERNAME 13 | ) { 14 | throw redirect(303, '/'); 15 | } 16 | 17 | let users = await prisma.authUser.findMany(); 18 | 19 | return { users }; 20 | }) satisfies PageServerLoad; 21 | 22 | export const actions = { 23 | default: async (event) => { 24 | const { locals, request } = event; 25 | 26 | const session = await locals.auth.validate(); 27 | 28 | if ( 29 | session!.user?.discordId !== env?.OWNER_ID && 30 | session!.user?.username !== env?.OWNER_USERNAME 31 | ) { 32 | throw error(401, 'Unauthorized'); 33 | } 34 | 35 | const formData = await request.formData(); 36 | const id = formData.get('id'); 37 | const discordId = formData.get('discordId'); 38 | const username = formData.get('username'); 39 | const value = formData.get('authorized'); 40 | 41 | if (!id || !discordId || !username || !value) throw error(400, 'Bad request'); 42 | 43 | if (discordId == env?.OWNER_ID || username == env?.OWNER_USERNAME) { 44 | return setFlash({ type: 'error', message: 'You cant deauthorize an admin!' }, event); 45 | } 46 | 47 | try { 48 | await prisma.authUser.update({ 49 | where: { 50 | id: id as string 51 | }, 52 | data: { 53 | authorized: value === 'true' 54 | } 55 | }); 56 | } catch { 57 | throw error(500, 'Error updating user'); 58 | } 59 | 60 | return setFlash({ type: 'success', message: 'User updated' }, event); 61 | } 62 | } satisfies Actions; 63 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mysql" 10 | url = env("DATABASE_URL") 11 | relationMode = "prisma" 12 | } 13 | 14 | model AuthUser { 15 | id String @id @unique 16 | auth_session AuthSession[] 17 | auth_key AuthKey[] 18 | 19 | username String @unique 20 | discordId String 21 | authorized Boolean 22 | userData UserData? 23 | 24 | @@map("auth_user") 25 | } 26 | 27 | model UserData { 28 | id String @id @default(uuid()) 29 | watchHistory Episode[] 30 | user AuthUser @relation(fields: [user_id], references: [id]) 31 | user_id String @unique 32 | } 33 | 34 | model Episode { 35 | id String @id @default(uuid()) 36 | createdAt DateTime @default(now()) 37 | animeName String? 38 | animeId String 39 | providerId String 40 | watchId String 41 | episodeNumber Float 42 | cover String? 43 | progress Int? 44 | totalLength Int? 45 | dubbed Boolean? 46 | 47 | UserData UserData? @relation(fields: [userDataId], references: [id]) 48 | userDataId String? 49 | 50 | @@unique([animeId, userDataId]) 51 | @@index([userDataId]) 52 | } 53 | 54 | model AuthSession { 55 | id String @id @unique 56 | user_id String 57 | active_expires BigInt 58 | idle_expires BigInt 59 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 60 | 61 | @@index([user_id]) 62 | @@map("auth_session") 63 | } 64 | 65 | model AuthKey { 66 | id String @id @unique 67 | hashed_password String? 68 | user_id String 69 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 70 | 71 | @@index([user_id]) 72 | @@map("auth_key") 73 | } 74 | -------------------------------------------------------------------------------- /src/routes/(admin)/whitelist/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 |

Users

19 | 20 | 21 | 22 | Total Users: {data.users.length} | Authorized Users: {data.users.filter((user) => 23 | user.authorized ? true : false 24 | ).length} 26 | 27 | 28 | Username 29 | Authorized 30 | 31 | 32 | 33 | {#each data.users as user, i (i)} 34 | 35 | {user.username} 36 | 37 |
43 | 44 | 45 | 46 | 53 | { 57 | // @ts-expect-error 58 | document.getElementById(user.id).requestSubmit(); 59 | }} 60 | /> 61 | 62 |
63 |
64 | {/each} 65 |
66 |
67 |
68 | -------------------------------------------------------------------------------- /src/app.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 47.4% 11.2%; 9 | 10 | --muted: 210 40% 96.1%; 11 | --muted-foreground: 215.4 16.3% 46.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 47.4% 11.2%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 47.4% 11.2%; 18 | 19 | --border: 214.3 31.8% 91.4%; 20 | --input: 214.3 31.8% 91.4%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --accent: 210 40% 96.1%; 29 | --accent-foreground: 222.2 47.4% 11.2%; 30 | 31 | --destructive: 0 92% 38%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 215 20.2% 65.1%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 224 71% 4%; 41 | --foreground: 213 31% 91%; 42 | 43 | --muted: 223 47% 11%; 44 | --muted-foreground: 215.4 16.3% 56.9%; 45 | 46 | --popover: 224 71% 4%; 47 | --popover-foreground: 215 20.2% 65.1%; 48 | 49 | --card: 224 71% 4%; 50 | --card-foreground: 213 31% 91%; 51 | 52 | --border: 216 34% 17%; 53 | --input: 216 34% 17%; 54 | 55 | --primary: 210 40% 98%; 56 | --primary-foreground: 222.2 47.4% 1.2%; 57 | 58 | --secondary: 222.2 47.4% 11.2%; 59 | --secondary-foreground: 210 40% 98%; 60 | 61 | --accent: 216 34% 17%; 62 | --accent-foreground: 210 40% 98%; 63 | 64 | --destructive: 359 51% 48%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 216 34% 17%; 68 | 69 | --radius: 0.5rem; 70 | } 71 | } 72 | 73 | @layer base { 74 | * { 75 | @apply border-border; 76 | } 77 | body { 78 | @apply bg-background text-foreground; 79 | font-feature-settings: 80 | 'rlig' 1, 81 | 'calt' 1; 82 | } 83 | *::-webkit-scrollbar-track { 84 | @apply bg-secondary; 85 | } 86 | *::-webkit-scrollbar-thumb { 87 | @apply rounded-[3px] bg-primary; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | const config = { 5 | darkMode: ["class"], 6 | content: ["./src/**/*.{html,js,svelte,ts}"], 7 | theme: { 8 | container: { 9 | center: true, 10 | padding: "2rem", 11 | screens: { 12 | "2xl": "1400px" 13 | } 14 | }, 15 | extend: { 16 | colors: { 17 | border: "hsl(var(--border))", 18 | input: "hsl(var(--input))", 19 | ring: "hsl(var(--ring))", 20 | background: "hsl(var(--background))", 21 | foreground: "hsl(var(--foreground))", 22 | primary: { 23 | DEFAULT: "hsl(var(--primary))", 24 | foreground: "hsl(var(--primary-foreground))" 25 | }, 26 | secondary: { 27 | DEFAULT: "hsl(var(--secondary))", 28 | foreground: "hsl(var(--secondary-foreground))" 29 | }, 30 | destructive: { 31 | DEFAULT: "hsl(var(--destructive))", 32 | foreground: "hsl(var(--destructive-foreground))" 33 | }, 34 | muted: { 35 | DEFAULT: "hsl(var(--muted))", 36 | foreground: "hsl(var(--muted-foreground))" 37 | }, 38 | accent: { 39 | DEFAULT: "hsl(var(--accent))", 40 | foreground: "hsl(var(--accent-foreground))" 41 | }, 42 | popover: { 43 | DEFAULT: "hsl(var(--popover))", 44 | foreground: "hsl(var(--popover-foreground))" 45 | }, 46 | card: { 47 | DEFAULT: "hsl(var(--card))", 48 | foreground: "hsl(var(--card-foreground))" 49 | } 50 | }, 51 | borderRadius: { 52 | lg: "var(--radius)", 53 | md: "calc(var(--radius) - 2px)", 54 | sm: "calc(var(--radius) - 4px)" 55 | }, 56 | fontFamily: { 57 | sans: [...fontFamily.sans] 58 | } 59 | } 60 | }, 61 | safelist: [ 62 | 'grid-cols-1', 63 | 'grid-cols-2', 64 | 'grid-cols-3', 65 | 'grid-cols-4', 66 | 'grid-cols-5', 67 | 'sm:grid-cols-1', 68 | 'sm:grid-cols-2', 69 | 'sm:grid-cols-3', 70 | 'sm:grid-cols-4', 71 | 'sm:grid-cols-5', 72 | 'w-10' 73 | ], 74 | plugins: [require('@tailwindcss/typography')] 75 | }; 76 | 77 | export default config; 78 | -------------------------------------------------------------------------------- /src/lib/icons/Kitsu.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types'; 2 | import type { Anime, MinifiedAnime, MinifiedSeasonalData, SeasonalData } from '$lib/types'; 3 | import { error } from '@sveltejs/kit'; 4 | import { api, bestFallback } from '$lib/api'; 5 | 6 | export const load = (async ({ fetch, data }) => { 7 | async function fetchSeasonal() { 8 | try { 9 | let response = await api(`seasonal/anime?fields=[id,title,coverImage,year,artwork]`, { 10 | fetch 11 | }).json(); 12 | 13 | let newResponse: MinifiedSeasonalData = { 14 | trending: [], 15 | popular: [], 16 | top: [], 17 | seasonal: [] 18 | }; 19 | 20 | for (const collection in response) { 21 | newResponse[collection] = response[collection].map((anime: Anime) => ({ 22 | id: anime.id, 23 | coverImage: anime.coverImage, 24 | title: anime.title, 25 | year: anime.year, 26 | fallback: bestFallback(anime.artwork) 27 | })); 28 | } 29 | 30 | return newResponse; 31 | } catch (e: any) { 32 | throw error(404, { 33 | message: 'Error fetching homepage data', 34 | info: e.message 35 | }); 36 | } 37 | } 38 | 39 | async function fetchRecent() { 40 | try { 41 | let response = await api(`recent?type=anime`, { fetch }).json(); 42 | 43 | let newReponse: (MinifiedAnime & { currentEpisode: number | undefined })[] = []; 44 | 45 | for (const anime of response) { 46 | newReponse.push({ 47 | id: anime.id, 48 | coverImage: anime.coverImage, 49 | title: anime.title, 50 | year: anime.year, 51 | currentEpisode: anime.currentEpisode, 52 | fallback: bestFallback(anime.artwork) 53 | }); 54 | } 55 | 56 | return newReponse; 57 | } catch (e: any) { 58 | return []; 59 | } 60 | } 61 | 62 | async function mergeData() { 63 | let [seasonal, recent] = await Promise.allSettled([fetchSeasonal(), fetchRecent()]); 64 | 65 | return { 66 | recent: recent.status == 'fulfilled' ? recent.value : [], 67 | ...(seasonal.status == 'fulfilled' ? seasonal.value : []) 68 | }; 69 | } 70 | 71 | return { 72 | ...data, 73 | streamed: { 74 | anime: mergeData() 75 | } 76 | }; 77 | }) satisfies PageLoad; 78 | -------------------------------------------------------------------------------- /src/lib/components/light-switch/local-storage-store.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/joshnuss/svelte-local-storage-store 2 | // https://github.com/joshnuss/svelte-local-storage-store/blob/master/index.ts 3 | // Represents version v0.4.0 (2023-01-18) 4 | import type { Writable } from 'svelte/store'; 5 | import { BROWSER } from 'esm-env'; 6 | import { get, writable as internal } from 'svelte/store'; 7 | 8 | declare type Updater = (value: T) => T; 9 | declare type StoreDict = { [key: string]: Writable }; 10 | 11 | /* eslint-disable @typescript-eslint/no-explicit-any */ 12 | const stores: StoreDict = {}; 13 | 14 | interface Serializer { 15 | parse(text: string): T; 16 | stringify(object: T): string; 17 | } 18 | 19 | type StorageType = 'local' | 'session'; 20 | 21 | interface Options { 22 | serializer?: Serializer; 23 | storage?: StorageType; 24 | } 25 | 26 | function getStorage(type: StorageType) { 27 | return type === 'local' ? localStorage : sessionStorage; 28 | } 29 | 30 | export function localStorageStore( 31 | key: string, 32 | initialValue: T, 33 | options?: Options 34 | ): Writable { 35 | const serializer = options?.serializer ?? JSON; 36 | const storageType = options?.storage ?? 'local'; 37 | 38 | function updateStorage(key: string, value: T) { 39 | if (!BROWSER) return; 40 | 41 | getStorage(storageType).setItem(key, serializer.stringify(value)); 42 | } 43 | 44 | if (!stores[key]) { 45 | const store = internal(initialValue, (set) => { 46 | const json = BROWSER ? getStorage(storageType).getItem(key) : null; 47 | 48 | if (json) { 49 | set(serializer.parse(json)); 50 | } 51 | 52 | if (BROWSER) { 53 | const handleStorage = (event: StorageEvent) => { 54 | if (event.key === key) set(event.newValue ? serializer.parse(event.newValue) : null); 55 | }; 56 | 57 | window.addEventListener('storage', handleStorage); 58 | 59 | return () => window.removeEventListener('storage', handleStorage); 60 | } 61 | }); 62 | 63 | const { subscribe, set } = store; 64 | 65 | stores[key] = { 66 | set(value: T) { 67 | updateStorage(key, value); 68 | set(value); 69 | }, 70 | update(updater: Updater) { 71 | const value = updater(get(store)); 72 | 73 | updateStorage(key, value); 74 | set(value); 75 | }, 76 | subscribe 77 | }; 78 | } 79 | 80 | return stores[key]; 81 | } 82 | -------------------------------------------------------------------------------- /src/routes/(auth)/history/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |

Watch History

12 | 13 |
14 | {#each data.history as episode} 15 |
18 | {#if episode?.cover && episode?.cover !== 'https://simkl.in/episodes/null_c.jpg'} 19 | anime episode cover 24 | 28 | {/if} 29 | 38 |
45 |

48 | {episode.animeName} 49 |

50 |

51 | EP {episode.episodeNumber} 52 |

53 |
54 |
55 |
56 | 57 | 62 |
63 |
64 | {:else} 65 |

theres nothing here yet!

66 | {/each} 67 |
68 |
69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sift 2 | ### a decent way to watch anime 3 | - no tracking/analytics 4 | - no ads 5 | - watch history 6 | - sub/dub 7 | image 8 | 9 |
10 | more screenshots 11 | 12 |
Anime info
13 | image 14 |
Episode player
15 | image 16 |
17 | 18 | # Info 19 | - Full stack framework: [SvelteKit](https://kit.svelte.dev/) 20 | - ORM: [Prisma](https://www.prisma.io/) 21 | - Auth library: [Lucia](https://lucia-auth.com/) 22 | - Video player: [Vidstack](https://www.vidstack.io/) 23 | - TypeScript 24 | - TailwindCSS 25 | 26 | # Hosting 27 | Video guide: https://youtu.be/w-v5Pm-gcy4 28 | ### Hosting with discord authentication 29 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fskearya%2Fsift&env=PUBLIC_PROXY,DATABASE_URL,OWNER_ID,CLIENT_ID,CLIENT_SECRET,REDIRECT_URI&envDescription=more%20info%20in%20github%20readme&envLink=https%3A%2F%2Fgithub.com%2Fskearya%2Fsift%23hosting) 30 | ### Hosting with username/password authentication 31 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fskearya%2Fsift&env=PUBLIC_PROXY,DATABASE_URL,OWNER_USERNAME&envDescription=more%20info%20in%20github%20readme&envLink=https%3A%2F%2Fgithub.com%2Fskearya%2Fsift%23hosting) 32 | 33 | `.env` details 34 | ```bash 35 | PUBLIC_PROXY="url to an instance of https://github.com/Eltik/M3U8-Proxy" 36 | DATABASE_URL="mysql db" 37 | 38 | # needed for discord authentication (https://discord.com/developers) 39 | OWNER_ID="discord owner id" 40 | CLIENT_ID="discord app client id" 41 | CLIENT_SECRET="discord app client secret" 42 | REDIRECT_URI="/oauth/discord" 43 | 44 | # if you dont want to use discord auth you can use username+pass auth 45 | OWNER_USERNAME="" 46 | # you have to then create an account with this username 47 | ``` 48 | 49 | `/whitelist` with an admin account to whitelist other users 50 | 51 | ## Acknowledgments 52 | - [Anify](https://github.com/Eltik/Anify) and [Eltik](https://github.com/eltik) 53 | - [shadcn-svelte](https://www.shadcn-svelte.com/) 54 | -------------------------------------------------------------------------------- /src/routes/(anime)/dmca/+page.svelte: -------------------------------------------------------------------------------- 1 |
2 |

DMCA takedown request requirements

3 |

4 | We take the intellectual property rights of others seriously and require that our users do the 5 | same. The Digital Millennium Copyright Act (DMCA) established a process for addressing claims of 6 | copyright infringement. If you own a copyright or have authority to act on behalf of a copyright 7 | owner and want to report a claim that a third party is infringing that material on or through 8 | GitLab's services, please submit a DMCA report via Discord or email and we will take appropriate 9 | action. 10 |

11 |
12 |

DMCA Report Requirements

13 |
    14 |
  • A description of the copyrighted work that you claim is being infringed;
  • 15 |
  • 16 | A description of the material you claim is infringing and that you want removed or access to 17 | which you want disabled with a URL and proof you are the original owner or other location of 18 | that material; 19 |
  • 20 |
  • Your name, title (if acting as an agent), address, telephone number, and email address;
  • 21 |
  • 22 | The following statement: "I have a good faith belief that the use of the copyrighted material I am complaining of is 24 | not authorized by the copyright owner, its agent, or the law (e.g., as a fair use)"; 26 |
  • 27 |
  • 28 | The following statement: "The information in this notice is accurate and, under penalty of perjury, I am the owner, 30 | or authorized to act on behalf of the owner, of the copyright or of an exclusive right that 31 | is allegedly infringed"; 33 |
  • 34 |
  • 35 | The following statement: "I understand that I am subject to legal action upon submitting a DMCA request without 37 | solid proof."; 39 |
  • 40 |
  • 41 | An electronic or physical signature of the owner of the copyright or a person authorized to 42 | act on the owner's behalf. 43 |
  • 44 |
45 |
46 |

Your DMCA take down request should be submit via the following links:

47 | 51 |

52 | We will then review your DMCA request and take proper actions, including removal of the 53 | content from the website. 54 |

55 |
56 |
57 | -------------------------------------------------------------------------------- /src/routes/(anime)/[animeId]/[providerId]/[watchId]/[episode]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import type { Anime, ContentMetadata } from '$lib/types'; 3 | import { api } from '$lib/api'; 4 | import { redirect, error } from '@sveltejs/kit'; 5 | import { prisma } from '$lib/server/prisma'; 6 | 7 | export const load = (async ({ url, params, locals }) => { 8 | const session = await locals.auth.validate(); 9 | 10 | let { animeId, providerId, watchId, episode } = params; 11 | 12 | if (!episode) throw redirect(303, `/search`); 13 | 14 | async function fetchInfo() { 15 | let response: Anime; 16 | 17 | try { 18 | response = await api(`info/${animeId}`).json(); 19 | } catch (e: any) { 20 | throw error(404, { 21 | message: 'Error fetching anime info', 22 | info: e.message 23 | }); 24 | } 25 | 26 | try { 27 | let userData = await prisma.userData.findUnique({ 28 | where: { user_id: session!.user!.userId }, 29 | select: { 30 | id: true, 31 | watchHistory: { 32 | take: 1, 33 | where: { 34 | animeId: animeId, 35 | UserData: { 36 | user_id: session!.user!.userId 37 | } 38 | } 39 | } 40 | } 41 | }); 42 | 43 | let newCover: string | undefined = undefined; 44 | let differentEpisode = userData?.watchHistory[0]?.episodeNumber !== Number(episode); 45 | 46 | if (userData?.watchHistory[0]?.cover == undefined || differentEpisode) { 47 | let response = await api(`content-metadata/${animeId}`).json(); 48 | 49 | newCover = response[0]?.data[Number(episode) - 1]?.img; 50 | } 51 | 52 | await prisma.userData.update({ 53 | where: { 54 | user_id: session!.user.userId 55 | }, 56 | data: { 57 | watchHistory: { 58 | upsert: { 59 | where: { 60 | animeId_userDataId: { 61 | animeId, 62 | userDataId: userData!.id 63 | } 64 | }, 65 | create: { 66 | animeId, 67 | episodeNumber: Number(episode), 68 | animeName: response.title.romaji, 69 | providerId, 70 | watchId, 71 | cover: newCover, 72 | dubbed: url.searchParams.get('subType') == 'dub' 73 | }, 74 | update: { 75 | animeId, 76 | episodeNumber: Number(episode), 77 | animeName: response.title.romaji, 78 | providerId, 79 | watchId, 80 | createdAt: new Date(), 81 | cover: newCover, 82 | dubbed: url.searchParams.get('subType') == 'dub', 83 | progress: differentEpisode ? 0 : undefined 84 | } 85 | } 86 | } 87 | } 88 | }); 89 | } catch {} 90 | 91 | return response; 92 | } 93 | 94 | return { 95 | info: fetchInfo() 96 | }; 97 | }) satisfies PageServerLoad; 98 | -------------------------------------------------------------------------------- /src/routes/(anime)/[animeId]/[providerId]/[watchId]/[episode]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types'; 2 | import type { EpisodeData, SourceInfo } from '$lib/types'; 3 | import { api } from '$lib/api'; 4 | import { error } from '@sveltejs/kit'; 5 | 6 | export const load = (async ({ fetch, data, url, params }) => { 7 | let { animeId, providerId, watchId, episode } = params; 8 | 9 | async function fetchSource() { 10 | try { 11 | let response = await api( 12 | `sources?providerId=${providerId}&watchId=${encodeURIComponent( 13 | watchId 14 | )}&episodeNumber=${episode}&id=${animeId}&subType=${ 15 | url.searchParams.get('subType') || 'sub' 16 | }`, 17 | { fetch } 18 | ).json(); 19 | 20 | if (response.sources.length == 0) throw new Error('No sources found'); 21 | 22 | for (const source of response.sources) { 23 | if (source.quality == 'default' || source.quality == 'auto') { 24 | response.default = source.url; 25 | break; 26 | } 27 | } 28 | 29 | if (response.default == undefined) throw new Error('No playable sources found'); 30 | 31 | return response; 32 | } catch (e: any) { 33 | throw error(404, { 34 | message: 'Error fetching episode sources', 35 | info: e.message 36 | }); 37 | } 38 | } 39 | 40 | async function fetchEpisodes() { 41 | try { 42 | let response = await api(`episodes/${animeId}`, { fetch }).json(); 43 | 44 | for (let i = 0; i < response.length; i++) { 45 | let firstItem = response[i].episodes[0].number; 46 | let lastItem = response[i].episodes[response[i].episodes.length - 1].number; 47 | 48 | if (firstItem > lastItem) { 49 | response[i].episodes.reverse(); 50 | } 51 | } 52 | 53 | const providerEpisodes = response.find((provider) => provider.providerId == providerId); 54 | 55 | if (providerEpisodes == undefined) { 56 | throw Error( 57 | `Episode could not be found on ${providerId} anymore, please try another provider` 58 | ); 59 | } 60 | 61 | if (url.searchParams.get('subType') == 'dub') { 62 | let dubbed: EpisodeData[] = []; 63 | 64 | response.forEach((provider) => { 65 | if (provider.episodes.filter((episode) => episode.hasDub).length == 0) return; 66 | 67 | dubbed[dubbed.length] = { 68 | providerId: provider.providerId, 69 | episodes: provider.episodes.filter((episode) => episode.hasDub) 70 | }; 71 | }); 72 | 73 | return dubbed; 74 | } 75 | 76 | return response; 77 | } catch (e: any) { 78 | throw error(404, { 79 | message: 'Error fetching episode info', 80 | info: e.message 81 | }); 82 | } 83 | } 84 | 85 | return { 86 | ...data, 87 | source: fetchSource(), 88 | episodes: fetchEpisodes(), 89 | time: url.searchParams.get('time'), 90 | dubbed: url.searchParams.get('subType') == 'dub' 91 | }; 92 | }) satisfies PageLoad; 93 | -------------------------------------------------------------------------------- /src/routes/(anime)/search/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |

Results

20 | 21 |
24 | {#await data.streamed.response} 25 | {#each Array(10) as _} 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 |
43 | {/each} 44 | {:then results} 45 | {#each results as anime} 46 | 51 | {:else} 52 | 53 | 54 | No results found 55 | 56 | Please try another search. 57 | 58 | {/each} 59 | {:catch e} 60 | 61 | Error 62 | {e.message} 63 | 64 | {/await} 65 |
66 | 67 |
68 | {#if (Number($page.url.searchParams.get('page')) || 1) !== 1} 69 | 76 | {/if} 77 | 86 |
87 |
88 | -------------------------------------------------------------------------------- /src/lib/components/light-switch/light-switch.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/skeletonlabs/skeleton/blob/master/packages/skeleton/src/lib/utilities/LightSwitch/lightswitch.ts 2 | 3 | // Lightswitch Service 4 | 5 | import { get } from 'svelte/store'; 6 | // DO NOT replace this ⬇ import, it has to be imported directly 7 | import { localStorageStore } from './local-storage-store'; 8 | 9 | // Stores --- 10 | // TRUE: light, FALSE: dark 11 | 12 | /** Store: OS Preference Mode */ 13 | export const modeOsPrefers = localStorageStore('modeOsPrefers', false); 14 | /** Store: User Preference Mode */ 15 | export const modeUserPrefers = localStorageStore('modeUserPrefers', undefined); 16 | /** Store: Current Mode State */ 17 | export const modeCurrent = localStorageStore('modeCurrent', false); 18 | 19 | // Get --- 20 | 21 | /** Get the OS Preference for light/dark mode */ 22 | export function getModeOsPrefers(): boolean { 23 | const prefersLightMode = window.matchMedia('(prefers-color-scheme: light)').matches; 24 | modeOsPrefers.set(prefersLightMode); 25 | return prefersLightMode; 26 | } 27 | 28 | /** Get the User for light/dark mode */ 29 | export function getModeUserPrefers(): boolean | undefined { 30 | return get(modeUserPrefers); 31 | } 32 | 33 | /** Get the Automatic Preference light/dark mode */ 34 | export function getModeAutoPrefers(): boolean { 35 | const os = getModeOsPrefers(); 36 | const user = getModeUserPrefers(); 37 | const modeValue = user !== undefined ? user : os; 38 | return modeValue; 39 | } 40 | 41 | // Set --- 42 | 43 | /** Set the User Preference for light/dark mode */ 44 | export function setModeUserPrefers(value: boolean): void { 45 | modeUserPrefers.set(value); 46 | } 47 | 48 | /** Set the the current light/dark mode */ 49 | export function setModeCurrent(value: boolean) { 50 | const elemHtmlClasses = document.documentElement.classList; 51 | const classDark = `dark`; 52 | value === true ? elemHtmlClasses.remove(classDark) : elemHtmlClasses.add(classDark); 53 | modeCurrent.set(value); 54 | } 55 | 56 | // Lightswitch Utility 57 | 58 | /** Set the visible light/dark mode on page load. */ 59 | export function setInitialClassState() { 60 | const elemHtmlClasses = document.documentElement.classList; 61 | // Conditions 62 | const condLocalStorageUserPrefs = localStorage.getItem('modeUserPrefers') === 'false'; 63 | const condLocalStorageUserPrefsExists = !('modeUserPrefers' in localStorage); 64 | const condMatchMedia = window.matchMedia('(prefers-color-scheme: dark)').matches; 65 | // Add/remove `.dark` class to HTML element 66 | if (condLocalStorageUserPrefs || (condLocalStorageUserPrefsExists && condMatchMedia)) { 67 | elemHtmlClasses.add('dark'); 68 | } else { 69 | elemHtmlClasses.remove('dark'); 70 | } 71 | } 72 | 73 | // Auto-Switch Utility 74 | 75 | /** Automatically set the visible light/dark, updates on change. */ 76 | export function autoModeWatcher(): void { 77 | const mql = window.matchMedia('(prefers-color-scheme: light)'); 78 | function setMode(value: boolean) { 79 | const elemHtmlClasses = document.documentElement.classList; 80 | const classDark = `dark`; 81 | value === true ? elemHtmlClasses.remove(classDark) : elemHtmlClasses.add(classDark); 82 | } 83 | setMode(mql.matches); 84 | mql.onchange = () => { 85 | setMode(mql.matches); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Anime { 2 | id: string; 3 | malId: string; 4 | slug: string; 5 | kitsuId: string; 6 | tvdbId?: string; 7 | coverImage: string; 8 | bannerImage: string; 9 | trailer: string; 10 | status: string; 11 | season: string; 12 | title: Title; 13 | currentEpisode?: number; 14 | mappings: Mapping[]; 15 | synonyms: string[]; 16 | countryOfOrigin?: string; 17 | description: string; 18 | duration: number; 19 | color?: string; 20 | year?: number; 21 | rating: Rating; 22 | popularity: Rating; 23 | type: string; 24 | format: string; 25 | relations: Relation[]; 26 | totalEpisodes: number; 27 | genres: string[]; 28 | tags: string[]; 29 | episodes: Episodes; 30 | averageRating?: number; 31 | averagePopularity?: number; 32 | artwork: Artwork[]; 33 | } 34 | 35 | interface Artwork { 36 | img: string; 37 | type: string; 38 | providerId: string; 39 | } 40 | 41 | interface Episodes { 42 | data: Datum[]; 43 | updatedAt: number | string; 44 | } 45 | 46 | interface Datum { 47 | episodes: Episode[]; 48 | providerId: string; 49 | } 50 | 51 | interface Episode { 52 | id: string; 53 | img?: string; 54 | title: string; 55 | hasDub: boolean; 56 | number: number; 57 | isFiller: boolean; 58 | } 59 | 60 | interface Relation { 61 | id: number; 62 | data: Data; 63 | type: string; 64 | } 65 | 66 | interface Data { 67 | id: number; 68 | type: string; 69 | title: Title; 70 | format: string; 71 | status: string; 72 | coverImage: CoverImage; 73 | bannerImage?: string; 74 | } 75 | 76 | interface CoverImage { 77 | large: string; 78 | } 79 | 80 | interface Rating { 81 | mal: number; 82 | kitsu: number; 83 | anilist: number; 84 | } 85 | 86 | interface Mapping { 87 | id: string; 88 | providerId: string; 89 | similarity: number; 90 | } 91 | 92 | export interface Title { 93 | native: string; 94 | romaji: string; 95 | english: string; 96 | } 97 | 98 | export interface EpisodeData { 99 | providerId: string; 100 | episodes: Episode[]; 101 | } 102 | 103 | export interface SourceInfo { 104 | sources: Source[]; 105 | default: string; 106 | subtitles: any[]; 107 | intro: Intro; 108 | outro: Intro; 109 | headers: Headers; 110 | } 111 | 112 | export interface Headers { 113 | Referer?: string; 114 | } 115 | 116 | interface Intro { 117 | end: number; 118 | start: number; 119 | } 120 | 121 | interface Source { 122 | url: string; 123 | quality: string; 124 | } 125 | 126 | export interface ContentMetadata { 127 | providerId: string; 128 | data: Datum[]; 129 | } 130 | 131 | interface Datum { 132 | id: string; 133 | description?: string; 134 | hasDub: boolean; 135 | img?: undefined | string; 136 | isFiller: boolean; 137 | number: number; 138 | title: string; 139 | updatedAt: number; 140 | rating?: number; 141 | } 142 | 143 | export interface SeasonalData { 144 | trending: Anime[]; 145 | popular: Anime[]; 146 | top: Anime[]; 147 | seasonal: Anime[]; 148 | [x: string]: Anime[]; 149 | } 150 | 151 | export interface MinifiedSeasonalData { 152 | trending: MinifiedAnime[]; 153 | popular: MinifiedAnime[]; 154 | top: MinifiedAnime[]; 155 | seasonal: MinifiedAnime[]; 156 | [x: string]: MinifiedAnime[]; 157 | } 158 | 159 | export type MinifiedAnime = Pick & { 160 | fallback?: string; 161 | }; 162 | -------------------------------------------------------------------------------- /src/lib/components/player/Player.svelte: -------------------------------------------------------------------------------- 1 | 114 | 115 | 127 | 128 | {#each subtitles as subtitle} 129 | 136 | {/each} 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/lib/components/episodes/Episodes.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 | {#if $toastState} 38 |
44 |
53 |

Episode Selector

54 | 55 | {#await fetchEpisodes()} 56 |
57 | 58 | 59 | {#await sleep(3000) then _} 60 |

61 | this can take a while.. 62 |

63 | {/await} 64 |
65 | {:then data} 66 | {#if data?.message} 67 |

{data.message}

68 | {:else} 69 |
70 | 71 | 72 | {#each data as provider} 73 | {provider.providerId} 74 | {:else} 75 |
76 |

No providers found

77 |
78 | {/each} 79 |
80 | 81 | {#each data as provider} 82 | 83 |
84 | {#each provider.episodes as episode} 85 | 91 | {episode.number} 92 | 93 | {:else} 94 |
95 |

No episodes found

96 |
97 | {/each} 98 |
99 |
100 | {/each} 101 |
102 |
103 | {/if} 104 | {:catch} 105 |

An error occurred

106 | {/await} 107 | 108 | 116 |
117 | {/if} 118 |
119 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 | {#if data.history?.length > 0} 38 |

39 | Continue Watching 40 |

41 | 42 | 77 | {/if} 78 | 79 | {#await data.streamed.anime} 80 | {#each ['recent', 'trending', 'popular', 'top', 'seasonal'] as collection} 81 |

84 | {collection} 85 |

86 | 87 |
88 | {#each Array(6) as _} 89 | 90 | {/each} 91 |
92 | {/each} 93 | {:then anime} 94 | {#each Object.entries(anime) as [collection, animeCollection]} 95 |
96 |

99 | {collection} 100 |

101 | 102 |
106 | {#each animeCollection as anime, i} 107 | 108 | {/each} 109 |
110 | 111 | 117 | 118 | 124 |
125 | {/each} 126 | {/await} 127 | 128 |
129 |
130 | -------------------------------------------------------------------------------- /src/routes/(anime)/[animeId]/[providerId]/[watchId]/[episode]/+page.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
41 | {#await import('$components/player')} 42 |
43 | {:then { Player }} 44 | 52 | {/await} 53 | 54 |
55 |
56 |

57 | {data.info.title[nameType]} 58 |

59 |

60 | {data.info.season} 61 | {data.info.year} 62 |

63 |
64 | 65 |
66 | 67 | 68 | {#if nextEp} 69 | 77 | {/if} 78 |
79 |
80 | 81 |
82 |
83 | {#each data.episodes as provider} 84 | {@const episode = provider.episodes.find( 85 | (episode) => episode.number == Number($page.params.episode) 86 | )} 87 | {#if episode?.id && episode?.number} 88 | 98 |

{provider.providerId}

99 |
100 | {/if} 101 | {/each} 102 |
103 | 104 |
105 | {#each currentProvider.episodes as episode} 106 | 117 | {episode.number} 118 | 119 | {/each} 120 |
121 |
122 | 123 |
124 |
125 |
126 | anime cover { 132 | // @ts-ignore 133 | if (event.target.src !== bestFallback(data.info.artwork)) { 134 | // @ts-ignore 135 | event.target.src = bestFallback(data.info.artwork); 136 | } 137 | }} 138 | /> 139 | {#if data.info.trailer !== '' && data.info.trailer !== 'https://youtube.com/watch?v=undefined'} 140 | 147 | {/if} 148 |
149 | 157 | {#if kitsuId} 158 | 166 | {/if} 167 | {#if simklId} 168 | 176 | {/if} 177 |
178 |
179 | 180 |
{@html data.info.description}
181 |
182 |
183 |
184 | 185 | 191 | -------------------------------------------------------------------------------- /src/routes/(anime)/[animeId]/+page.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 |
45 |
46 |
49 | anime cover 55 | {#if data.info.trailer !== '' && data.info.trailer !== 'https://youtube.com/watch?v=undefined'} 56 | 63 | {/if} 64 |
65 | 73 | {#if kitsuId} 74 | 82 | {/if} 83 | {#if simklId} 84 | 92 | {/if} 93 |
94 |
95 |
96 |

{data.info.title[nameType]}

97 | {#if data.info?.year} 98 |

99 | {data.info.season == 'UNKNOWN' ? '' : data.info.season} 100 | {data.info.year} 101 |

102 | {/if} 103 | {#if data.info?.genres} 104 |
105 | {#each data.info.genres || [] as genre} 106 | {genre} 107 | {/each} 108 |
109 | {/if} 110 |
{@html data.info.description}
111 |
112 |
113 | 114 |
115 |
116 |

Episodes

117 |
118 | 119 | 120 |
121 |
122 | 123 | 124 | 129 | {#each checked ? dubbed : data.episodes as provider} 130 | {provider.providerId} 131 | {:else} 132 |
133 |

No providers found

134 |
135 | {/each} 136 |
137 | 138 | {#each checked ? dubbed : data.episodes as provider} 139 | 140 |
141 | {#each provider.episodes as episode} 142 | 148 |
151 |

{episode.number}

152 |
153 |

154 | {episode.title || episode.id} 155 |

156 | {#if validCover(data.covers[0]?.data[episode.number - 1]?.img)} 157 | 166 | {/if} 167 |
168 | {:else} 169 | 170 | 171 | No episodes found 172 | 173 | 174 | {/each} 175 |
176 |
177 | {/each} 178 |
179 | 180 | 183 |
184 |
185 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 77 | 78 | 79 | {@html ``} 80 | sift 81 | 82 | 83 | {#if $navigating} 84 |
88 | 89 | 90 | {#await sleep(3000) then _} 91 |

this can take a while..

92 | {/await} 93 |
94 | {/if} 95 | 96 | 221 | 222 | {#key data.url} 223 |
228 | 229 |
230 | {/key} 231 | 232 | 257 | 258 | 259 | 260 | 266 | 267 | 283 | --------------------------------------------------------------------------------