mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 18:59:28 -05:00
Compare commits
6 Commits
preview-mo
...
test-exter
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b133d5a8a6 | ||
|
|
64f4610b9f | ||
|
|
2d3b777daf | ||
|
|
cf59102ef9 | ||
|
|
ca838a00fa | ||
|
|
f2ed101e52 |
@@ -18,7 +18,7 @@ config/logs/*
|
||||
config/*.json
|
||||
dist
|
||||
Dockerfile*
|
||||
docker-compose.yml
|
||||
compose.yaml
|
||||
docs
|
||||
LICENSE
|
||||
node_modules
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -40,7 +40,7 @@ docs export-ignore
|
||||
.all-contributorsrc export-ignore
|
||||
.editorconfig export-ignore
|
||||
Dockerfile.local export-ignore
|
||||
docker-compose.yml export-ignore
|
||||
compose.yaml export-ignore
|
||||
stylelint.config.js export-ignore
|
||||
|
||||
public/os_logo_filled.png export-ignore
|
||||
|
||||
@@ -52,7 +52,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
|
||||
5. Create your patch and test your changes.
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
jellyseerr:
|
||||
build:
|
||||
@@ -190,7 +190,7 @@ Caddy will automatically obtain and renew SSL certificates for your domain.
|
||||
|
||||
## Traefik (v2)
|
||||
|
||||
Add the following labels to the Jellyseerr service in your `docker-compose.yml` file:
|
||||
Add the following labels to the Jellyseerr service in your `compose.yaml` file:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
|
||||
@@ -71,7 +71,7 @@ You could also use [diun](https://github.com/crazy-max/diun) to receive notifica
|
||||
For details on how to use Docker Compose, please [review the official Compose documentation](https://docs.docker.com/compose/reference/).
|
||||
|
||||
#### Installation:
|
||||
Define the `jellyseerr` service in your `docker-compose.yml` as follows:
|
||||
Define the `jellyseerr` service in your `compose.yaml` as follows:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
@@ -94,17 +94,17 @@ If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable
|
||||
|
||||
Then, start all services defined in the Compose file:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Updating:
|
||||
Pull the latest image:
|
||||
```bash
|
||||
docker-compose pull jellyseerr
|
||||
docker compose pull jellyseerr
|
||||
```
|
||||
Then, restart all services defined in the Compose file:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
:::tip
|
||||
You may alternatively use a third-party mechanism like [dockge](https://github.com/louislam/dockge) to manage your docker compose files.
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"@svgr/webpack": "6.5.1",
|
||||
"@tanem/react-nprogress": "5.0.30",
|
||||
"ace-builds": "1.15.2",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "5.1.0",
|
||||
"bowser": "2.11.0",
|
||||
"connect-typeorm": "1.1.4",
|
||||
|
||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
||||
ace-builds:
|
||||
specifier: 1.15.2
|
||||
version: 1.15.2
|
||||
axios:
|
||||
specifier: ^1.7.7
|
||||
version: 1.7.7
|
||||
bcrypt:
|
||||
specifier: 5.1.0
|
||||
version: 5.1.0(encoding@0.1.13)
|
||||
@@ -3402,6 +3405,9 @@ packages:
|
||||
resolution: {integrity: sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axios@1.7.7:
|
||||
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
|
||||
|
||||
axobject-query@3.1.1:
|
||||
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
|
||||
|
||||
@@ -5022,6 +5028,15 @@ packages:
|
||||
fn.name@1.1.0:
|
||||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||
|
||||
follow-redirects@1.15.9:
|
||||
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
for-each@0.3.3:
|
||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||
|
||||
@@ -5036,6 +5051,10 @@ packages:
|
||||
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
|
||||
engines: {node: '>= 0.12'}
|
||||
|
||||
form-data@4.0.1:
|
||||
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formik@2.4.6:
|
||||
resolution: {integrity: sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==}
|
||||
peerDependencies:
|
||||
@@ -7384,6 +7403,9 @@ packages:
|
||||
proxy-from-env@1.0.0:
|
||||
resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
psl@1.9.0:
|
||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||
|
||||
@@ -13263,6 +13285,14 @@ snapshots:
|
||||
|
||||
axe-core@4.9.1: {}
|
||||
|
||||
axios@1.7.7:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9
|
||||
form-data: 4.0.1
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axobject-query@3.1.1:
|
||||
dependencies:
|
||||
deep-equal: 2.2.3
|
||||
@@ -15278,6 +15308,8 @@ snapshots:
|
||||
|
||||
fn.name@1.1.0: {}
|
||||
|
||||
follow-redirects@1.15.9: {}
|
||||
|
||||
for-each@0.3.3:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
@@ -15295,6 +15327,12 @@ snapshots:
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@4.0.1:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
formik@2.4.6(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/hoist-non-react-statics': 3.3.5
|
||||
@@ -17842,6 +17880,8 @@ snapshots:
|
||||
|
||||
proxy-from-env@1.0.0: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
psl@1.9.0: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
// import rateLimit from 'axios-rate-limit';
|
||||
import type NodeCache from 'node-cache';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
@@ -11,87 +12,71 @@ const DEFAULT_ROLLING_BUFFER = 10000;
|
||||
interface ExternalAPIOptions {
|
||||
nodeCache?: NodeCache;
|
||||
headers?: Record<string, unknown>;
|
||||
rateLimit?: RateLimitOptions;
|
||||
rateLimit?: {
|
||||
maxRPS: number;
|
||||
maxRequests: number;
|
||||
};
|
||||
}
|
||||
|
||||
class ExternalAPI {
|
||||
protected fetch: typeof fetch;
|
||||
protected params: Record<string, string>;
|
||||
protected defaultHeaders: { [key: string]: string };
|
||||
protected axios: AxiosInstance;
|
||||
private baseUrl: string;
|
||||
private cache?: NodeCache;
|
||||
|
||||
constructor(
|
||||
baseUrl: string,
|
||||
params: Record<string, string> = {},
|
||||
params: Record<string, unknown>,
|
||||
options: ExternalAPIOptions = {}
|
||||
) {
|
||||
if (options.rateLimit) {
|
||||
this.fetch = rateLimit(fetch, options.rateLimit);
|
||||
} else {
|
||||
this.fetch = fetch;
|
||||
}
|
||||
this.axios = axios.create({
|
||||
baseURL: baseUrl,
|
||||
params,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
// if (options.rateLimit) {
|
||||
// this.axios = rateLimit(this.axios, {
|
||||
// maxRequests: options.rateLimit.maxRequests,
|
||||
// maxRPS: options.rateLimit.maxRPS,
|
||||
// });
|
||||
// }
|
||||
|
||||
this.baseUrl = baseUrl;
|
||||
this.params = params;
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
this.cache = options.nodeCache;
|
||||
}
|
||||
|
||||
protected async get<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const data = await this.getDataFromResponse(response);
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
|
||||
if (this.cache && ttl !== 0) {
|
||||
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return data;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async post<T>(
|
||||
endpoint: string,
|
||||
data?: Record<string, unknown>,
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: { ...this.params, ...params },
|
||||
config: config?.params,
|
||||
data,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
@@ -99,43 +84,23 @@ class ExternalAPI {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
method: 'POST',
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const resData = await this.getDataFromResponse(response);
|
||||
const response = await this.axios.post<T>(endpoint, data, config);
|
||||
|
||||
if (this.cache && ttl !== 0) {
|
||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return resData;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async put<T>(
|
||||
endpoint: string,
|
||||
data: Record<string, unknown>,
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: { ...this.params, ...params },
|
||||
config: config?.params,
|
||||
data,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
@@ -143,73 +108,41 @@ class ExternalAPI {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
method: 'PUT',
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const resData = await this.getDataFromResponse(response);
|
||||
const response = await this.axios.put<T>(endpoint, data, config);
|
||||
|
||||
if (this.cache && ttl !== 0) {
|
||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return resData;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async delete<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
config?: RequestInit
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
method: 'DELETE',
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
}
|
||||
const data = await this.getDataFromResponse(response);
|
||||
|
||||
return data;
|
||||
const response = await this.axios.delete<T>(endpoint, config);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async getRolling<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit,
|
||||
overwriteBaseUrl?: string
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
|
||||
if (cachedItem) {
|
||||
@@ -220,78 +153,20 @@ class ExternalAPI {
|
||||
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
||||
Date.now() - DEFAULT_ROLLING_BUFFER
|
||||
) {
|
||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||
this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
}).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${
|
||||
text ? ': ' + text : ''
|
||||
}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const data = await this.getDataFromResponse(response);
|
||||
this.cache?.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||
this.axios.get<T>(endpoint, config).then((response) => {
|
||||
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
});
|
||||
}
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const data = await this.getDataFromResponse(response);
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private formatUrl(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
overwriteBaseUrl?: string
|
||||
): string {
|
||||
const baseUrl = overwriteBaseUrl || this.baseUrl;
|
||||
const href =
|
||||
baseUrl +
|
||||
(baseUrl.endsWith('/') ? '' : '/') +
|
||||
(endpoint.startsWith('/') ? endpoint.slice(1) : endpoint);
|
||||
const searchParams = new URLSearchParams({
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
return (
|
||||
href +
|
||||
(searchParams.toString().length
|
||||
? '?' + searchParams.toString()
|
||||
: searchParams.toString())
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private serializeCacheKey(
|
||||
@@ -304,29 +179,6 @@ class ExternalAPI {
|
||||
|
||||
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
||||
}
|
||||
|
||||
private async getDataFromResponse(response: Response) {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return await response.json();
|
||||
} else if (
|
||||
contentType?.includes('application/xml') ||
|
||||
contentType?.includes('text/html') ||
|
||||
contentType?.includes('text/plain')
|
||||
) {
|
||||
return await response.text();
|
||||
} else {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
try {
|
||||
return await response.blob();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ExternalAPI;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import logger from '@server/logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface GitHubRelease {
|
||||
url: string;
|
||||
@@ -67,6 +67,10 @@ class GithubAPI extends ExternalAPI {
|
||||
'https://api.github.com',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('github').data,
|
||||
}
|
||||
);
|
||||
@@ -81,7 +85,9 @@ class GithubAPI extends ExternalAPI {
|
||||
const data = await this.get<GitHubRelease[]>(
|
||||
'/repos/fallenbagel/jellyseerr/releases',
|
||||
{
|
||||
per_page: take.toString(),
|
||||
params: {
|
||||
per_page: take,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -106,8 +112,10 @@ class GithubAPI extends ExternalAPI {
|
||||
const data = await this.get<GithubCommit[]>(
|
||||
'/repos/fallenbagel/jellyseerr/commits',
|
||||
{
|
||||
per_page: take.toString(),
|
||||
branch,
|
||||
params: {
|
||||
per_page: take,
|
||||
branch,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -129,8 +129,6 @@ class JellyfinAPI extends ExternalAPI {
|
||||
Username,
|
||||
Pw: Password,
|
||||
},
|
||||
{},
|
||||
undefined,
|
||||
{ headers }
|
||||
);
|
||||
};
|
||||
@@ -293,13 +291,15 @@ class JellyfinAPI extends ExternalAPI {
|
||||
const libraryItemsResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items`,
|
||||
{
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Series,Movie,Others',
|
||||
Recursive: 'true',
|
||||
StartIndex: '0',
|
||||
ParentId: id,
|
||||
collapseBoxSetItems: 'false',
|
||||
params: {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Series,Movie,Others',
|
||||
Recursive: 'true',
|
||||
StartIndex: '0',
|
||||
ParentId: id,
|
||||
collapseBoxSetItems: 'false',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -321,8 +321,10 @@ class JellyfinAPI extends ExternalAPI {
|
||||
const itemResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items/Latest`,
|
||||
{
|
||||
Limit: '12',
|
||||
ParentId: id,
|
||||
params: {
|
||||
Limit: '12',
|
||||
ParentId: id,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -384,7 +386,9 @@ class JellyfinAPI extends ExternalAPI {
|
||||
const episodeResponse = await this.get<any>(
|
||||
`/Shows/${seriesID}/Episodes`,
|
||||
{
|
||||
seasonId: seasonID,
|
||||
params: {
|
||||
seasonId: seasonID,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -410,7 +414,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
).AccessToken;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while creating an API key the Jellyfin server: ${e.message}`,
|
||||
`Something went wrong while creating an API key from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import xml2js from 'xml2js';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface PlexAccountResponse {
|
||||
user: PlexUser;
|
||||
@@ -137,6 +137,8 @@ class PlexTvAPI extends ExternalAPI {
|
||||
{
|
||||
headers: {
|
||||
'X-Plex-Token': authToken,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('plextv').data,
|
||||
}
|
||||
@@ -147,11 +149,15 @@ class PlexTvAPI extends ExternalAPI {
|
||||
|
||||
public async getDevices(): Promise<PlexDevice[]> {
|
||||
try {
|
||||
const devicesResp = await this.get('/api/resources', {
|
||||
includeHttps: '1',
|
||||
});
|
||||
const devicesResp = await this.axios.get(
|
||||
'/api/resources?includeHttps=1',
|
||||
{
|
||||
transformResponse: [],
|
||||
responseType: 'text',
|
||||
}
|
||||
);
|
||||
const parsedXml = await xml2js.parseStringPromise(
|
||||
devicesResp as DeviceResponse
|
||||
devicesResp.data as DeviceResponse
|
||||
);
|
||||
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
||||
name: pxml.$.name,
|
||||
@@ -199,11 +205,11 @@ class PlexTvAPI extends ExternalAPI {
|
||||
|
||||
public async getUser(): Promise<PlexUser> {
|
||||
try {
|
||||
const account = await this.get<PlexAccountResponse>(
|
||||
const account = await this.axios.get<PlexAccountResponse>(
|
||||
'/users/account.json'
|
||||
);
|
||||
|
||||
return account.user;
|
||||
return account.data.user;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from plex.tv: ${e.message}`,
|
||||
@@ -243,10 +249,13 @@ class PlexTvAPI extends ExternalAPI {
|
||||
}
|
||||
|
||||
public async getUsers(): Promise<UsersResponse> {
|
||||
const data = await this.get('/api/users');
|
||||
const response = await this.axios.get('/api/users', {
|
||||
transformResponse: [],
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
const parsedXml = (await xml2js.parseStringPromise(
|
||||
data as string
|
||||
response.data
|
||||
)) as UsersResponse;
|
||||
return parsedXml;
|
||||
}
|
||||
@@ -261,49 +270,49 @@ class PlexTvAPI extends ExternalAPI {
|
||||
items: PlexWatchlistItem[];
|
||||
}> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
'X-Plex-Container-Start': offset.toString(),
|
||||
'X-Plex-Container-Size': size.toString(),
|
||||
});
|
||||
const response = await this.fetch(
|
||||
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
|
||||
const response = await this.axios.get<WatchlistResponse>(
|
||||
'/library/sections/watchlist/all',
|
||||
{
|
||||
headers: this.defaultHeaders,
|
||||
params: {
|
||||
'X-Plex-Container-Start': offset,
|
||||
'X-Plex-Container-Size': size,
|
||||
},
|
||||
baseURL: 'https://metadata.provider.plex.tv',
|
||||
}
|
||||
);
|
||||
const data = (await response.json()) as WatchlistResponse;
|
||||
|
||||
const watchlistDetails = await Promise.all(
|
||||
(data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
'https://metadata.provider.plex.tv'
|
||||
);
|
||||
(response.data.MediaContainer.Metadata ?? []).map(
|
||||
async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{
|
||||
baseURL: 'https://metadata.provider.plex.tv',
|
||||
}
|
||||
);
|
||||
|
||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||
|
||||
const tmdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tmdb')
|
||||
);
|
||||
const tvdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tvdb')
|
||||
);
|
||||
const tmdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tmdb')
|
||||
);
|
||||
const tvdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tvdb')
|
||||
);
|
||||
|
||||
return {
|
||||
ratingKey: metadata.ratingKey,
|
||||
// This should always be set? But I guess it also cannot be?
|
||||
// We will filter out the 0's afterwards
|
||||
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||
tvdbId: tvdbString
|
||||
? Number(tvdbString.id.split('//')[1])
|
||||
: undefined,
|
||||
title: metadata.title,
|
||||
type: metadata.type,
|
||||
};
|
||||
})
|
||||
return {
|
||||
ratingKey: metadata.ratingKey,
|
||||
// This should always be set? But I guess it also cannot be?
|
||||
// We will filter out the 0's afterwards
|
||||
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||
tvdbId: tvdbString
|
||||
? Number(tvdbString.id.split('//')[1])
|
||||
: undefined,
|
||||
title: metadata.title,
|
||||
type: metadata.type,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
||||
@@ -311,7 +320,7 @@ class PlexTvAPI extends ExternalAPI {
|
||||
return {
|
||||
offset,
|
||||
size,
|
||||
totalSize: data.MediaContainer.totalSize,
|
||||
totalSize: response.data.MediaContainer.totalSize,
|
||||
items: filteredList,
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface PushoverSoundsResponse {
|
||||
sounds: {
|
||||
@@ -26,13 +26,24 @@ export const mapSounds = (sounds: {
|
||||
|
||||
class PushoverAPI extends ExternalAPI {
|
||||
constructor() {
|
||||
super('https://api.pushover.net/1');
|
||||
super(
|
||||
'https://api.pushover.net/1',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async getSounds(appToken: string): Promise<PushoverSound[]> {
|
||||
try {
|
||||
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
|
||||
token: appToken,
|
||||
params: {
|
||||
token: appToken,
|
||||
},
|
||||
});
|
||||
|
||||
return mapSounds(data.sounds);
|
||||
|
||||
@@ -160,7 +160,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
const data = await this.get<QueueResponse<QueueItemAppendT>>(
|
||||
`/queue`,
|
||||
{
|
||||
includeEpisode: 'true',
|
||||
params: {
|
||||
includeEpisode: 'true',
|
||||
},
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
@@ -58,7 +58,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
|
||||
try {
|
||||
const data = await this.get<RadarrMovie[]>('/movie/lookup', {
|
||||
term: `tmdb:${id}`,
|
||||
params: {
|
||||
term: `tmdb:${id}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data[0]) {
|
||||
@@ -222,8 +224,10 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
try {
|
||||
const { id, title } = await this.getMovieByTmdbId(movieId);
|
||||
await this.delete(`/movie/${id}`, {
|
||||
deleteFiles: 'true',
|
||||
addImportExclusion: 'false',
|
||||
params: {
|
||||
deleteFiles: 'true',
|
||||
addImportExclusion: 'false',
|
||||
},
|
||||
});
|
||||
logger.info(`[Radarr] Removed movie ${title}`);
|
||||
} catch (e) {
|
||||
|
||||
@@ -138,7 +138,9 @@ class SonarrAPI extends ServarrBase<{
|
||||
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
||||
try {
|
||||
const data = await this.get<SonarrSeries[]>('/series/lookup', {
|
||||
term: title,
|
||||
params: {
|
||||
term: title,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data[0]) {
|
||||
@@ -159,7 +161,9 @@ class SonarrAPI extends ServarrBase<{
|
||||
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
|
||||
try {
|
||||
const data = await this.get<SonarrSeries[]>('/series/lookup', {
|
||||
term: `tvdb:${id}`,
|
||||
params: {
|
||||
term: `tvdb:${id}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data[0]) {
|
||||
@@ -345,8 +349,10 @@ class SonarrAPI extends ServarrBase<{
|
||||
try {
|
||||
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
||||
await this.delete(`/series/${id}`, {
|
||||
deleteFiles: 'true',
|
||||
addImportExclusion: 'false',
|
||||
params: {
|
||||
deleteFiles: 'true',
|
||||
addImportExclusion: 'false',
|
||||
},
|
||||
});
|
||||
logger.info(`[Radarr] Removed serie ${title}`);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import type { User } from '@server/entity/User';
|
||||
import type { TautulliSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { uniqWith } from 'lodash';
|
||||
|
||||
export interface TautulliHistoryRecord {
|
||||
@@ -112,25 +113,25 @@ interface TautulliInfoResponse {
|
||||
};
|
||||
}
|
||||
|
||||
class TautulliAPI extends ExternalAPI {
|
||||
class TautulliAPI {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor(settings: TautulliSettings) {
|
||||
super(
|
||||
`${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||
this.axios = axios.create({
|
||||
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||
settings.port
|
||||
}${settings.urlBase ?? ''}`,
|
||||
{
|
||||
apikey: settings.apiKey || '',
|
||||
}
|
||||
);
|
||||
params: { apikey: settings.apiKey },
|
||||
});
|
||||
}
|
||||
|
||||
public async getInfo(): Promise<TautulliInfo> {
|
||||
try {
|
||||
return (
|
||||
await this.get<TautulliInfoResponse>('/api/v2', {
|
||||
cmd: 'get_tautulli_info',
|
||||
await this.axios.get<TautulliInfoResponse>('/api/v2', {
|
||||
params: { cmd: 'get_tautulli_info' },
|
||||
})
|
||||
).response.data;
|
||||
).data.response.data;
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong fetching Tautulli server info', {
|
||||
label: 'Tautulli API',
|
||||
@@ -147,12 +148,14 @@ class TautulliAPI extends ExternalAPI {
|
||||
): Promise<TautulliWatchStats[]> {
|
||||
try {
|
||||
return (
|
||||
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
cmd: 'get_item_watch_time_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: '1',
|
||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_item_watch_time_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: 1,
|
||||
},
|
||||
})
|
||||
).response.data;
|
||||
).data.response.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching media watch stats from Tautulli',
|
||||
@@ -173,12 +176,14 @@ class TautulliAPI extends ExternalAPI {
|
||||
): Promise<TautulliWatchUser[]> {
|
||||
try {
|
||||
return (
|
||||
await this.get<TautulliWatchUsersResponse>('/api/v2', {
|
||||
cmd: 'get_item_user_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: '1',
|
||||
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_item_user_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: 1,
|
||||
},
|
||||
})
|
||||
).response.data;
|
||||
).data.response.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching media watch users from Tautulli',
|
||||
@@ -201,13 +206,15 @@ class TautulliAPI extends ExternalAPI {
|
||||
}
|
||||
|
||||
return (
|
||||
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
cmd: 'get_user_watch_time_stats',
|
||||
user_id: user.plexId.toString(),
|
||||
query_days: '0',
|
||||
grouping: '1',
|
||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_user_watch_time_stats',
|
||||
user_id: user.plexId,
|
||||
query_days: 0,
|
||||
grouping: 1,
|
||||
},
|
||||
})
|
||||
).response.data[0];
|
||||
).data.response.data[0];
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching user watch stats from Tautulli',
|
||||
@@ -238,17 +245,19 @@ class TautulliAPI extends ExternalAPI {
|
||||
|
||||
while (results.length < 20) {
|
||||
const tautulliData = (
|
||||
await this.get<TautulliHistoryResponse>('/api/v2', {
|
||||
cmd: 'get_history',
|
||||
grouping: '1',
|
||||
order_column: 'date',
|
||||
order_dir: 'desc',
|
||||
user_id: user.plexId.toString(),
|
||||
media_type: 'movie,episode',
|
||||
length: take.toString(),
|
||||
start: start.toString(),
|
||||
await this.axios.get<TautulliHistoryResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_history',
|
||||
grouping: 1,
|
||||
order_column: 'date',
|
||||
order_dir: 'desc',
|
||||
user_id: user.plexId,
|
||||
media_type: 'movie,episode',
|
||||
length: take,
|
||||
start,
|
||||
},
|
||||
})
|
||||
).response.data.data;
|
||||
).data.response.data.data;
|
||||
|
||||
if (!tautulliData.length) {
|
||||
return results;
|
||||
|
||||
@@ -112,10 +112,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
rateLimit: {
|
||||
maxRPS: 50,
|
||||
id: 'tmdb',
|
||||
},
|
||||
// rateLimit: {
|
||||
// maxRPS: 50,
|
||||
// id: 'tmdb',
|
||||
// },
|
||||
}
|
||||
);
|
||||
this.region = region;
|
||||
@@ -130,10 +130,12 @@ class TheMovieDb extends ExternalAPI {
|
||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
params: {
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -156,11 +158,13 @@ class TheMovieDb extends ExternalAPI {
|
||||
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
primary_release_year: year?.toString() || '',
|
||||
params: {
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
primary_release_year: year?.toString() || '',
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -183,11 +187,13 @@ class TheMovieDb extends ExternalAPI {
|
||||
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
first_air_date_year: year?.toString() || '',
|
||||
params: {
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
first_air_date_year: year?.toString() || '',
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -210,7 +216,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
}): Promise<TmdbPersonDetails> => {
|
||||
try {
|
||||
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
|
||||
language,
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -230,7 +238,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbPersonCombinedCredits>(
|
||||
`/person/${personId}/combined_credits`,
|
||||
{
|
||||
language,
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -253,9 +263,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbMovieDetails>(
|
||||
`/movie/${movieId}`,
|
||||
{
|
||||
language,
|
||||
append_to_response:
|
||||
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
||||
},
|
||||
},
|
||||
43200
|
||||
);
|
||||
@@ -277,9 +289,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbTvDetails>(
|
||||
`/tv/${tvId}`,
|
||||
{
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||
},
|
||||
},
|
||||
43200
|
||||
);
|
||||
@@ -303,8 +317,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSeasonWithEpisodes>(
|
||||
`/tv/${tvId}/season/${seasonNumber}`,
|
||||
{
|
||||
language: language || '',
|
||||
append_to_response: 'external_ids',
|
||||
params: {
|
||||
language: language || '',
|
||||
append_to_response: 'external_ids',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -327,8 +343,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/recommendations`,
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
params: {
|
||||
page: page.toString(),
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -351,8 +369,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/similar`,
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
params: {
|
||||
page: page.toString(),
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -375,8 +395,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/keyword/${keywordId}/movies`,
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
params: {
|
||||
page: page.toString(),
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -399,8 +421,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/tv/${tvId}/recommendations`,
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
params: {
|
||||
page: page.toString(),
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -423,8 +447,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
|
||||
page: page.toString(),
|
||||
language,
|
||||
params: {
|
||||
page: page.toString(),
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -465,38 +491,40 @@ class TheMovieDb extends ExternalAPI {
|
||||
.split('T')[0];
|
||||
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||
sort_by: sortBy,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
region: this.region || '',
|
||||
with_original_language:
|
||||
originalLanguage && originalLanguage !== 'all'
|
||||
? originalLanguage
|
||||
: originalLanguage === 'all'
|
||||
? ''
|
||||
: this.originalLanguage || '',
|
||||
// Set our release date values, but check if one is set and not the other,
|
||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||
'primary_release_date.gte':
|
||||
!primaryReleaseDateGte && primaryReleaseDateLte
|
||||
? defaultPastDate
|
||||
: primaryReleaseDateGte || '',
|
||||
'primary_release_date.lte':
|
||||
!primaryReleaseDateLte && primaryReleaseDateGte
|
||||
? defaultFutureDate
|
||||
: primaryReleaseDateLte || '',
|
||||
with_genres: genre || '',
|
||||
with_companies: studio || '',
|
||||
with_keywords: keywords || '',
|
||||
'with_runtime.gte': withRuntimeGte || '',
|
||||
'with_runtime.lte': withRuntimeLte || '',
|
||||
'vote_average.gte': voteAverageGte || '',
|
||||
'vote_average.lte': voteAverageLte || '',
|
||||
'vote_count.gte': voteCountGte || '',
|
||||
'vote_count.lte': voteCountLte || '',
|
||||
watch_region: watchRegion || '',
|
||||
with_watch_providers: watchProviders || '',
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
region: this.region || '',
|
||||
with_original_language:
|
||||
originalLanguage && originalLanguage !== 'all'
|
||||
? originalLanguage
|
||||
: originalLanguage === 'all'
|
||||
? ''
|
||||
: this.originalLanguage || '',
|
||||
// Set our release date values, but check if one is set and not the other,
|
||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||
'primary_release_date.gte':
|
||||
!primaryReleaseDateGte && primaryReleaseDateLte
|
||||
? defaultPastDate
|
||||
: primaryReleaseDateGte || '',
|
||||
'primary_release_date.lte':
|
||||
!primaryReleaseDateLte && primaryReleaseDateGte
|
||||
? defaultFutureDate
|
||||
: primaryReleaseDateLte || '',
|
||||
with_genres: genre || '',
|
||||
with_companies: studio || '',
|
||||
with_keywords: keywords || '',
|
||||
'with_runtime.gte': withRuntimeGte || '',
|
||||
'with_runtime.lte': withRuntimeLte || '',
|
||||
'vote_average.gte': voteAverageGte || '',
|
||||
'vote_average.lte': voteAverageLte || '',
|
||||
'vote_count.gte': voteCountGte || '',
|
||||
'vote_count.lte': voteCountLte || '',
|
||||
watch_region: watchRegion || '',
|
||||
with_watch_providers: watchProviders || '',
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -538,41 +566,43 @@ class TheMovieDb extends ExternalAPI {
|
||||
.split('T')[0];
|
||||
|
||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||
sort_by: sortBy,
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
// Set our release date values, but check if one is set and not the other,
|
||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||
'first_air_date.gte':
|
||||
!firstAirDateGte && firstAirDateLte
|
||||
? defaultPastDate
|
||||
: firstAirDateGte || '',
|
||||
'first_air_date.lte':
|
||||
!firstAirDateLte && firstAirDateGte
|
||||
? defaultFutureDate
|
||||
: firstAirDateLte || '',
|
||||
with_original_language:
|
||||
originalLanguage && originalLanguage !== 'all'
|
||||
? originalLanguage
|
||||
: originalLanguage === 'all'
|
||||
? ''
|
||||
: this.originalLanguage || '',
|
||||
include_null_first_air_dates: includeEmptyReleaseDate
|
||||
? 'true'
|
||||
: 'false',
|
||||
with_genres: genre || '',
|
||||
with_networks: network?.toString() || '',
|
||||
with_keywords: keywords || '',
|
||||
'with_runtime.gte': withRuntimeGte || '',
|
||||
'with_runtime.lte': withRuntimeLte || '',
|
||||
'vote_average.gte': voteAverageGte || '',
|
||||
'vote_average.lte': voteAverageLte || '',
|
||||
'vote_count.gte': voteCountGte || '',
|
||||
'vote_count.lte': voteCountLte || '',
|
||||
with_watch_providers: watchProviders || '',
|
||||
watch_region: watchRegion || '',
|
||||
with_status: withStatus || '',
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
// Set our release date values, but check if one is set and not the other,
|
||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||
'first_air_date.gte':
|
||||
!firstAirDateGte && firstAirDateLte
|
||||
? defaultPastDate
|
||||
: firstAirDateGte || '',
|
||||
'first_air_date.lte':
|
||||
!firstAirDateLte && firstAirDateGte
|
||||
? defaultFutureDate
|
||||
: firstAirDateLte || '',
|
||||
with_original_language:
|
||||
originalLanguage && originalLanguage !== 'all'
|
||||
? originalLanguage
|
||||
: originalLanguage === 'all'
|
||||
? ''
|
||||
: this.originalLanguage || '',
|
||||
include_null_first_air_dates: includeEmptyReleaseDate
|
||||
? 'true'
|
||||
: 'false',
|
||||
with_genres: genre || '',
|
||||
with_networks: network?.toString() || '',
|
||||
with_keywords: keywords || '',
|
||||
'with_runtime.gte': withRuntimeGte || '',
|
||||
'with_runtime.lte': withRuntimeLte || '',
|
||||
'vote_average.gte': voteAverageGte || '',
|
||||
'vote_average.lte': voteAverageLte || '',
|
||||
'vote_count.gte': voteCountGte || '',
|
||||
'vote_count.lte': voteCountLte || '',
|
||||
with_watch_providers: watchProviders || '',
|
||||
watch_region: watchRegion || '',
|
||||
with_status: withStatus || '',
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -592,10 +622,12 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
||||
'/movie/upcoming',
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
originalLanguage: this.originalLanguage || '',
|
||||
params: {
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
originalLanguage: this.originalLanguage || '',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -618,9 +650,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMultiResponse>(
|
||||
`/trending/all/${timeWindow}`,
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
params: {
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -641,7 +675,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/trending/movie/${timeWindow}`,
|
||||
{
|
||||
page: page.toString(),
|
||||
params: {
|
||||
page: page.toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -662,7 +698,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/trending/tv/${timeWindow}`,
|
||||
{
|
||||
page: page.toString(),
|
||||
params: {
|
||||
page: page.toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -691,8 +729,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbExternalIdResponse>(
|
||||
`/find/${externalId}`,
|
||||
{
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
params: {
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -782,7 +822,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbCollection>(
|
||||
`/collection/${collectionId}`,
|
||||
{
|
||||
language,
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -855,7 +897,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/movie/list',
|
||||
{
|
||||
language,
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -867,7 +911,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const englishData = await this.get<TmdbGenresResult>(
|
||||
'/genre/movie/list',
|
||||
{
|
||||
language: 'en',
|
||||
params: {
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -902,7 +948,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/tv/list',
|
||||
{
|
||||
language,
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -914,7 +962,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const englishData = await this.get<TmdbGenresResult>(
|
||||
'/genre/tv/list',
|
||||
{
|
||||
language: 'en',
|
||||
params: {
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -969,8 +1019,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbKeywordSearchResponse>(
|
||||
'/search/keyword',
|
||||
{
|
||||
query,
|
||||
page: page.toString(),
|
||||
params: {
|
||||
query,
|
||||
page: page.toString(),
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -992,8 +1044,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbCompanySearchResponse>(
|
||||
'/search/company',
|
||||
{
|
||||
query,
|
||||
page: page.toString(),
|
||||
params: {
|
||||
query,
|
||||
page: page.toString(),
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1013,7 +1067,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
|
||||
'/watch/providers/regions',
|
||||
{
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
params: {
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1037,8 +1093,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||
'/watch/providers/movie',
|
||||
{
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
watch_region: watchRegion,
|
||||
params: {
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
watch_region: watchRegion,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1062,8 +1120,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||
'/watch/providers/tv',
|
||||
{
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
watch_region: watchRegion,
|
||||
params: {
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
watch_region: watchRegion,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
@@ -27,8 +27,14 @@ const migrateApiTokens = async (settings: any): Promise<AllSettings> => {
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
try {
|
||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to create Jellyfin API token from admin account. Please check your network configuration or edit your settings.json by adding an 'apiKey' field inside of the 'jellyfin' section to fix this issue."
|
||||
);
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import fs from 'fs/promises';
|
||||
@@ -45,10 +44,20 @@ export const runMigrations = async (
|
||||
}
|
||||
migrated = newSettings;
|
||||
} catch (e) {
|
||||
logger.error(`Error while running migration '${migration}'`, {
|
||||
label: 'Settings Migrator',
|
||||
});
|
||||
throw e;
|
||||
// we stop jellyseerr if the migration failed
|
||||
logger.error(
|
||||
`Error while running migration '${migration}': ${e.message}`,
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
logger.error(
|
||||
'A common cause for this error is a permission issue with your configuration folder, a network issue or a corrupted database.',
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,22 +81,18 @@ export const runMigrations = async (
|
||||
await fs.writeFile(BACKUP_PATH, oldBackup.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
// we stop jellyseerr if the migration failed
|
||||
logger.error(
|
||||
`Something went wrong while running settings migrations: ${e.message}`,
|
||||
{ label: 'Settings Migrator' }
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
// we stop jellyseerr if the migration failed
|
||||
console.log(
|
||||
'===================================================================='
|
||||
);
|
||||
console.log(
|
||||
' SOMETHING WENT WRONG WHILE RUNNING SETTINGS MIGRATIONS '
|
||||
);
|
||||
console.log(
|
||||
' Please check that your configuration folder is properly set up '
|
||||
);
|
||||
console.log(
|
||||
'===================================================================='
|
||||
logger.error(
|
||||
'A common cause for this issue is a permission error of your configuration folder.',
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
@@ -299,54 +299,84 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
});
|
||||
|
||||
if (!user && !(await userRepository.count())) {
|
||||
const missingAdminUser = !user && !(await userRepository.count());
|
||||
if (
|
||||
missingAdminUser ||
|
||||
settings.main.mediaServerType === MediaServerType.NOT_CONFIGURED
|
||||
) {
|
||||
// Check if user is admin on jellyfin
|
||||
if (account.User.Policy.IsAdministrator === false) {
|
||||
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
if (
|
||||
body.serverType !== MediaServerType.JELLYFIN &&
|
||||
body.serverType !== MediaServerType.EMBY
|
||||
) {
|
||||
throw new Error('select_server_type');
|
||||
}
|
||||
settings.main.mediaServerType = body.serverType;
|
||||
|
||||
if (missingAdminUser) {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Jellyseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
// User doesn't exist, and there are no users in the database, we'll create the user
|
||||
// with admin permissions
|
||||
|
||||
user = new User({
|
||||
id: 1,
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
userType:
|
||||
body.serverType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
});
|
||||
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; editing admin user for Jellyseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
// User alread exist but settings.json is not configured, we'll edit the admin user
|
||||
|
||||
user = await userRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('Unable to find admin user to edit');
|
||||
}
|
||||
);
|
||||
user.email = body.email || account.User.Name;
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
user.jellyfinUserId = account.User.Id;
|
||||
user.jellyfinDeviceId = deviceId;
|
||||
user.jellyfinAuthToken = account.AccessToken;
|
||||
user.permissions = Permission.ADMIN;
|
||||
user.avatar = `/avatarproxy/${account.User.Id}`;
|
||||
user.userType =
|
||||
body.serverType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY;
|
||||
|
||||
// User doesn't exist, and there are no users in the database, we'll create the user
|
||||
// with admin permissions
|
||||
switch (body.serverType) {
|
||||
case MediaServerType.EMBY:
|
||||
settings.main.mediaServerType = MediaServerType.EMBY;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
userType: UserType.EMBY,
|
||||
});
|
||||
|
||||
break;
|
||||
case MediaServerType.JELLYFIN:
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('select_server_type');
|
||||
await userRepository.save(user);
|
||||
}
|
||||
|
||||
// Create an API key on Jellyfin from this admin user
|
||||
@@ -368,8 +398,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
await settings.save();
|
||||
startJobs();
|
||||
|
||||
await userRepository.save(user);
|
||||
}
|
||||
// User already exists, let's update their information
|
||||
else if (account.User.Id === user?.jellyfinUserId) {
|
||||
|
||||
Reference in New Issue
Block a user