mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 15:25:29 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e7702b16b | |||
| fa2a4a40d0 | |||
| 203e4a31ef | |||
| 5f2c1977ee | |||
| 5860fd2783 | |||
| 3c2cbf55c6 |
@@ -0,0 +1,3 @@
|
||||
# Docker dev volume
|
||||
container_data/
|
||||
data/
|
||||
@@ -54,7 +54,7 @@ Feel free to abuse this demo instance (nicely), which runs on the latest `dev` b
|
||||
|
||||
# Set Up
|
||||
|
||||
[Checkout our documentation](https://watcharr.app/docs/category/installation) for an up to date guide on setup! If you hate manuals, but love docker, this [docker-compose.yml](./docker-compose.yml) file is your friend.
|
||||
[Checkout our documentation](https://watcharr.app/docs/category/installation) for an up to date guide on setup! If you hate manuals, but love docker, this [compose.yml](./compose.yml) file is your friend.
|
||||
|
||||
# Community Made Tools
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# This compose file will replace watcharrs
|
||||
# built in UI with a custom one provided in
|
||||
# a named volume (for use with ui-build image).
|
||||
|
||||
services:
|
||||
watcharr:
|
||||
# We dont need to build here too for testing if
|
||||
# a custom ui works, so using latest prod image.
|
||||
image: ghcr.io/sbondco/watcharr:latest
|
||||
container_name: watcharr
|
||||
ports:
|
||||
- 3080:3080
|
||||
volumes:
|
||||
- ./container_data:/data
|
||||
- type: volume
|
||||
source: watcharr-ui
|
||||
target: /ui
|
||||
volume:
|
||||
nocopy: true
|
||||
subpath: build
|
||||
|
||||
volumes:
|
||||
watcharr-ui:
|
||||
external: true
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: Install and setup with Docker Compose for access through a subpath.
|
||||
---
|
||||
|
||||
# Docker Compose (subpath)
|
||||
|
||||
Install and setup with Docker Compose for access through a subpath.
|
||||
|
||||
:::info Not a great experience at the moment!
|
||||
|
||||
Currently, hosting Watcharr via a subpath on your server is not very easy (unlike hosting under a subdomain). Hopefully this will change in the future, but as a temporary measure to at least allow hosting under a subpath, this method has been provided.
|
||||
|
||||
The following issue will continue to track this: https://github.com/sbondCo/Watcharr/issues/312
|
||||
|
||||
:::
|
||||
|
||||
## Installing
|
||||
|
||||
### Build UI
|
||||
|
||||
First we have to build the frontend with our subpath provided as an environment variable.
|
||||
|
||||
If you don't want to use `/watcharr` as your subpath, replace the value of `WATCHARR_BASE` before running the command.
|
||||
|
||||
```bash
|
||||
docker run -e WATCHARR_BASE=/watcharr -v watcharr-ui:/ui --rm ghcr.io/sbondco/watcharr-ui-build:latest
|
||||
```
|
||||
|
||||
Once the container has finished running the build script, it will exit and remove itself. The `watcharr-ui` volume will contain the built files.
|
||||
|
||||
### Install Watcharr
|
||||
|
||||
Now we can install Watcharr. You can copy the example below to get started:
|
||||
|
||||
```yaml title="compose.yml"
|
||||
services:
|
||||
watcharr:
|
||||
# The :latest tag is used for simplicity, it is recommended
|
||||
# to use an actual version, then when updating check the releases for changelogs.
|
||||
image: ghcr.io/sbondco/watcharr:latest
|
||||
container_name: watcharr
|
||||
ports:
|
||||
- 3080:3080
|
||||
volumes:
|
||||
# Contains all of watcharr data (database & cache)
|
||||
- ./data:/data
|
||||
# Use our volume containing built ui files
|
||||
# instead of default ui included in image.
|
||||
- type: volume
|
||||
source: watcharr-ui
|
||||
target: /ui
|
||||
volume:
|
||||
nocopy: true
|
||||
subpath: build
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
watcharr-ui:
|
||||
external: true
|
||||
```
|
||||
|
||||
:::danger first account
|
||||
|
||||
When **first** running Watcharr, make sure only you have access. The first user created will become admin.
|
||||
|
||||
:::
|
||||
|
||||
You can now start `Watcharr` like so:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
If you didn't change the ports in the example, the server will be available at [http://localhost:3080/](http://localhost:3080/).
|
||||
|
||||
## Updating
|
||||
|
||||
:::danger Take care
|
||||
|
||||
We try taking care as to not release breaking changes, however it is still recommended that
|
||||
you lookover changelogs before updating!
|
||||
|
||||
Breaking changes are marked at the top of releases: https://github.com/sbondCo/Watcharr/releases
|
||||
|
||||
:::
|
||||
|
||||
1. Update your built ui files by following the [Build UI](#build-ui) step again.
|
||||
|
||||
2. Update the `image` version in your `compose.yml` file.
|
||||
Skip this step if you are using the `latest` tag.
|
||||
|
||||
```yaml
|
||||
# eg. update v1.19.0 to v1.20.0 (or whatever version you are updating to)
|
||||
image: ghcr.io/sbondco/watcharr:v1.19.0
|
||||
```
|
||||
|
||||
3. Pull the new changes and re-create your container:
|
||||
|
||||
```bash
|
||||
docker compose pull && docker compose down && docker compose up -d
|
||||
```
|
||||
|
||||
And that is it!
|
||||
@@ -9,7 +9,7 @@ description: Install and setup with Docker Compose.
|
||||
|
||||
Installing Watcharr with a docker compose file is easy. You can copy the example below to get started:
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
```yaml title="compose.yml"
|
||||
services:
|
||||
watcharr:
|
||||
# The :latest tag is used for simplicity, it is recommended
|
||||
@@ -51,7 +51,7 @@ Breaking changes are marked at the top of releases: https://github.com/sbondCo/W
|
||||
|
||||
Updating your server can be done in two steps:
|
||||
|
||||
1. Update the `image` version in your `docker-compose.yml` file.
|
||||
1. Update the `image` version in your `compose.yml` file.
|
||||
Skip this step if you are using the `latest` tag.
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -80,9 +80,6 @@ const config = {
|
||||
position: "left",
|
||||
label: "Docs",
|
||||
},
|
||||
{
|
||||
type: "docsVersionDropdown",
|
||||
},
|
||||
{
|
||||
href: "https://beta.watcharr.app",
|
||||
label: "Demo",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
This folder is for any _extra_ images that we produce.
|
||||
|
||||
The main watcharr Dockerfile remains in project root.
|
||||
@@ -0,0 +1,16 @@
|
||||
#
|
||||
# Frontend
|
||||
#
|
||||
FROM node:20-alpine AS ui
|
||||
|
||||
COPY ./docker/ui-build/entrypoint.sh /entrypoint.sh
|
||||
RUN ["chmod", "+x", "/entrypoint.sh"]
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json vite.config.ts svelte.config.js tsconfig.json ./
|
||||
COPY ./src ./src
|
||||
COPY ./static ./static
|
||||
|
||||
VOLUME /ui
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,7 @@
|
||||
Image for building watcharr ui.
|
||||
|
||||
Accepts environment variables:
|
||||
|
||||
- `WATCHARR_BASE`
|
||||
|
||||
https://watcharr.app/docs/installation/docker-compose-subpath
|
||||
@@ -0,0 +1,15 @@
|
||||
# To test prod
|
||||
services:
|
||||
watcharr-ui-build:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: ./docker/ui-build/Dockerfile
|
||||
container_name: watcharr-ui-build
|
||||
volumes:
|
||||
- watcharr-ui:/ui
|
||||
environment:
|
||||
WATCHARR_BASE: "/sp"
|
||||
|
||||
volumes:
|
||||
watcharr-ui:
|
||||
name: "watcharr-ui"
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This is loosely based on the main containers Dockerfile commands.
|
||||
# Just everything needed to get ui built and ready and nothing else.
|
||||
|
||||
set -x
|
||||
|
||||
echo "wuib: Starting build"
|
||||
|
||||
# Install all deps
|
||||
/usr/local/bin/npm install
|
||||
|
||||
# Run build
|
||||
/usr/local/bin/npm run build
|
||||
|
||||
# Remove any existing build files from volume
|
||||
rm -rf /ui/build
|
||||
|
||||
# Move build folder to /ui
|
||||
mv /app/build /ui
|
||||
# Move package.json and lock file to /ui/build for final npm ci call
|
||||
mv /app/package.json /app/package-lock.json /ui/build
|
||||
|
||||
# cd to build files final destination and install dependencies for production
|
||||
cd /ui/build && /usr/local/bin/npm ci --omit=dev --ignore-scripts=true
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { PublicUser } from "@/types";
|
||||
import { toBaseUrl } from "./util/url";
|
||||
|
||||
interface Props {
|
||||
users: PublicUser[];
|
||||
@@ -13,7 +14,7 @@
|
||||
<ul>
|
||||
{#each users as user}
|
||||
<li title={user.username}>
|
||||
<a href="/lists/{user.id}/{user.username}">
|
||||
<a href={toBaseUrl(`/lists/${user.id}/${user.username}`)}>
|
||||
<span>{user.username}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { getLatestWatchedInTv } from "./util/helpers";
|
||||
import { notify } from "./util/notify";
|
||||
import { untrack } from "svelte";
|
||||
import { toBaseUrl } from "./util/url";
|
||||
|
||||
interface Props {
|
||||
list: Watched[];
|
||||
@@ -296,7 +297,7 @@
|
||||
<h4 class="norm">
|
||||
Try searching for something you would like to add.
|
||||
</h4>
|
||||
<button onclick={() => goto("/import")}>Import</button>
|
||||
<button onclick={() => goto(toBaseUrl("/import"))}>Import</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import Spinner from "../Spinner.svelte";
|
||||
import { clearWatcharrData } from ".";
|
||||
import { goto } from "$app/navigation";
|
||||
import { toBaseUrl } from "../util/url";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -34,7 +35,7 @@
|
||||
|
||||
function logout() {
|
||||
clearWatcharrData();
|
||||
goto("/login?noAuto=1");
|
||||
goto(toBaseUrl("/login?noAuto=1"));
|
||||
}
|
||||
|
||||
function proxyLogout() {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { clearWatcharrData } from "../logout";
|
||||
import { notify } from "../util/notify";
|
||||
import AboutModal from "./AboutModal.svelte";
|
||||
import { toBaseUrl } from "../util/url";
|
||||
|
||||
let user = $derived(store.userInfo);
|
||||
let proxyUserLogoutShown = $state(false);
|
||||
@@ -20,23 +21,23 @@
|
||||
return;
|
||||
}
|
||||
clearWatcharrData();
|
||||
goto("/login");
|
||||
goto(toBaseUrl("/login"));
|
||||
}
|
||||
|
||||
function profile() {
|
||||
goto("/profile");
|
||||
goto(toBaseUrl("/profile"));
|
||||
}
|
||||
|
||||
function serverSettings() {
|
||||
goto("/server");
|
||||
goto(toBaseUrl("/server"));
|
||||
}
|
||||
|
||||
function userManagement() {
|
||||
goto("/manage_users");
|
||||
goto(toBaseUrl("/manage_users"));
|
||||
}
|
||||
|
||||
function requestManagement() {
|
||||
goto("/arr_requests");
|
||||
goto(toBaseUrl("/arr_requests"));
|
||||
}
|
||||
|
||||
function shareWatchedList() {
|
||||
@@ -44,7 +45,9 @@
|
||||
const ud = parseTokenPayload();
|
||||
console.log(ud);
|
||||
if (ud?.userId && ud?.username) {
|
||||
const shareLink = `${window.location.origin}/lists/${ud.userId}/${ud.username}`;
|
||||
const shareLink = `${window.location.origin}${toBaseUrl(
|
||||
`/lists/${ud.userId}/${ud.username}`,
|
||||
)}`;
|
||||
navigator.clipboard
|
||||
.writeText(shareLink)
|
||||
.then(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { store } from "@/store.svelte";
|
||||
import Menu from "../Menu.svelte";
|
||||
import { toBaseUrl } from "../util/url";
|
||||
|
||||
interface Props {
|
||||
close: () => {};
|
||||
@@ -15,7 +16,9 @@
|
||||
<div class="list">
|
||||
{#each store.follows as f}
|
||||
<a
|
||||
href="/lists/{f.followedUser.id}/{f.followedUser.username}"
|
||||
href={toBaseUrl(
|
||||
`/lists/${f.followedUser.id}/${f.followedUser.username}`,
|
||||
)}
|
||||
onclick={() => close()}
|
||||
>
|
||||
{f.followedUser.username}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import PosterRating from "./PosterRating.svelte";
|
||||
import { decode } from "blurhash";
|
||||
import ExtraDetails from "./ExtraDetails.svelte";
|
||||
import { toBaseUrl } from "../util/url";
|
||||
|
||||
interface Props {
|
||||
id?: number | undefined; // Watched list id
|
||||
@@ -72,7 +73,7 @@
|
||||
const poster = media.poster?.path
|
||||
? `${baseURL}/${media.poster.path}`
|
||||
: `https://images.igdb.com/igdb/image/upload/t_cover_big/${media.coverId}.jpg`;
|
||||
const link = media.id ? `/game/${media.id}` : undefined;
|
||||
const link = media.id ? toBaseUrl(`/game/${media.id}`) : undefined;
|
||||
const dateStr = media.firstReleaseDate;
|
||||
const year = dateStr ? new Date(dateStr).getFullYear() : undefined;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
addClassToParent,
|
||||
calculateTransformOrigin,
|
||||
} from "@/lib/util/helpers";
|
||||
import { toBaseUrl } from "../util/url";
|
||||
|
||||
interface Props {
|
||||
id: number | undefined;
|
||||
@@ -24,7 +25,7 @@
|
||||
const poster = path
|
||||
? `https://image.tmdb.org/t/p/w300_and_h450_bestv2${path}`
|
||||
: undefined;
|
||||
const link = id ? `/person/${id}` : undefined;
|
||||
const link = id ? toBaseUrl(`/person/${id}`) : undefined;
|
||||
</script>
|
||||
|
||||
<!-- Quick fix to ignore error, should be fixed -->
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import PosterStatus from "./PosterStatus.svelte";
|
||||
import PosterRating from "./PosterRating.svelte";
|
||||
import ExtraDetails from "./ExtraDetails.svelte";
|
||||
import { toBaseUrl } from "../util/url";
|
||||
|
||||
interface Props {
|
||||
id?: number | undefined; // Watched list id
|
||||
@@ -84,7 +85,7 @@
|
||||
: `https://image.tmdb.org/t/p/w500${media.poster_path}`,
|
||||
);
|
||||
let link = $derived(
|
||||
media.id ? `/${media.media_type}/${media.id}` : undefined,
|
||||
media.id ? toBaseUrl(`/${media.media_type}/${media.id}`) : undefined,
|
||||
);
|
||||
let dateStr = $derived(media.release_date || media.first_air_date);
|
||||
let year = $derived(dateStr ? new Date(dateStr).getFullYear() : undefined);
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ import {
|
||||
import axios from "axios";
|
||||
import { notify, unNotify } from "./notify";
|
||||
import { browser } from "$app/environment";
|
||||
import { toBaseUrl } from "./url";
|
||||
const { MODE } = import.meta.env;
|
||||
|
||||
export const baseURL =
|
||||
@@ -23,7 +24,7 @@ export const baseURL =
|
||||
? browser
|
||||
? `${location.protocol}//${location.hostname}:3080/api`
|
||||
: "http://127.0.0.1:3080/api"
|
||||
: "/api";
|
||||
: toBaseUrl("/api");
|
||||
console.log("api: baseURL constructed:", baseURL);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { base } from "$app/paths";
|
||||
|
||||
/**
|
||||
* Takes in a `path` and prefixes it with `base`.
|
||||
* This enables support for using base paths to access
|
||||
* the frontend by fixing all absolute paths passed through.
|
||||
*/
|
||||
export function toBaseUrl(path: string) {
|
||||
return base + path;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto } from "$app/navigation";
|
||||
import { base } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import Icon from "@/lib/Icon.svelte";
|
||||
import PageError from "@/lib/PageError.svelte";
|
||||
@@ -12,6 +13,7 @@
|
||||
import SortMenu from "@/lib/nav/SortMenu.svelte";
|
||||
import TagMenu from "@/lib/tag/TagMenu.svelte";
|
||||
import { isTouch } from "@/lib/util/helpers";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
import { store, defaultSort } from "@/store.svelte";
|
||||
import axios from "axios";
|
||||
import { onMount } from "svelte";
|
||||
@@ -34,7 +36,7 @@
|
||||
|
||||
function handleProfileClick() {
|
||||
if (!localStorage.getItem("token")) {
|
||||
goto("/login");
|
||||
goto(toBaseUrl("/login"));
|
||||
} else {
|
||||
closeAllSubMenus("sub");
|
||||
subMenuShown = !subMenuShown;
|
||||
@@ -64,7 +66,7 @@
|
||||
// Using autofocus seems to work. Disables after goto runs.
|
||||
// https://github.com/sbondCo/Watcharr/issues/169
|
||||
target.autofocus = true;
|
||||
goto(`/search?q=${encodeURIComponent(query)}`).then(() => {
|
||||
goto(toBaseUrl(`/search?q=${encodeURIComponent(query)}`)).then(() => {
|
||||
// Use mainSearchEl if nav not split, otherwise use ev target.
|
||||
if (
|
||||
!document.body.classList.contains("split-nav") &&
|
||||
@@ -112,7 +114,7 @@
|
||||
store.tags = ts.data;
|
||||
}
|
||||
} else {
|
||||
goto("/login?again=1");
|
||||
goto(toBaseUrl("/login?again=1"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +189,7 @@
|
||||
|
||||
<nav bind:this={navEl}>
|
||||
<div class="wrapper">
|
||||
<a href="/">
|
||||
<a href={toBaseUrl("/")}>
|
||||
<span class="large">Watcharr</span>
|
||||
<span class="small">W</span>
|
||||
</a>
|
||||
@@ -278,7 +280,7 @@
|
||||
{#if tagMenuShown}
|
||||
<TagMenu
|
||||
onTagClick={(tag) => {
|
||||
goto(`/tag/${tag.id}`);
|
||||
goto(toBaseUrl(`/tag/${tag.id}`));
|
||||
tagMenuShown = false;
|
||||
}}
|
||||
showManageBtn={true}
|
||||
@@ -286,7 +288,7 @@
|
||||
{/if}
|
||||
<button
|
||||
class="plain other discover"
|
||||
onclick={() => goto("/discover")}
|
||||
onclick={() => goto(toBaseUrl("/discover"))}
|
||||
use:tooltip={{ text: "Discover", pos: "bot" }}
|
||||
>
|
||||
<Icon i="compass" wh={26} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import axios from "axios";
|
||||
import { baseURL } from "@/lib/util/api";
|
||||
import { notify } from "@/lib/util/notify";
|
||||
import { clearWatcharrData } from "@/lib/logout";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
@@ -18,7 +19,7 @@ axios.interceptors.request.use(
|
||||
// Don't require token check if going to auth route (login/register)
|
||||
if (!token && !config.url?.includes("/auth")) {
|
||||
console.error("No token, going to login. Endpoint:", config.url);
|
||||
goto("/login?again=1");
|
||||
goto(toBaseUrl("/login?again=1"));
|
||||
throw new axios.Cancel("No auth token found");
|
||||
}
|
||||
config.headers.set("Authorization", token);
|
||||
@@ -40,7 +41,7 @@ axios.interceptors.response.use(
|
||||
console.error("Recieved 401 response, going to login.");
|
||||
notify({ text: "Request Authorization Failed!", type: "error" });
|
||||
clearWatcharrData();
|
||||
goto("/login?again=1");
|
||||
goto(toBaseUrl("/login?again=1"));
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { baseURL } from "@/lib/util/api";
|
||||
import { toRelativeDate } from "@/lib/util/helpers";
|
||||
import { notify } from "@/lib/util/notify";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
import {
|
||||
type ArrRequestResponse,
|
||||
type TMDBMovieDetails,
|
||||
@@ -94,7 +95,7 @@
|
||||
<h2 class="norm">
|
||||
<a
|
||||
data-sveltekit-preload-data="tap"
|
||||
href={`/${r.content.type}/${r.content.tmdbId}`}
|
||||
href={toBaseUrl(`/${r.content.type}/${r.content.tmdbId}`)}
|
||||
class="plain"
|
||||
>
|
||||
{r.content.title}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
TodoMoviesMovie,
|
||||
} from "@/types";
|
||||
import Icon from "@/lib/Icon.svelte";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
|
||||
let isDragOver = $state(false);
|
||||
let isLoading = $state(false);
|
||||
@@ -83,7 +84,7 @@
|
||||
data: r.result.toString(),
|
||||
type,
|
||||
};
|
||||
goto("/import/process");
|
||||
goto(toBaseUrl("/import/process"));
|
||||
}
|
||||
},
|
||||
false,
|
||||
@@ -255,7 +256,7 @@
|
||||
data: JSON.stringify(toImport),
|
||||
type: "movary",
|
||||
};
|
||||
goto("/import/process");
|
||||
goto(toBaseUrl("/import/process"));
|
||||
} catch (err) {
|
||||
isLoading = false;
|
||||
notify({ type: "error", text: "Failed to read files!" });
|
||||
@@ -331,7 +332,7 @@
|
||||
data: JSON.stringify(toImport),
|
||||
type: "watcharr",
|
||||
};
|
||||
goto("/import/process");
|
||||
goto(toBaseUrl("/import/process"));
|
||||
} catch (err) {
|
||||
isLoading = false;
|
||||
notify({ type: "error", text: "Failed to read file!" });
|
||||
@@ -380,7 +381,7 @@
|
||||
data: r.result.toString(),
|
||||
type: "myanimelist",
|
||||
};
|
||||
goto("/import/process");
|
||||
goto(toBaseUrl("/import/process"));
|
||||
}
|
||||
},
|
||||
false,
|
||||
@@ -539,7 +540,7 @@
|
||||
data: JSON.stringify(toImport),
|
||||
type: "ryot",
|
||||
};
|
||||
goto("/import/process");
|
||||
goto(toBaseUrl("/import/process"));
|
||||
} catch (err) {
|
||||
isLoading = false;
|
||||
notify({ type: "error", text: "Failed to read file!" });
|
||||
@@ -657,7 +658,7 @@
|
||||
type: "todomovies",
|
||||
};
|
||||
|
||||
goto("/import/process");
|
||||
goto(toBaseUrl("/import/process"));
|
||||
} catch (err) {
|
||||
isLoading = false;
|
||||
notify({ type: "error", text: "Failed to read files!" });
|
||||
@@ -667,7 +668,7 @@
|
||||
|
||||
onMount(() => {
|
||||
if (!localStorage.getItem("token")) {
|
||||
goto("/login");
|
||||
goto(toBaseUrl("/login"));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -702,7 +703,7 @@
|
||||
filesSelected={(f) => processFiles(f, "tmdb")}
|
||||
/>
|
||||
|
||||
<button class="plain" onclick={() => goto("/import/trakt")}>
|
||||
<button class="plain" onclick={() => goto(toBaseUrl("/import/trakt"))}>
|
||||
<Icon i="trakt" wh="100%" />
|
||||
<h4 class="norm">Trakt Import</h4>
|
||||
</button>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { onDestroy } from "svelte";
|
||||
import papa from "papaparse";
|
||||
import Status from "@/lib/Status.svelte";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
|
||||
interface ImportedListItemMultiProblem {
|
||||
original: ImportedList;
|
||||
@@ -58,7 +59,7 @@
|
||||
const list = store.importedList;
|
||||
if (!list) {
|
||||
console.log("import/process, no list, returning to /import");
|
||||
goto("/import");
|
||||
goto(toBaseUrl("/import"));
|
||||
return;
|
||||
}
|
||||
console.log("getList", list);
|
||||
@@ -431,14 +432,14 @@
|
||||
) {
|
||||
// Some items failed.. go to some-failed
|
||||
store.parsedImportedList = rList;
|
||||
goto("/import/some-failed");
|
||||
goto(toBaseUrl("/import/some-failed"));
|
||||
} else {
|
||||
notify({
|
||||
type: "success",
|
||||
text: "All content successfully imported! Try refreshing if you are missing data.",
|
||||
time: 15000,
|
||||
});
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,7 +678,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="btns">
|
||||
<button onclick={() => goto("/import")}><Icon i="arrow" />Back</button
|
||||
<button onclick={() => goto(toBaseUrl("/import"))}
|
||||
><Icon i="arrow" />Back</button
|
||||
>
|
||||
<button onclick={() => changeAllStatuses()} disabled={isImporting}>
|
||||
Change Statuses
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
import { store } from "@/store.svelte";
|
||||
import { ImportResponseType, type ImportedList } from "@/types";
|
||||
import { onMount } from "svelte";
|
||||
@@ -36,7 +37,7 @@
|
||||
}
|
||||
console.log("failedlen", failed.length);
|
||||
} else {
|
||||
goto("/import");
|
||||
goto(toBaseUrl("/import"));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import SyncModal from "./modals/SyncModal.svelte";
|
||||
import RegionDropDown from "@/lib/RegionDropDown.svelte";
|
||||
import RatingSetting from "@/lib/rating/RatingSetting.svelte";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
|
||||
let user = $derived(store.userInfo);
|
||||
let settings = $derived(store.userSettings);
|
||||
@@ -345,7 +346,7 @@
|
||||
<RatingSetting />
|
||||
|
||||
<div class="row btns">
|
||||
<button onclick={() => goto("/import")}>Import</button>
|
||||
<button onclick={() => goto(toBaseUrl("/import"))}>Import</button>
|
||||
<button onclick={() => downloadWatchedList()} disabled={exportDisabled}
|
||||
>Export</button
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { notify } from "@/lib/util/notify";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
import { store } from "@/store.svelte";
|
||||
import { UserPermission } from "@/types";
|
||||
import axios from "axios";
|
||||
@@ -32,7 +33,7 @@
|
||||
if (store.userInfo) {
|
||||
store.userInfo.permissions = UserPermission.PERM_ADMIN;
|
||||
}
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import Icon from "@/lib/Icon.svelte";
|
||||
import { afterNavigate, goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import { toBaseUrl } from "@/lib/util/url.js";
|
||||
|
||||
type GameWithMediaType = GameSearch & { media_type: "game" };
|
||||
type CombinedResult =
|
||||
@@ -278,7 +279,7 @@
|
||||
data[0],
|
||||
);
|
||||
|
||||
goto(`/game/${data[0].id}`);
|
||||
goto(toBaseUrl(`/game/${data[0].id}`));
|
||||
return;
|
||||
}
|
||||
allSearchResults.push(...data);
|
||||
@@ -290,7 +291,7 @@
|
||||
// assuming that people paste the id in, this should work
|
||||
// without the debounce going to an incomplete id.
|
||||
// Flesh out if anyone has issues.
|
||||
goto(`/${extProvider.provider}/${extProvider.id}`);
|
||||
goto(toBaseUrl(`/${extProvider.provider}/${extProvider.id}`));
|
||||
return;
|
||||
} else {
|
||||
// Else call tmdb `external id` endpoint
|
||||
@@ -324,7 +325,11 @@
|
||||
mediaType,
|
||||
);
|
||||
} else {
|
||||
goto(`/${data.results[0].media_type}/${data.results[0].id}`);
|
||||
goto(
|
||||
toBaseUrl(
|
||||
`/${data.results[0].media_type}/${data.results[0].id}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import RegionDropDown from "@/lib/RegionDropDown.svelte";
|
||||
import TaskScheduleModal from "./modals/TaskScheduleModal.svelte";
|
||||
import TrustedHeaderAuthModal from "./modals/TrustedHeaderAuthModal.svelte";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
|
||||
let serverConfig: ServerConfig | undefined = $state();
|
||||
let jellyfinOrEmby = $derived(serverConfig?.USE_EMBY ? "Emby" : "Jellyfin");
|
||||
@@ -108,7 +109,12 @@
|
||||
{#await getServerStats()}
|
||||
<Spinner />
|
||||
{:then stats}
|
||||
<Stat name="Users" value={stats.users} href="/manage_users" large />
|
||||
<Stat
|
||||
name="Users"
|
||||
value={stats.users}
|
||||
href={toBaseUrl("/manage_users")}
|
||||
large
|
||||
/>
|
||||
<Stat name="Private Users" value={stats.privateUsers} large />
|
||||
<Stat name="Watched Movies" value={stats.watchedMovies} large />
|
||||
<Stat name="Watched Shows" value={stats.watchedShows} large />
|
||||
@@ -118,14 +124,14 @@
|
||||
<Stat
|
||||
name="Most Watched Movie"
|
||||
value={stats.mostWatchedMovie.title}
|
||||
href="/movie/{stats.mostWatchedMovie.tmdbId}"
|
||||
href={toBaseUrl(`/movie/${stats.mostWatchedMovie.tmdbId}`)}
|
||||
/>
|
||||
{/if}
|
||||
{#if stats.mostWatchedShow?.title}
|
||||
<Stat
|
||||
name="Most Watched Show"
|
||||
value={stats.mostWatchedShow.title}
|
||||
href="/tv/{stats.mostWatchedShow.tmdbId}"
|
||||
href={toBaseUrl(`/tv/${stats.mostWatchedShow.tmdbId}`)}
|
||||
/>
|
||||
{/if}
|
||||
{:catch err}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { noAuthAxios } from "@/lib/util/api";
|
||||
import { onMount } from "svelte";
|
||||
import { notify, unNotify } from "@/lib/util/notify";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
|
||||
let error: string | undefined = $state();
|
||||
let login = $state(true);
|
||||
@@ -18,7 +19,7 @@
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem("token")) {
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
}
|
||||
|
||||
if (!error && page.url.searchParams.get("again")) {
|
||||
@@ -35,7 +36,7 @@
|
||||
if (r?.data) {
|
||||
if (r.data.isInSetup) {
|
||||
console.log("Server is in setup.. navigating to web setup page.");
|
||||
goto("/setup");
|
||||
goto(toBaseUrl("/setup"));
|
||||
}
|
||||
availableProviders = r.data.available;
|
||||
apHeader = availableProviders?.includes("header");
|
||||
@@ -83,7 +84,7 @@
|
||||
} else {
|
||||
localStorage.removeItem("useEmby");
|
||||
}
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
notify({ id: nid, text: `Welcome ${user}!`, type: "success" });
|
||||
}
|
||||
})
|
||||
@@ -120,7 +121,7 @@
|
||||
if (resp.data?.token) {
|
||||
console.log("Received token... logging in.");
|
||||
localStorage.setItem("token", resp.data.token);
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
notify({ id: nid, text: `Welcome!`, type: "success" });
|
||||
}
|
||||
})
|
||||
@@ -148,7 +149,7 @@
|
||||
if (resp.data?.token) {
|
||||
console.log("Received token... logging in.");
|
||||
localStorage.setItem("token", resp.data.token);
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
notify({ id: nid, text: `Welcome!`, type: "success" });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from "svelte/legacy";
|
||||
|
||||
import { goto } from "$app/navigation";
|
||||
import type { AvailableAuthProviders } from "@/types";
|
||||
import { noAuthAxios } from "@/lib/util/api";
|
||||
import { onMount } from "svelte";
|
||||
import { notify, unNotify } from "@/lib/util/notify";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
|
||||
let error: string = $state();
|
||||
let error: string | undefined = $state();
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem("token")) {
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
}
|
||||
|
||||
noAuthAxios.get<AvailableAuthProviders>("/auth/available").then((r) => {
|
||||
if (r?.data) {
|
||||
if (!r?.data?.isInSetup) {
|
||||
console.log("Server not in setup.. navigating to login page.");
|
||||
goto("/login");
|
||||
goto(toBaseUrl("/login"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleLogin(ev: SubmitEvent) {
|
||||
ev.preventDefault();
|
||||
const fd = new FormData(ev.target! as HTMLFormElement);
|
||||
const user = fd.get("username");
|
||||
const pass = fd.get("password");
|
||||
@@ -44,7 +44,7 @@
|
||||
if (resp.data?.token) {
|
||||
console.log("Received token... logging in.");
|
||||
localStorage.setItem("token", resp.data.token);
|
||||
goto("/");
|
||||
goto(toBaseUrl("/"));
|
||||
notify({ id: nid, text: `Welcome ${user}!`, type: "success" });
|
||||
}
|
||||
})
|
||||
@@ -74,7 +74,7 @@
|
||||
<span class="error">{error}!</span>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={preventDefault(handleLogin)}>
|
||||
<form onsubmit={handleLogin}>
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" placeholder="Username" />
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import { toBaseUrl } from "@/lib/util/url";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -16,7 +17,7 @@
|
||||
<h4 class="norm">We couldn't load this page</h4>
|
||||
{/if}
|
||||
<div class="btns">
|
||||
<button onclick={() => goto("/")}>Home</button>
|
||||
<button onclick={() => goto(toBaseUrl("/"))}>Home</button>
|
||||
<button onclick={() => location.reload()}>Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,14 @@ import adapter from "@sveltejs/adapter-node";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import { sveltePreprocess } from "svelte-preprocess";
|
||||
|
||||
if (
|
||||
process.env.WATCHARR_BASE &&
|
||||
(!process.env.WATCHARR_BASE.startsWith("/") ||
|
||||
process.env.WATCHARR_BASE.endsWith("/"))
|
||||
) {
|
||||
throw new Error("WATCHARR_BASE must start with, but not end with '/'");
|
||||
}
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
@@ -21,6 +29,11 @@ const config = {
|
||||
alias: {
|
||||
"@": "src",
|
||||
},
|
||||
|
||||
paths: {
|
||||
base: process.env.WATCHARR_BASE,
|
||||
relative: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user