Remove sveltekit dependency, create simple test project
@@ -1,6 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
build
|
build
|
||||||
|
dist
|
||||||
.idea
|
.idea
|
||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
PUBLIC_RADARR_API_KEY=yourapikeyhere
|
|
||||||
PUBLIC_RADARR_BASE_URL=http://127.0.0.1:7878
|
|
||||||
|
|
||||||
PUBLIC_SONARR_API_KEY=yourapikeyhere
|
|
||||||
PUBLIC_SONARR_BASE_URL=http://127.0.0.1:8989
|
|
||||||
|
|
||||||
PUBLIC_JELLYFIN_API_KEY=yourapikeyhere
|
|
||||||
PUBLIC_JELLYFIN_BASE_URL=http://127.0.0.1:8096
|
|
||||||
@@ -6,8 +6,32 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
.vercel
|
||||||
|
.output
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
/config/*.sqlite
|
||||||
|
/dist
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Logs
|
||||||
pnpm-lock.yaml
|
logs
|
||||||
package-lock.json
|
*.log
|
||||||
yarn.lock
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|||||||
25
.gitignore
vendored
@@ -11,4 +11,27 @@ node_modules
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
/config/*.sqlite
|
/config/*.sqlite
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
/.svelte-kit
|
/.svelte-kit
|
||||||
/package
|
/package
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"appId": "me.aleksilassila.reiverr",
|
|
||||||
"productName": "Reiverr",
|
|
||||||
"directories": {
|
|
||||||
"output": "dist"
|
|
||||||
},
|
|
||||||
"mac": {
|
|
||||||
"asar": false
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"asar": false,
|
|
||||||
"target": "msi"
|
|
||||||
},
|
|
||||||
"asar": false,
|
|
||||||
"files": [
|
|
||||||
"src/electron.cjs",
|
|
||||||
{
|
|
||||||
"from": "build",
|
|
||||||
"to": "build"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
33
index.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="./public/favicon.png" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta
|
||||||
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
|
content="black-translucent"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||||
|
<title>Vite + Svelte + TS</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Nunito+Sans:wght@200;300;400;500;600;700;800;900;1000&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Montserrat:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body class="bg-stone-950 min-h-screen text-white touch-manipulation relative -z-10">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7658
package-lock.json
generated
81
package.json
@@ -1,67 +1,58 @@
|
|||||||
{
|
{
|
||||||
"name": "reiverr",
|
"name": "reiverr",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/aleksilassila/reiverr"
|
"url": "https://github.com/aleksilassila/reiverr"
|
||||||
},
|
},
|
||||||
"main": "src/electron.cjs",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite --open",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"deploy": "PORT=9494 NODE_ENV=production node build/",
|
"deploy": "PORT=9494 NODE_ENV=production node build/",
|
||||||
"deploy:electron": "vite build && electron-builder -mw --x64 --config build.config.json; electron-builder -m --arm64 --config build.config.json",
|
"deploy:electron": "vite build && electron-builder -mw --x64 --config build.config.json; electron-builder -m --arm64 --config build.config.json",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
"format": "prettier --plugin-search-dir . --write ."
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/fira-mono": "^4.5.10",
|
|
||||||
"@neoconfetti/svelte": "^1.0.0",
|
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"vitest": "^0.25.3",
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
|
||||||
"@sveltejs/adapter-static": "2.0.3",
|
|
||||||
"@sveltejs/kit": "^1.5.0",
|
|
||||||
"@types/axios": "^0.14.0",
|
|
||||||
"@types/cookie": "^0.5.1",
|
|
||||||
"@types/node": "^20.3.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"classnames": "^2.3.2",
|
|
||||||
"electron": "^26.1.0",
|
|
||||||
"electron-builder": "^24.6.3",
|
|
||||||
"eslint": "^8.28.0",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-svelte": "^2.26.0",
|
|
||||||
"openapi-typescript": "^6.2.7",
|
|
||||||
"postcss": "^8.4.24",
|
|
||||||
"prettier": "^2.8.0",
|
|
||||||
"prettier-plugin-svelte": "^2.8.1",
|
|
||||||
"svelte": "^4.1.0",
|
|
||||||
"svelte-check": "^3.0.1",
|
|
||||||
"tailwindcss": "^3.3.2",
|
|
||||||
"tslib": "^2.4.1",
|
|
||||||
"typescript": "^5.1.3",
|
|
||||||
"vite": "^4.3.0",
|
|
||||||
"vitest": "^0.25.3"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@jellyfin/sdk": "^0.7.0",
|
|
||||||
"axios": "^1.4.0",
|
|
||||||
"hls.js": "^1.4.6",
|
|
||||||
"openapi-fetch": "^0.2.1",
|
|
||||||
"radix-icons-svelte": "^1.2.1",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sqlite3": "^5.1.6",
|
"@jellyfin/sdk": "^0.8.2",
|
||||||
"svelte-i18n": "^3.7.0",
|
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||||
|
"@tsconfig/svelte": "^5.0.2",
|
||||||
|
"@types/axios": "^0.14.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
|
"@typescript-eslint/parser": "^6.16.0",
|
||||||
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"classnames": "^2.4.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^8.10.0",
|
||||||
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
|
"hls.js": "^1.4.14",
|
||||||
|
"openapi-fetch": "^0.8.2",
|
||||||
|
"openapi-typescript": "^6.7.3",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
|
"radix-icons-svelte": "^1.2.1",
|
||||||
|
"svelte": "^3.59.1",
|
||||||
|
"svelte-check": "^3.6.2",
|
||||||
|
"svelte-i18n": "^4.0.0",
|
||||||
|
"svelte-navigator": "^3.2.2",
|
||||||
"tailwind-scrollbar-hide": "^1.1.7",
|
"tailwind-scrollbar-hide": "^1.1.7",
|
||||||
"typeorm": "^0.3.17"
|
"tailwindcss": "^3.4.0",
|
||||||
|
"terser": "^5.26.0",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^4.5.1",
|
||||||
|
"vite-plugin-singlefile": "^0.13.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 846 B After Width: | Height: | Size: 846 B |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 684 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
56
src/App.svelte
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Carousel from "./lib/components/Carousel/Carousel.svelte";
|
||||||
|
import I18n from "./lib/components/Lang/I18n.svelte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import CarouselPlaceholderItems from "./lib/components/Carousel/CarouselPlaceholderItems.svelte";
|
||||||
|
import { Link, navigate, Route, Router } from "svelte-navigator";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
import { networks } from "./lib/discover";
|
||||||
|
import NetworkCard from "./lib/components/NetworkCard.svelte";
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "ArrowRight") {
|
||||||
|
console.log("right");
|
||||||
|
navigate("/about");
|
||||||
|
}
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
console.log("left");
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<I18n />
|
||||||
|
<main class="bg-stone-950 text-white">
|
||||||
|
<Router>
|
||||||
|
<nav>
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
<Link to="about">About</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Carousel>
|
||||||
|
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||||
|
{$_("discover.upcomingSeries")}
|
||||||
|
</div>
|
||||||
|
<CarouselPlaceholderItems />
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
<Carousel>
|
||||||
|
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||||
|
{$_("discover.TVNetworks")}
|
||||||
|
</div>
|
||||||
|
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
||||||
|
<NetworkCard {network} />
|
||||||
|
{/each}
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
<Route path="/">
|
||||||
|
<div transition:fade|global>Home path</div>
|
||||||
|
</Route>
|
||||||
|
<Route path="about">
|
||||||
|
<div transition:fade|global>about path</div>
|
||||||
|
</Route>
|
||||||
|
</Router>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
31
src/app.html
@@ -1,31 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" style="background-color: black">
|
|
||||||
<head>
|
|
||||||
<title>Reiverr</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Nunito+Sans:wght@200;300;400;500;600;700;800;900;1000&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Montserrat:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
%sveltekit.head%
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
data-sveltekit-preload-data="hover"
|
|
||||||
class="bg-stone-950 min-h-screen text-white touch-manipulation relative -z-10"
|
|
||||||
>
|
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
process.env.PORT = '9494';
|
|
||||||
const { app, BrowserWindow } = require('electron');
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await import('../build/index.js');
|
|
||||||
// const serveURL = serve({ directory: '.' });
|
|
||||||
const port = process.env.PORT || 5173;
|
|
||||||
let mainWindow;
|
|
||||||
|
|
||||||
const createWindow = () => {
|
|
||||||
if (!mainWindow) {
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow.once('close', () => {
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
loadSite(port);
|
|
||||||
|
|
||||||
return mainWindow;
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadSite(port) {
|
|
||||||
mainWindow.loadURL(`http://localhost:${port}`).catch((e) => {
|
|
||||||
console.log('Error loading URL, retrying', e);
|
|
||||||
setTimeout(() => {
|
|
||||||
loadSite(port);
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
createWindow();
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (!mainWindow) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') app.quit();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import TypeOrm from '$lib/db';
|
|
||||||
import 'reflect-metadata';
|
|
||||||
|
|
||||||
await TypeOrm.getDb();
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { settings } from '$lib/stores/settings.store';
|
|
||||||
import { addMessages, init, locale } from 'svelte-i18n';
|
import { addMessages, init, locale } from 'svelte-i18n';
|
||||||
|
|
||||||
import de from '../../lang/de.json';
|
import de from '../../lang/de.json';
|
||||||
@@ -7,6 +6,7 @@
|
|||||||
import es from '../../lang/es.json';
|
import es from '../../lang/es.json';
|
||||||
import fr from '../../lang/fr.json';
|
import fr from '../../lang/fr.json';
|
||||||
import it from '../../lang/it.json';
|
import it from '../../lang/it.json';
|
||||||
|
import { settings } from '../../stores/settings.store';
|
||||||
|
|
||||||
addMessages('de', de);
|
addMessages('de', de);
|
||||||
addMessages('en', en);
|
addMessages('en', en);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TitleId } from '$lib/types';
|
import type { TitleId } from '$lib/types';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import MoviePage from '../../../routes/movie/[id]/MoviePage.svelte';
|
import MoviePage from '../MoviePage.svelte';
|
||||||
import SeriesPage from '../../../routes/series/[id]/SeriesPage.svelte';
|
import SeriesPage from '../SeriesPage.svelte';
|
||||||
import { modalStack } from '../../stores/modal.store';
|
import { modalStack } from '../../stores/modal.store';
|
||||||
import PersonPage from '../../../routes/person/[id]/PersonPage.svelte';
|
import PersonPage from '../PersonPage.svelte';
|
||||||
|
|
||||||
export let titleId: TitleId;
|
export let titleId: TitleId;
|
||||||
export let modalId: symbol;
|
export let modalId: symbol;
|
||||||
|
|||||||
8
src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import './app.css'
|
||||||
|
import App from './App.svelte'
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.getElementById('app'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import I18n from '$lib/components/Lang/I18n.svelte';
|
|
||||||
import DynamicModal from '$lib/components/Modal/DynamicModal.svelte';
|
|
||||||
import Navbar from '$lib/components/Navbar/Navbar.svelte';
|
|
||||||
import UpdateChecker from '$lib/components/UpdateChecker.svelte';
|
|
||||||
import { type SettingsValues, defaultSettings, settings } from '$lib/stores/settings.store';
|
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import '../app.css';
|
|
||||||
import Notifications from '$lib/components/Notification/Notifications.svelte';
|
|
||||||
|
|
||||||
// export let data: LayoutServerData;
|
|
||||||
// settings.set(data.settings);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- {#if data.isApplicationSetUp} -->
|
|
||||||
<I18n />
|
|
||||||
<div class="app">
|
|
||||||
<Navbar />
|
|
||||||
<main>
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
{#key $page.url.pathname}
|
|
||||||
<DynamicModal />
|
|
||||||
{/key}
|
|
||||||
<Notifications />
|
|
||||||
<UpdateChecker />
|
|
||||||
</div>
|
|
||||||
<!-- {:else} -->
|
|
||||||
<!-- <SetupRequired missingEnvironmentVariables={data.missingEnvironmentVariables} /> -->
|
|
||||||
<!-- {/if} -->
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// import { dev } from '$app/environment';
|
|
||||||
|
|
||||||
// Disable SSR when running the dev server
|
|
||||||
// This is a fix to vite dev server freezing on mac :(
|
|
||||||
// https://github.com/vitejs/vite/issues/11468
|
|
||||||
// export const ssr = !dev;
|
|
||||||
export const ssr = false;
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
getJellyfinBackdrop,
|
|
||||||
getJellyfinContinueWatching,
|
|
||||||
getJellyfinNextUp,
|
|
||||||
type JellyfinItem
|
|
||||||
} from '$lib/apis/jellyfin/jellyfinApi';
|
|
||||||
import {
|
|
||||||
getPosterProps,
|
|
||||||
getTmdbMovie,
|
|
||||||
getTmdbPopularMovies,
|
|
||||||
TmdbApiOpen
|
|
||||||
} from '$lib/apis/tmdb/tmdbApi';
|
|
||||||
import Carousel from '$lib/components/Carousel/Carousel.svelte';
|
|
||||||
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
|
|
||||||
import GenreCard from '$lib/components/GenreCard.svelte';
|
|
||||||
import NetworkCard from '$lib/components/NetworkCard.svelte';
|
|
||||||
import PersonCard from '$lib/components/PersonCard/PersonCard.svelte';
|
|
||||||
import Poster from '$lib/components/Poster/Poster.svelte';
|
|
||||||
import TitleShowcases from '$lib/components/TitleShowcase/TitleShowcasesContainer.svelte';
|
|
||||||
import { genres, networks } from '$lib/discover';
|
|
||||||
import { jellyfinItemsStore } from '$lib/stores/data.store';
|
|
||||||
import { settings } from '$lib/stores/settings.store';
|
|
||||||
import type { TitleType } from '$lib/types';
|
|
||||||
import { formatDateToYearMonthDay } from '$lib/utils';
|
|
||||||
import type { ComponentProps } from 'svelte';
|
|
||||||
import { _ } from 'svelte-i18n';
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
|
|
||||||
let continueWatchingVisible = true;
|
|
||||||
|
|
||||||
const tmdbPopularMoviesPromise = getTmdbPopularMovies()
|
|
||||||
.then((movies) => Promise.all(movies.map((movie) => getTmdbMovie(movie.id || 0))))
|
|
||||||
.then((movies) => movies.filter((m) => !!m).slice(0, 10));
|
|
||||||
|
|
||||||
let nextUpP = getJellyfinNextUp();
|
|
||||||
let continueWatchingP = getJellyfinContinueWatching();
|
|
||||||
|
|
||||||
let nextUpProps = Promise.all([nextUpP, continueWatchingP])
|
|
||||||
.then(([nextUp, continueWatching]) => [
|
|
||||||
...(continueWatching || []),
|
|
||||||
...(nextUp?.filter((i) => !continueWatching?.find((c) => c.SeriesId === i.SeriesId)) || [])
|
|
||||||
])
|
|
||||||
.then((items) =>
|
|
||||||
Promise.all(
|
|
||||||
items?.map(async (item) => {
|
|
||||||
const parentSeries = await jellyfinItemsStore.promise.then((items) =>
|
|
||||||
items.find((i) => i.Id === item.SeriesId)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
tmdbId: Number(item.ProviderIds?.Tmdb) || Number(parentSeries?.ProviderIds?.Tmdb) || 0,
|
|
||||||
jellyfinId: item.Id,
|
|
||||||
backdropUrl: getJellyfinBackdrop(item),
|
|
||||||
title: item.Name || '',
|
|
||||||
progress: item.UserData?.PlayedPercentage || undefined,
|
|
||||||
runtime: item.RunTimeTicks ? item.RunTimeTicks / 10_000_000 / 60 : 0,
|
|
||||||
...(item.Type === 'Movie'
|
|
||||||
? {
|
|
||||||
type: 'movie',
|
|
||||||
subtitle: item.Genres?.join(', ') || ''
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: 'series',
|
|
||||||
subtitle:
|
|
||||||
(item?.IndexNumber && 'Episode ' + item.IndexNumber) ||
|
|
||||||
item.Genres?.join(', ') ||
|
|
||||||
''
|
|
||||||
})
|
|
||||||
} as const;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
nextUpProps.then((props) => {
|
|
||||||
if (props.length === 0) {
|
|
||||||
continueWatchingVisible = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let showcaseIndex = 0;
|
|
||||||
|
|
||||||
async function onNext() {
|
|
||||||
showcaseIndex = (showcaseIndex + 1) % (await tmdbPopularMoviesPromise).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onPrevious() {
|
|
||||||
showcaseIndex =
|
|
||||||
(showcaseIndex - 1 + (await tmdbPopularMoviesPromise).length) %
|
|
||||||
(await tmdbPopularMoviesPromise).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jellyfinItemsPromise = new Promise<JellyfinItem[]>((resolve) => {
|
|
||||||
jellyfinItemsStore.subscribe((data) => {
|
|
||||||
if (data.loading) return;
|
|
||||||
resolve(data.data || []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchCardProps = async (
|
|
||||||
items: {
|
|
||||||
name?: string;
|
|
||||||
title?: string;
|
|
||||||
id?: number;
|
|
||||||
vote_average?: number;
|
|
||||||
number_of_seasons?: number;
|
|
||||||
first_air_date?: string;
|
|
||||||
poster_path?: string;
|
|
||||||
}[],
|
|
||||||
type: TitleType | undefined = undefined
|
|
||||||
): Promise<ComponentProps<Poster>[]> => {
|
|
||||||
const filtered = $settings.discover.excludeLibraryItems
|
|
||||||
? items.filter(
|
|
||||||
async (item) =>
|
|
||||||
!(await jellyfinItemsPromise).find((i) => i.ProviderIds?.Tmdb === String(item.id))
|
|
||||||
)
|
|
||||||
: items;
|
|
||||||
|
|
||||||
return Promise.all(filtered.map(async (item) => getPosterProps(item, type))).then((props) =>
|
|
||||||
props.filter((p) => p.backdropUrl)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const trendingItemsPromise = TmdbApiOpen.get('/3/trending/all/{time_window}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
time_window: 'day'
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
language: $settings.language
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then((res) => res.data?.results || []);
|
|
||||||
|
|
||||||
const fetchTrendingProps = () => trendingItemsPromise.then(fetchCardProps);
|
|
||||||
|
|
||||||
const fetchTrendingActorProps = () =>
|
|
||||||
TmdbApiOpen.get('/3/trending/person/{time_window}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
time_window: 'week'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then((actors) =>
|
|
||||||
actors
|
|
||||||
.filter((a) => a.profile_path)
|
|
||||||
.map((actor) => ({
|
|
||||||
tmdbId: actor.id || 0,
|
|
||||||
backdropUri: actor.profile_path || '',
|
|
||||||
name: actor.name || '',
|
|
||||||
subtitle: actor.known_for_department || ''
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchUpcomingMovies = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/movie', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
language: $settings.language,
|
|
||||||
region: $settings.discover.region,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then(fetchCardProps);
|
|
||||||
|
|
||||||
const fetchUpcomingSeries = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/tv', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
'first_air_date.gte': formatDateToYearMonthDay(new Date()),
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
language: $settings.language,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then((i) => fetchCardProps(i, 'series'));
|
|
||||||
|
|
||||||
const fetchDigitalReleases = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/movie', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
with_release_type: 4,
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
'release_date.lte': formatDateToYearMonthDay(new Date()),
|
|
||||||
language: $settings.language,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
// region: $settings.discover.region
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then(fetchCardProps);
|
|
||||||
|
|
||||||
const fetchNowStreaming = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/tv', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
'air_date.gte': formatDateToYearMonthDay(new Date()),
|
|
||||||
'first_air_date.lte': formatDateToYearMonthDay(new Date()),
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
language: $settings.language,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then((i) => fetchCardProps(i, 'series'));
|
|
||||||
|
|
||||||
function parseIncludedLanguages(includedLanguages: string) {
|
|
||||||
return includedLanguages.replace(' ', '').split(',').join('|');
|
|
||||||
}
|
|
||||||
|
|
||||||
const PADDING = 'px-4 lg:px-8 2xl:px-16';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TitleShowcases />
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-12 py-6 bg-stone-950"
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<Carousel scrollClass={PADDING}>
|
|
||||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
|
||||||
{$_('discover.popularPeople')}
|
|
||||||
</div>
|
|
||||||
{#await fetchTrendingActorProps()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<PersonCard {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel scrollClass={PADDING}>
|
|
||||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
|
||||||
{$_('discover.upcomingMovies')}
|
|
||||||
</div>
|
|
||||||
{#await fetchUpcomingMovies()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel scrollClass={PADDING}>
|
|
||||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
|
||||||
{$_('discover.upcomingSeries')}
|
|
||||||
</div>
|
|
||||||
{#await fetchUpcomingSeries()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel scrollClass={PADDING}>
|
|
||||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
|
||||||
{$_('discover.genres')}
|
|
||||||
</div>
|
|
||||||
{#each Object.values(genres) as genre (genre.tmdbGenreId)}
|
|
||||||
<GenreCard {genre} />
|
|
||||||
{/each}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel scrollClass={PADDING}>
|
|
||||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
|
||||||
{$_('discover.newDigitalReleases')}
|
|
||||||
</div>
|
|
||||||
{#await fetchDigitalReleases()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel scrollClass={PADDING}>
|
|
||||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
|
||||||
{$_('discover.streamingNow')}
|
|
||||||
</div>
|
|
||||||
{#await fetchNowStreaming()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel scrollClass={PADDING}>
|
|
||||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
|
||||||
{$_('discover.TVNetworks')}
|
|
||||||
</div>
|
|
||||||
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
|
||||||
<NetworkCard {network} />
|
|
||||||
{/each}
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Settings } from '$lib/entities/Settings';
|
|
||||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ url }) => {
|
|
||||||
return json(await Settings.get());
|
|
||||||
};
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
|
||||||
const values = await request.json();
|
|
||||||
return json(await Settings.set('default', values));
|
|
||||||
};
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { JellyfinItem } from '$lib/apis/jellyfin/jellyfinApi';
|
|
||||||
import { TmdbApiOpen, getPosterProps } from '$lib/apis/tmdb/tmdbApi';
|
|
||||||
import Carousel from '$lib/components/Carousel/Carousel.svelte';
|
|
||||||
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
|
|
||||||
import GenreCard from '$lib/components/GenreCard.svelte';
|
|
||||||
import NetworkCard from '$lib/components/NetworkCard.svelte';
|
|
||||||
import PersonCard from '$lib/components/PersonCard/PersonCard.svelte';
|
|
||||||
import Poster from '$lib/components/Poster/Poster.svelte';
|
|
||||||
import { genres, networks } from '$lib/discover';
|
|
||||||
import { jellyfinItemsStore } from '$lib/stores/data.store';
|
|
||||||
import { settings } from '$lib/stores/settings.store';
|
|
||||||
import type { TitleType } from '$lib/types';
|
|
||||||
import { formatDateToYearMonthDay } from '$lib/utils';
|
|
||||||
import type { ComponentProps } from 'svelte';
|
|
||||||
import { _ } from 'svelte-i18n';
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
|
|
||||||
const jellyfinItemsPromise = new Promise<JellyfinItem[]>((resolve) => {
|
|
||||||
jellyfinItemsStore.subscribe((data) => {
|
|
||||||
if (data.loading) return;
|
|
||||||
resolve(data.data || []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchCardProps = async (
|
|
||||||
items: {
|
|
||||||
name?: string;
|
|
||||||
title?: string;
|
|
||||||
id?: number;
|
|
||||||
vote_average?: number;
|
|
||||||
number_of_seasons?: number;
|
|
||||||
first_air_date?: string;
|
|
||||||
poster_path?: string;
|
|
||||||
}[],
|
|
||||||
type: TitleType | undefined = undefined
|
|
||||||
): Promise<ComponentProps<Poster>[]> => {
|
|
||||||
const filtered = $settings.discover.excludeLibraryItems
|
|
||||||
? items.filter(
|
|
||||||
async (item) =>
|
|
||||||
!(await jellyfinItemsPromise).find((i) => i.ProviderIds?.Tmdb === String(item.id))
|
|
||||||
)
|
|
||||||
: items;
|
|
||||||
|
|
||||||
return Promise.all(filtered.map(async (item) => getPosterProps(item, type))).then((props) =>
|
|
||||||
props.filter((p) => p.backdropUrl)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const trendingItemsPromise = TmdbApiOpen.get('/3/trending/all/{time_window}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
time_window: 'day'
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
language: $settings.language
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then((res) => res.data?.results || []);
|
|
||||||
|
|
||||||
const fetchTrendingProps = () => trendingItemsPromise.then(fetchCardProps);
|
|
||||||
|
|
||||||
const fetchTrendingActorProps = () =>
|
|
||||||
TmdbApiOpen.get('/3/trending/person/{time_window}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
time_window: 'week'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then((actors) =>
|
|
||||||
actors
|
|
||||||
.filter((a) => a.profile_path)
|
|
||||||
.map((actor) => ({
|
|
||||||
tmdbId: actor.id || 0,
|
|
||||||
backdropUri: actor.profile_path || '',
|
|
||||||
name: actor.name || '',
|
|
||||||
subtitle: actor.known_for_department || ''
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchUpcomingMovies = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/movie', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
language: $settings.language,
|
|
||||||
region: $settings.discover.region,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then(fetchCardProps);
|
|
||||||
|
|
||||||
const fetchUpcomingSeries = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/tv', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
'first_air_date.gte': formatDateToYearMonthDay(new Date()),
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
language: $settings.language,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then((i) => fetchCardProps(i, 'series'));
|
|
||||||
|
|
||||||
const fetchDigitalReleases = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/movie', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
with_release_type: 4,
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
'release_date.lte': formatDateToYearMonthDay(new Date()),
|
|
||||||
language: $settings.language,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
// region: $settings.discover.region
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then(fetchCardProps);
|
|
||||||
|
|
||||||
const fetchNowStreaming = () =>
|
|
||||||
TmdbApiOpen.get('/3/discover/tv', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
'air_date.gte': formatDateToYearMonthDay(new Date()),
|
|
||||||
'first_air_date.lte': formatDateToYearMonthDay(new Date()),
|
|
||||||
sort_by: 'popularity.desc',
|
|
||||||
language: $settings.language,
|
|
||||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.data?.results || [])
|
|
||||||
.then((i) => fetchCardProps(i, 'series'));
|
|
||||||
|
|
||||||
function parseIncludedLanguages(includedLanguages: string) {
|
|
||||||
return includedLanguages.replace(' ', '').split(',').join('|');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- {#await trendingItemsPromise then items}
|
|
||||||
{#if items.length}
|
|
||||||
<div class="absolute inset-0 blur-3xl brightness-[0.2] z-[-1] scale-125">
|
|
||||||
<LazyImg src={TMDB_IMAGES_ORIGINAL + items?.[4].backdrop_path} class="h-full" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/await} -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="pt-24 pb-8"
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<div class="max-w-screen-2xl mx-auto">
|
|
||||||
<Carousel
|
|
||||||
gradientFromColor="from-stone-950"
|
|
||||||
heading={$_('discover.trending')}
|
|
||||||
class="mx-2 sm:mx-8 2xl:mx-0"
|
|
||||||
>
|
|
||||||
{#await fetchTrendingProps()}
|
|
||||||
<CarouselPlaceholderItems size="lg" />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} size="lg" />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-12 max-w-screen-2xl mx-auto py-4"
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.popularPeople')}>
|
|
||||||
{#await fetchTrendingActorProps()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<PersonCard {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.upcomingMovies')}>
|
|
||||||
{#await fetchUpcomingMovies()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.upcomingSeries')}>
|
|
||||||
{#await fetchUpcomingSeries()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.genres')}>
|
|
||||||
{#each Object.values(genres) as genre (genre.tmdbGenreId)}
|
|
||||||
<GenreCard {genre} />
|
|
||||||
{/each}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.newDigitalReleases')}>
|
|
||||||
{#await fetchDigitalReleases()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.streamingNow')}>
|
|
||||||
{#await fetchNowStreaming()}
|
|
||||||
<CarouselPlaceholderItems />
|
|
||||||
{:then props}
|
|
||||||
{#each props as prop (prop.tmdbId)}
|
|
||||||
<Poster {...prop} />
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
</Carousel>
|
|
||||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.TVNetworks')}>
|
|
||||||
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
|
||||||
<NetworkCard {network} />
|
|
||||||
{/each}
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { getTmdbGenreMovies } from '$lib/apis/tmdb/tmdbApi';
|
|
||||||
import Card from '$lib/components/Card/Card.svelte';
|
|
||||||
import CardPlaceholder from '$lib/components/Card/CardPlaceholder.svelte';
|
|
||||||
import { fetchCardTmdbProps } from '$lib/components/Card/card';
|
|
||||||
import GridPage from '$lib/components/GridPage/GridPage.svelte';
|
|
||||||
import { genres, type Genre } from '$lib/discover';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
|
|
||||||
const genre = genres[data.genre];
|
|
||||||
|
|
||||||
async function fetchGenreItems(genre: Genre) {
|
|
||||||
const movies = await getTmdbGenreMovies(genre.tmdbGenreId);
|
|
||||||
|
|
||||||
const itemProps = await Promise.all(movies.map(fetchCardTmdbProps));
|
|
||||||
|
|
||||||
return {
|
|
||||||
movies,
|
|
||||||
itemProps
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<GridPage title={data.genre}>
|
|
||||||
{#if genre}
|
|
||||||
{#await fetchGenreItems(genre)}
|
|
||||||
{#each [...Array(20).keys()] as index (index)}
|
|
||||||
<CardPlaceholder size="dynamic" {index} />
|
|
||||||
{/each}
|
|
||||||
{:then { itemProps }}
|
|
||||||
{#each itemProps as itemProps}
|
|
||||||
<Card {...itemProps} size="dynamic" />
|
|
||||||
{/each}
|
|
||||||
{:catch error}
|
|
||||||
{error.message}
|
|
||||||
{/await}
|
|
||||||
{:else}
|
|
||||||
404
|
|
||||||
{/if}
|
|
||||||
</GridPage>
|
|
||||||
|
|
||||||
<!-- <div class="pt-24 p-8 bg-black">
|
|
||||||
<button on:click={() => window?.history?.back()}>Back</button>
|
|
||||||
{data.genre}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-8">
|
|
||||||
{#if genre}
|
|
||||||
{#await fetchGenreItems(genre)}
|
|
||||||
<CardGrid>
|
|
||||||
{#each [...Array(20).keys()] as index (index)}
|
|
||||||
<CardPlaceholder size="dynamic" {index} />
|
|
||||||
{/each}
|
|
||||||
</CardGrid>
|
|
||||||
{:then { itemProps }}
|
|
||||||
<CardGrid>
|
|
||||||
{#each itemProps as itemProps}
|
|
||||||
<Card {...itemProps} size="dynamic" />
|
|
||||||
{/each}
|
|
||||||
</CardGrid>
|
|
||||||
{:catch error}
|
|
||||||
{error.message}
|
|
||||||
{/await}
|
|
||||||
{:else}
|
|
||||||
404
|
|
||||||
{/if}
|
|
||||||
</div> -->
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
|
||||||
return {
|
|
||||||
genre: params.genre
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { getTmdbNetworkSeries } from '$lib/apis/tmdb/tmdbApi';
|
|
||||||
import Card from '$lib/components/Card/Card.svelte';
|
|
||||||
import CardGrid from '$lib/components/Card/CardGrid.svelte';
|
|
||||||
import CardPlaceholder from '$lib/components/Card/CardPlaceholder.svelte';
|
|
||||||
import { fetchCardTmdbSeriesProps } from '$lib/components/Card/card';
|
|
||||||
import type { ComponentProps } from 'svelte';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import GridPage from '$lib/components/GridPage/GridPage.svelte';
|
|
||||||
import { networks, type Network } from '../../../../lib/discover';
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
|
|
||||||
const network = networks[data.network] || undefined;
|
|
||||||
|
|
||||||
async function fetchNetworkSeries(network: Network) {
|
|
||||||
const shows = await getTmdbNetworkSeries(network.tmdbNetworkId);
|
|
||||||
|
|
||||||
const showProps: ComponentProps<Card>[] = await Promise.all(
|
|
||||||
shows.map(fetchCardTmdbSeriesProps)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
shows,
|
|
||||||
showProps
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<GridPage title={data.network}>
|
|
||||||
{#if network}
|
|
||||||
{#await fetchNetworkSeries(network)}
|
|
||||||
{#each [...Array(20).keys()] as index (index)}
|
|
||||||
<CardPlaceholder size="dynamic" {index} />
|
|
||||||
{/each}
|
|
||||||
{:then { showProps }}
|
|
||||||
{#each showProps as showProps}
|
|
||||||
<Card {...showProps} size="dynamic" />
|
|
||||||
{/each}
|
|
||||||
{:catch error}
|
|
||||||
{error.message}
|
|
||||||
{/await}
|
|
||||||
{:else}
|
|
||||||
404
|
|
||||||
{/if}
|
|
||||||
</GridPage>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
|
||||||
return {
|
|
||||||
network: params.network
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
getJellyfinBackdrop,
|
|
||||||
getJellyfinPosterUrl,
|
|
||||||
type JellyfinItem
|
|
||||||
} from '$lib/apis/jellyfin/jellyfinApi';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
|
||||||
import Carousel from '$lib/components/Carousel/Carousel.svelte';
|
|
||||||
import Poster from '$lib/components/Poster/Poster.svelte';
|
|
||||||
import { playerState } from '$lib/components/VideoPlayer/VideoPlayer';
|
|
||||||
import { PLACEHOLDER_BACKDROP } from '$lib/constants';
|
|
||||||
import { jellyfinItemsStore, servarrDownloadsStore } from '$lib/stores/data.store';
|
|
||||||
import { settings } from '$lib/stores/settings.store';
|
|
||||||
import { ChevronRight } from 'radix-icons-svelte';
|
|
||||||
import type { ComponentProps } from 'svelte';
|
|
||||||
import { _ } from 'svelte-i18n';
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import LibraryItems from './LibraryItems.svelte';
|
|
||||||
import { capitalize } from '$lib/utils';
|
|
||||||
import LazyImg from '$lib/components/LazyImg.svelte';
|
|
||||||
|
|
||||||
let openNextUpTab: 'downloading' | 'nextUp' = 'downloading';
|
|
||||||
let noItems = false;
|
|
||||||
|
|
||||||
let showcasePromise: Promise<JellyfinItem | undefined> = jellyfinItemsStore.promise.then(
|
|
||||||
(items) =>
|
|
||||||
items
|
|
||||||
?.slice()
|
|
||||||
?.sort((a, b) =>
|
|
||||||
(a.DateCreated || a.DateLastMediaAdded || '') <
|
|
||||||
(b.DateCreated || b.DateLastMediaAdded || '')
|
|
||||||
? 1
|
|
||||||
: -1
|
|
||||||
)?.[3]
|
|
||||||
);
|
|
||||||
|
|
||||||
let downloadProps: ComponentProps<Poster>[] = [];
|
|
||||||
$: {
|
|
||||||
const sonarrProps: ComponentProps<Poster>[] =
|
|
||||||
$servarrDownloadsStore.sonarrDownloads?.map((item) => ({
|
|
||||||
tvdbId: item.series.tvdbId,
|
|
||||||
title: item.series.title || '',
|
|
||||||
subtitle:
|
|
||||||
`S${item.episode?.seasonNumber}E${item.episode?.episodeNumber} • ` +
|
|
||||||
capitalize(item.status || ''),
|
|
||||||
type: 'series',
|
|
||||||
progress: 100 * (((item.size || 0) - (item.sizeleft || 0)) / (item.size || 1)),
|
|
||||||
backdropUrl: item.series.images?.find((i) => i.coverType === 'poster')?.url || '',
|
|
||||||
orientation: 'portrait'
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const radarrProps: ComponentProps<Poster>[] =
|
|
||||||
$servarrDownloadsStore.radarrDownloads?.map((item) => ({
|
|
||||||
tmdbId: item.movie.tmdbId,
|
|
||||||
title: item.movie.title || '',
|
|
||||||
subtitle: capitalize(item.status || ''),
|
|
||||||
type: 'movie',
|
|
||||||
backdropUrl: item.movie.images?.find((i) => i.coverType === 'poster')?.url || '',
|
|
||||||
progress: 100 * (((item.size || 0) - (item.sizeleft || 0)) / (item.size || 1)),
|
|
||||||
orientation: 'portrait'
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
downloadProps = [...(sonarrProps || []), ...(radarrProps || [])];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if noItems}
|
|
||||||
<div
|
|
||||||
class="h-screen flex items-center justify-center text-zinc-500 p-8"
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
{$_('library.missingConfiguration')}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<div class="relative pt-24">
|
|
||||||
{#await showcasePromise then showcase}
|
|
||||||
<LazyImg
|
|
||||||
src={(showcase && getJellyfinBackdrop(showcase)) || PLACEHOLDER_BACKDROP}
|
|
||||||
class="absolute inset-0"
|
|
||||||
/>
|
|
||||||
{/await}
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-stone-950 to-80% to-darken" />
|
|
||||||
<div
|
|
||||||
class="max-w-screen-2xl mx-auto relative z-[1] px-2 md:px-8 pt-32 xl:pt-56 pb-12 overflow-hidden"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="absolute font-bold uppercase text-amber-200 opacity-10 bottom-12 right-8 text-9xl hidden xl:block z-[-1]"
|
|
||||||
>
|
|
||||||
Library
|
|
||||||
</h1>
|
|
||||||
<div class="flex gap-4 items-end">
|
|
||||||
{#await showcasePromise}
|
|
||||||
<div class="w-32 aspect-[2/3] placeholder rounded-lg shadow-lg" />
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="placeholder-text w-20">Placeholder</div>
|
|
||||||
<div class="placeholder-text w-[50vw] text-3xl sm:text-4xl md:text-5xl">
|
|
||||||
Placeholder
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 mt-2">
|
|
||||||
<div class="placeholder-text w-28 h-10" />
|
|
||||||
<div class="placeholder-text w-28 h-10" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:then showcase}
|
|
||||||
<div
|
|
||||||
style={"background-image: url('" +
|
|
||||||
(showcase ? getJellyfinPosterUrl(showcase) : '') +
|
|
||||||
"');"}
|
|
||||||
class="w-32 aspect-[2/3] rounded-lg bg-center bg-cover flex-shrink-0 shadow-lg"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p class="text-zinc-400 font-medium">Latest Addition</p>
|
|
||||||
<h1 class="text-3xl sm:text-4xl md:text-5xl font-semibold">
|
|
||||||
{showcase?.Name}
|
|
||||||
</h1>
|
|
||||||
<div class="flex gap-2 mt-4">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
on:click={() => showcase?.Id && playerState.streamJellyfinId(showcase?.Id)}
|
|
||||||
>
|
|
||||||
Play<ChevronRight size={20} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
href={`/${showcase?.Type === 'Movie' ? 'movie' : 'series'}/${
|
|
||||||
showcase?.ProviderIds?.Tmdb || showcase?.ProviderIds?.Tvdb
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>{$_('titleShowcase.details')}</span><ChevronRight size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="py-4 px-2 md:px-8"
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<div class="max-w-screen-2xl m-auto flex flex-col gap-12">
|
|
||||||
{#if downloadProps?.length}
|
|
||||||
<div>
|
|
||||||
<Carousel heading="Downloading">
|
|
||||||
{#each downloadProps as props}
|
|
||||||
<Poster {...props} />
|
|
||||||
{/each}
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<LibraryItems />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import MoviePage from './MoviePage.svelte';
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
|
|
||||||
let tmdbId: number;
|
|
||||||
$: tmdbId = Number(data.tmdbId);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#key tmdbId}
|
|
||||||
<MoviePage {tmdbId} />
|
|
||||||
{/key}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
|
||||||
return {
|
|
||||||
tmdbId: params.id
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import PersonPage from './PersonPage.svelte';
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
|
|
||||||
let personId: number;
|
|
||||||
$: personId = Number(data.personId);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#key personId}
|
|
||||||
<PersonPage tmdbId={personId} />
|
|
||||||
{/key}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
|
||||||
return {
|
|
||||||
personId: Number(params.id)
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { TitleId } from '$lib/types';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import SeriesPage from './SeriesPage.svelte';
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
|
|
||||||
let titleId: TitleId;
|
|
||||||
$: titleId = { provider: 'tmdb', id: data.tmdbId, type: 'series' };
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#key titleId}
|
|
||||||
<SeriesPage {titleId} />
|
|
||||||
{/key}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
|
||||||
return {
|
|
||||||
tmdbId: Number(params.id)
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { version } from '$app/environment';
|
|
||||||
import { beforeNavigate } from '$app/navigation';
|
|
||||||
import { getJellyfinHealth, getJellyfinUsers } from '$lib/apis/jellyfin/jellyfinApi';
|
|
||||||
import { getRadarrHealth } from '$lib/apis/radarr/radarrApi';
|
|
||||||
import { getSonarrHealth } from '$lib/apis/sonarr/sonarrApi';
|
|
||||||
import FormButton from '$lib/components/forms/FormButton.svelte';
|
|
||||||
import Select from '$lib/components/forms/Select.svelte';
|
|
||||||
import { settings, type SettingsValues } from '$lib/stores/settings.store';
|
|
||||||
import axios from 'axios';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { ChevronLeft } from 'radix-icons-svelte';
|
|
||||||
import GeneralSettingsPage from './GeneralSettingsPage.svelte';
|
|
||||||
import IntegrationSettingsPage from './IntegrationSettingsPage.svelte';
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import { _ } from 'svelte-i18n';
|
|
||||||
import { createErrorNotification } from '$lib/stores/notification.store';
|
|
||||||
|
|
||||||
type Section = 'general' | 'integrations';
|
|
||||||
|
|
||||||
let openTab: Section = 'general';
|
|
||||||
|
|
||||||
let sonarrConnected = false;
|
|
||||||
let radarrConnected = false;
|
|
||||||
let jellyfinConnected = false;
|
|
||||||
|
|
||||||
let values: SettingsValues;
|
|
||||||
let initialValues: SettingsValues;
|
|
||||||
settings.subscribe(async (v) => {
|
|
||||||
values = structuredClone(v);
|
|
||||||
initialValues = structuredClone(v);
|
|
||||||
const s = updateSonarrHealth();
|
|
||||||
const r = updateRadarrHealth();
|
|
||||||
const j = updateJellyfinHealth();
|
|
||||||
|
|
||||||
await Promise.all([s, r, j]);
|
|
||||||
|
|
||||||
checkForPartialConfiguration(v);
|
|
||||||
});
|
|
||||||
|
|
||||||
let valuesChanged = false;
|
|
||||||
$: valuesChanged = JSON.stringify(initialValues) !== JSON.stringify(values);
|
|
||||||
|
|
||||||
let submitLoading = false;
|
|
||||||
function handleSubmit() {
|
|
||||||
if (submitLoading || !valuesChanged) return;
|
|
||||||
submitLoading = true;
|
|
||||||
submit().finally(() => (submitLoading = false));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
if (
|
|
||||||
values.sonarr.apiKey &&
|
|
||||||
values.sonarr.baseUrl &&
|
|
||||||
!(await getSonarrHealth(values.sonarr.baseUrl, values.sonarr.apiKey))
|
|
||||||
) {
|
|
||||||
createErrorNotification(
|
|
||||||
'Invalid Configuration',
|
|
||||||
'Could not connect to Sonarr. Check Sonarr credentials.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
values.radarr.apiKey &&
|
|
||||||
values.radarr.baseUrl &&
|
|
||||||
!(await getRadarrHealth(values.radarr.baseUrl, values.radarr.apiKey))
|
|
||||||
) {
|
|
||||||
createErrorNotification(
|
|
||||||
'Invalid Configuration',
|
|
||||||
'Could not connect to Radarr. Check Radarr credentials.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.jellyfin.apiKey && values.jellyfin.baseUrl) {
|
|
||||||
if (!(await getJellyfinHealth(values.jellyfin.baseUrl, values.jellyfin.apiKey))) {
|
|
||||||
createErrorNotification(
|
|
||||||
'Invalid Configuration',
|
|
||||||
'Could not connect to Jellyfin. Check Jellyfin credentials.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const users = await getJellyfinUsers(values.jellyfin.baseUrl, values.jellyfin.apiKey);
|
|
||||||
if (!users.find((u) => u.Id === values.jellyfin.userId)) values.jellyfin.userId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSonarrHealth();
|
|
||||||
updateRadarrHealth();
|
|
||||||
updateJellyfinHealth();
|
|
||||||
|
|
||||||
axios.post('/api/settings', values).then(() => {
|
|
||||||
settings.set(values);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForPartialConfiguration(v: SettingsValues) {
|
|
||||||
let error = '';
|
|
||||||
if (sonarrConnected && !v.sonarr.rootFolderPath) {
|
|
||||||
error = 'Sonarr disabled: Root folder path is required';
|
|
||||||
} else if (sonarrConnected && !v.sonarr.qualityProfileId) {
|
|
||||||
error = 'Sonarr disabled: Quality profile is required';
|
|
||||||
} else if (sonarrConnected && !v.sonarr.languageProfileId) {
|
|
||||||
error = 'Sonarr disabled: Language profile is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (radarrConnected && !v.radarr.rootFolderPath) {
|
|
||||||
error = 'Radarr disabled: Root folder path is required';
|
|
||||||
} else if (radarrConnected && !v.radarr.qualityProfileId) {
|
|
||||||
error = 'Radarr disabled: Quality profile is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jellyfinConnected && !v.jellyfin.userId) {
|
|
||||||
error = 'Jellyfin disabled: User is required';
|
|
||||||
}
|
|
||||||
if (error) createErrorNotification('Incomplete Configuration', error, 'warning');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateSonarrHealth(reset = false): Promise<boolean> {
|
|
||||||
if (!values.sonarr.baseUrl || !values.sonarr.apiKey || reset) {
|
|
||||||
sonarrConnected = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return getSonarrHealth(
|
|
||||||
values.sonarr.baseUrl || undefined,
|
|
||||||
values.sonarr.apiKey || undefined
|
|
||||||
).then((ok) => {
|
|
||||||
sonarrConnected = ok;
|
|
||||||
return ok;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateRadarrHealth(reset = false): Promise<boolean> {
|
|
||||||
if (!values.radarr.baseUrl || !values.radarr.apiKey || reset) {
|
|
||||||
radarrConnected = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return getRadarrHealth(
|
|
||||||
values.radarr.baseUrl || undefined,
|
|
||||||
values.radarr.apiKey || undefined
|
|
||||||
).then((ok) => {
|
|
||||||
radarrConnected = ok;
|
|
||||||
return ok;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateJellyfinHealth(reset = false): Promise<boolean> {
|
|
||||||
if (!values.jellyfin.baseUrl || !values.jellyfin.apiKey || reset) {
|
|
||||||
jellyfinConnected = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getJellyfinHealth(
|
|
||||||
values.jellyfin.baseUrl || undefined,
|
|
||||||
values.jellyfin.apiKey || undefined
|
|
||||||
).then((ok) => {
|
|
||||||
jellyfinConnected = ok;
|
|
||||||
return ok;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNavButtonStyle = (section: Section) =>
|
|
||||||
classNames('rounded-xl p-2 px-6 font-medium text-left', {
|
|
||||||
'text-zinc-200 bg-lighten': openTab === section,
|
|
||||||
'text-zinc-300 hover:text-zinc-200': openTab !== section
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeNavigate(({ cancel }) => {
|
|
||||||
if (valuesChanged) {
|
|
||||||
if (!confirm('You have unsaved changes. Are you sure you want to leave?')) cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleKeybinds(event: KeyboardEvent) {
|
|
||||||
if (event.key === 's' && (event.ctrlKey || event.metaKey)) {
|
|
||||||
event.preventDefault();
|
|
||||||
handleSubmit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeybinds} />
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="min-h-screen sm:h-screen flex-1 flex flex-col sm:flex-row w-full sm:pt-24"
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="hidden sm:flex flex-col gap-2 border-r border-zinc-800 justify-between w-64 p-8 border-t"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<button
|
|
||||||
class="mb-6 text-lg font-medium flex items-center text-zinc-300 hover:text-zinc-200"
|
|
||||||
on:click={() => history.back()}
|
|
||||||
>
|
|
||||||
<ChevronLeft size={22} />
|
|
||||||
{$_('settings.navbar.settings')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
on:click={() => (openTab = 'general')}
|
|
||||||
class={openTab && getNavButtonStyle('general')}
|
|
||||||
>
|
|
||||||
{$_('settings.navbar.general')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
on:click={() => (openTab = 'integrations')}
|
|
||||||
class={openTab && getNavButtonStyle('integrations')}
|
|
||||||
>
|
|
||||||
{$_('settings.navbar.integrations')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<FormButton
|
|
||||||
disabled={!valuesChanged}
|
|
||||||
loading={submitLoading}
|
|
||||||
on:click={handleSubmit}
|
|
||||||
type={valuesChanged ? 'success' : 'base'}
|
|
||||||
>
|
|
||||||
{$_('settings.misc.saveChanges')}
|
|
||||||
</FormButton>
|
|
||||||
<FormButton
|
|
||||||
disabled={!valuesChanged}
|
|
||||||
type="error"
|
|
||||||
on:click={() => {
|
|
||||||
settings.set(initialValues);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$_('settings.misc.resetToDefaults')}
|
|
||||||
</FormButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sm:hidden px-8 pt-20 pb-4 flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
class="text-lg font-medium flex items-center text-zinc-300 hover:text-zinc-200"
|
|
||||||
on:click={() => history.back()}
|
|
||||||
>
|
|
||||||
<ChevronLeft size={22} />
|
|
||||||
{$_('settings.navbar.settings')}
|
|
||||||
</button>
|
|
||||||
<Select bind:value={openTab}>
|
|
||||||
<option value="general"> {$_('settings.navbar.general')} </option>
|
|
||||||
<option value="integrations">
|
|
||||||
{$_('settings.navbar.integrations')}
|
|
||||||
</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col border-t border-zinc-800 justify-between">
|
|
||||||
<div class="overflow-y-scroll overflow-x-hidden px-8">
|
|
||||||
<div class="max-w-screen-md mx-auto mb-auto w-full">
|
|
||||||
{#if openTab === 'general'}
|
|
||||||
<GeneralSettingsPage bind:values />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if openTab === 'integrations'}
|
|
||||||
<IntegrationSettingsPage
|
|
||||||
bind:values
|
|
||||||
{sonarrConnected}
|
|
||||||
{radarrConnected}
|
|
||||||
{jellyfinConnected}
|
|
||||||
{updateSonarrHealth}
|
|
||||||
{updateRadarrHealth}
|
|
||||||
{updateJellyfinHealth}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center p-4 gap-8 justify-center text-zinc-500 bg-stone-950">
|
|
||||||
<div>v{version}</div>
|
|
||||||
<a target="_blank" href="https://github.com/aleksilassila/reiverr/releases">
|
|
||||||
{$_('settings.misc.changelog')}
|
|
||||||
</a>
|
|
||||||
<a target="_blank" href="https://github.com/aleksilassila/reiverr">GitHub</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Language settings -->
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import RadarrStats from '$lib/components/SourceStats/RadarrStats.svelte';
|
|
||||||
import SonarrStats from '$lib/components/SourceStats/SonarrStats.svelte';
|
|
||||||
import { settings } from '$lib/stores/settings.store';
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="pt-24 px-8 min-h-screen flex justify-center"
|
|
||||||
in:fade|global={{
|
|
||||||
duration: $settings.animationDuration,
|
|
||||||
delay: $settings.animationDuration
|
|
||||||
}}
|
|
||||||
out:fade|global={{ duration: $settings.animationDuration }}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-4 max-w-3xl flex-1">
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<RadarrStats large />
|
|
||||||
<SonarrStats large />
|
|
||||||
<!-- <div
|
|
||||||
class="border-zinc-800 border-2 border-dashed relative w-full p-3 px-4 rounded-xl overflow-hidden text-zinc-500 text-center flex flex-col gap-1"
|
|
||||||
>
|
|
||||||
<h2 class="font-medium">Sonarr is not set up</h2>
|
|
||||||
<p class="text-sm">
|
|
||||||
To set up Sonarr, define the <code>PUBLIC_SONARR_API_KEY</code> and <code>SONARR_URL</code> environment
|
|
||||||
variables.
|
|
||||||
</p>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
<!-- <div>Sources</div>-->
|
|
||||||
<!-- <div>Streaming services</div>-->
|
|
||||||
<!-- <div>(P2P network?)</div>-->
|
|
||||||
<!-- <div>Configure Sonarr</div>-->
|
|
||||||
<!-- <div>Configure Radarr</div>-->
|
|
||||||
<!-- <div>(Configure Plex)</div>-->
|
|
||||||
<!-- <div>(Configure Jellyfinn)</div>-->
|
|
||||||
<!-- <div>(Configure IMDB)</div>-->
|
|
||||||
<!-- <div>(Configure TMDB)</div>-->
|
|
||||||
</div>
|
|
||||||
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
27
svelte.config.js
Executable file → Normal file
@@ -1,5 +1,4 @@
|
|||||||
import adapter from '@sveltejs/adapter-static';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
@@ -7,28 +6,10 @@ const file = fileURLToPath(new URL('package.json', import.meta.url));
|
|||||||
const json = readFileSync(file, 'utf8');
|
const json = readFileSync(file, 'utf8');
|
||||||
const pkg = JSON.parse(json);
|
const pkg = JSON.parse(json);
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
export default {
|
||||||
const config = {
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
kit: {
|
|
||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
|
||||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
|
||||||
adapter: adapter({
|
|
||||||
fallback: 'index.html'
|
|
||||||
}),
|
|
||||||
paths: {
|
|
||||||
assets: 'https://replaceme.com'
|
|
||||||
},
|
|
||||||
|
|
||||||
version: {
|
|
||||||
name: pkg.version
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
vitePlugin: {
|
vitePlugin: {
|
||||||
inspector: {
|
inspector: {
|
||||||
// toggleKeyCombo: 'meta',
|
// toggleKeyCombo: 'meta',
|
||||||
@@ -36,5 +17,3 @@ const config = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
{
|
{
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"target": "ESNext",
|
||||||
"checkJs": true,
|
"useDefineForClassFields": true,
|
||||||
"esModuleInterop": true,
|
"module": "ESNext",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"resolveJsonModule": true,
|
||||||
"resolveJsonModule": true,
|
/**
|
||||||
"skipLibCheck": true,
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
"sourceMap": true,
|
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||||
"strict": true,
|
* Note that setting allowJs false does not prevent the use
|
||||||
"target": "ES2022",
|
* of JS in `.svelte` files.
|
||||||
"module": "ES2022",
|
*/
|
||||||
"lib": ["es6"],
|
"allowJs": true,
|
||||||
"emitDecoratorMetadata": true,
|
"checkJs": true,
|
||||||
"experimentalDecorators": true,
|
"isolatedModules": true
|
||||||
"strictPropertyInitialization": false
|
},
|
||||||
}
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
//
|
|
||||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
|
||||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
|
||||||
}
|
}
|
||||||
|
|||||||
9
tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { defineConfig } from 'vite';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
// import * as pkg from './package.json';
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import viteLegacyPlugin from '@vitejs/plugin-legacy';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [viteLegacyPlugin(), svelte(), viteSingleFile()]
|
||||||
// define: {
|
|
||||||
// PKG: pkg
|
// base: '/dist',
|
||||||
// },
|
// experimental: {
|
||||||
test: {
|
// renderBuiltUrl() {
|
||||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
// return { relative: true }
|
||||||
},
|
// }
|
||||||
ssr: {
|
// },
|
||||||
external: ['reflect-metadata']
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||