started implementing radarr and sonarr
This commit is contained in:
parent
7b363964f7
commit
f198d98437
23 changed files with 45022 additions and 15 deletions
27
Caddyfile
Normal file
27
Caddyfile
Normal file
|
@ -0,0 +1,27 @@
|
|||
|
||||
http://localhost
|
||||
|
||||
route /sonarr/api/v3/* {
|
||||
uri strip_prefix /sonarr
|
||||
reverse_proxy sonarr_v3:4010
|
||||
}
|
||||
|
||||
route /sonarr/api/v5/* {
|
||||
uri strip_prefix /sonarr
|
||||
reverse_proxy sonarr_v5:4010
|
||||
}
|
||||
|
||||
route /radarr/* {
|
||||
uri strip_prefix /radarr
|
||||
reverse_proxy radarr:4010
|
||||
}
|
||||
|
||||
# route /tmdb/* {
|
||||
# uri strip_prefix /tmdb
|
||||
# reverse_proxy tmdb:4010
|
||||
# }
|
||||
|
||||
route /jellifin/* {
|
||||
uri strip_prefix /jellifin
|
||||
reverse_proxy jellifin:4010
|
||||
}
|
|
@ -5,6 +5,12 @@
|
|||
|
||||
## APIS
|
||||
|
||||
### Host mocked api's
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
### Generate openapi client
|
||||
|
||||
- path to source yml or json
|
||||
|
@ -12,5 +18,5 @@
|
|||
|
||||
example
|
||||
```bash
|
||||
bunx openapi-typescript .\src\features\content\apis\api.yml -o .\src\features\content\apis\api.generated.ts
|
||||
bunx openapi-typescript .\src\features\content\apis\api.json -o .\src\features\content\apis\api.generated.ts
|
||||
```
|
54
docker-compose.mocks.yml
Normal file
54
docker-compose.mocks.yml
Normal file
|
@ -0,0 +1,54 @@
|
|||
version: '3'
|
||||
services:
|
||||
proxy:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
ports:
|
||||
- '8080:80'
|
||||
depends_on:
|
||||
- sonarr_v3
|
||||
- sonarr_v5
|
||||
- radarr
|
||||
- jellyfin
|
||||
|
||||
sonarr_v3:
|
||||
image: stoplight/prism
|
||||
volumes:
|
||||
- './src/features/content/apis:/var/apis'
|
||||
command: >-
|
||||
mock
|
||||
-h 0.0.0.0
|
||||
-p 4010
|
||||
/var/apis/sonarr.v3.json
|
||||
|
||||
sonarr_v5:
|
||||
image: stoplight/prism
|
||||
volumes:
|
||||
- './src/features/content/apis:/var/apis'
|
||||
command: >-
|
||||
mock
|
||||
-h 0.0.0.0
|
||||
-p 4010
|
||||
/var/apis/sonarr.v5.json
|
||||
|
||||
radarr:
|
||||
image: stoplight/prism
|
||||
volumes:
|
||||
- './src/features/content/apis:/var/apis'
|
||||
command: >-
|
||||
mock
|
||||
-h 0.0.0.0
|
||||
-p 4010
|
||||
/var/apis/radarr.json
|
||||
|
||||
jellyfin:
|
||||
image: stoplight/prism
|
||||
volumes:
|
||||
- './src/features/content/apis:/var/apis'
|
||||
command: >-
|
||||
mock
|
||||
-h 0.0.0.0
|
||||
-p 4010
|
||||
/var/apis/jellyfin.json
|
||||
/var/apis/jellyfin.json
|
|
@ -1,8 +1,17 @@
|
|||
version: '3'
|
||||
services:
|
||||
jellyfin:
|
||||
image: stoplight/prism
|
||||
ports:
|
||||
- '9003:4010'
|
||||
sonarr:
|
||||
image: hotio/sonarr
|
||||
container_name: sonarr
|
||||
volumes:
|
||||
- './src/features/content/apis:/var/apis'
|
||||
command: 'mock -h 0.0.0.0 /var/apis/jellyfin.json'
|
||||
- ./media:/media
|
||||
ports:
|
||||
- 8989:8989
|
||||
|
||||
radarr:
|
||||
image: hotio/radarr
|
||||
container_name: radarr
|
||||
volumes:
|
||||
- ./media:/media
|
||||
ports:
|
||||
- 7878:7878
|
9275
src/features/content/apis/radarr.generated.ts
Normal file
9275
src/features/content/apis/radarr.generated.ts
Normal file
File diff suppressed because it is too large
Load diff
12181
src/features/content/apis/radarr.json
Normal file
12181
src/features/content/apis/radarr.json
Normal file
File diff suppressed because it is too large
Load diff
49
src/features/content/apis/radarr.ts
Normal file
49
src/features/content/apis/radarr.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import type { paths } from "./radarr.generated";
|
||||
import { query } from "@solidjs/router";
|
||||
import createClient from "openapi-fetch";
|
||||
|
||||
const getBaseUrl = () => {
|
||||
"use server";
|
||||
|
||||
return process.env.RADARR_BASE_URL;
|
||||
};
|
||||
|
||||
const getClient = () => {
|
||||
"use server";
|
||||
|
||||
return createClient<paths>({
|
||||
baseUrl: getBaseUrl(),
|
||||
headers: {
|
||||
"X-Api-Key": `${process.env.RADARR_API_KEY}`,
|
||||
"Content-Type": 'application/json;',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const get = query(async () => {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET('/api/v3/movie');
|
||||
|
||||
return data;
|
||||
}, 'radarr.get');
|
||||
|
||||
export const addMovie = query(async (id: string) => {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().POST('/api/v3/movie', {
|
||||
body: {
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
}, 'radarr.get');
|
||||
|
||||
export const listIds = query(async () => {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET('/api/v3/movie');
|
||||
|
||||
return Object.fromEntries(data?.map(({ id, tmdbId }) => ([ `m${tmdbId}`, { radarr: id } ] as const)) ?? []);
|
||||
}, 'radarr.listIds');
|
57
src/features/content/apis/sonarr.ts
Normal file
57
src/features/content/apis/sonarr.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import type { paths as v3Paths } from "./sonarr.v3.generated";
|
||||
import type { paths as v5Paths } from "./sonarr.v5.generated";
|
||||
import { query } from "@solidjs/router";
|
||||
import createClient from "openapi-fetch";
|
||||
|
||||
const getBaseUrl = () => {
|
||||
"use server";
|
||||
|
||||
return process.env.SONARR_BASE_URL;
|
||||
};
|
||||
|
||||
const getClient = () => {
|
||||
"use server";
|
||||
|
||||
return createClient<v3Paths&v5Paths>({
|
||||
baseUrl: getBaseUrl(),
|
||||
headers: {
|
||||
"X-Api-Key": `${process.env.SONARR_API_KEY}`,
|
||||
"Content-Type": 'application/json;',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const TEST = query(async () => {
|
||||
"use server";
|
||||
|
||||
const { data } = await getClient().GET('/api/v3/series', {
|
||||
params: {
|
||||
query: {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}, 'sonarr.TEST');
|
||||
|
||||
export const getByTmdbId = query(async (id: string) => {
|
||||
"use server";
|
||||
|
||||
const { data } = await getClient().GET('/api/v3/series/lookup', {
|
||||
params: {
|
||||
query: {
|
||||
term: `tmdb:${id}`
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data?.[0];
|
||||
}, 'sonarr.getByTmdbId');
|
||||
|
||||
export const listIds = query(async () => {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET('/api/v3/series');
|
||||
|
||||
return Object.fromEntries(data?.map(({ id, tmdbId }) => ([ `s${tmdbId}`, { sonarr: id } ] as const)) ?? []);
|
||||
}, 'sonarr.listIds');
|
9216
src/features/content/apis/sonarr.v3.generated.ts
Normal file
9216
src/features/content/apis/sonarr.v3.generated.ts
Normal file
File diff suppressed because it is too large
Load diff
11887
src/features/content/apis/sonarr.v3.json
Normal file
11887
src/features/content/apis/sonarr.v3.json
Normal file
File diff suppressed because it is too large
Load diff
902
src/features/content/apis/sonarr.v5.generated.ts
Normal file
902
src/features/content/apis/sonarr.v5.generated.ts
Normal file
|
@ -0,0 +1,902 @@
|
|||
/**
|
||||
* This file was auto-generated by openapi-typescript.
|
||||
* Do not make direct changes to the file.
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/api": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/login": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post: {
|
||||
parameters: {
|
||||
query?: {
|
||||
returnUrl?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
username?: string;
|
||||
password?: string;
|
||||
rememberMe?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/logout": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/log": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortKey?: string;
|
||||
sortDirection?: components["schemas"]["SortDirection"];
|
||||
level?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["LogResourcePagingResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/ping": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PingResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PingResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/series": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
tvdbId?: number;
|
||||
includeSeasonImages?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SeriesResource"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SeriesResource"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SeriesResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/series/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
includeSeasonImages?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SeriesResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put: {
|
||||
parameters: {
|
||||
query?: {
|
||||
moveFiles?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SeriesResource"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SeriesResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
post?: never;
|
||||
delete: {
|
||||
parameters: {
|
||||
query?: {
|
||||
deleteFiles?: boolean;
|
||||
addImportListExclusion?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/series/{id}/folder": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/series/lookup": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
term?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"text/plain": components["schemas"]["SeriesResource"][];
|
||||
"application/json": components["schemas"]["SeriesResource"][];
|
||||
"text/json": components["schemas"]["SeriesResource"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/content/{path}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
path: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
path: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/{path}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
path: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/update": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdateResource"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/settings/update": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"text/plain": components["schemas"]["UpdateSettingsResource"];
|
||||
"application/json": components["schemas"]["UpdateSettingsResource"];
|
||||
"text/json": components["schemas"]["UpdateSettingsResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdateSettingsResource"];
|
||||
"text/json": components["schemas"]["UpdateSettingsResource"];
|
||||
"application/*+json": components["schemas"]["UpdateSettingsResource"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"text/plain": components["schemas"]["UpdateSettingsResource"];
|
||||
"application/json": components["schemas"]["UpdateSettingsResource"];
|
||||
"text/json": components["schemas"]["UpdateSettingsResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v5/settings/update/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdateSettingsResource"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
AddSeriesOptions: {
|
||||
ignoreEpisodesWithFiles?: boolean;
|
||||
ignoreEpisodesWithoutFiles?: boolean;
|
||||
monitor?: components["schemas"]["MonitorTypes"];
|
||||
searchForMissingEpisodes?: boolean;
|
||||
searchForCutoffUnmetEpisodes?: boolean;
|
||||
};
|
||||
AlternateTitleResource: {
|
||||
title?: string | null;
|
||||
/** Format: int32 */
|
||||
seasonNumber?: number | null;
|
||||
/** Format: int32 */
|
||||
sceneSeasonNumber?: number | null;
|
||||
sceneOrigin?: string | null;
|
||||
comment?: string | null;
|
||||
};
|
||||
Language: {
|
||||
/** Format: int32 */
|
||||
id?: number;
|
||||
name?: string | null;
|
||||
};
|
||||
LogResource: {
|
||||
/** Format: int32 */
|
||||
id?: number;
|
||||
/** Format: date-time */
|
||||
time?: string;
|
||||
exception?: string | null;
|
||||
exceptionType?: string | null;
|
||||
level: string | null;
|
||||
logger: string | null;
|
||||
message: string | null;
|
||||
};
|
||||
LogResourcePagingResource: {
|
||||
/** Format: int32 */
|
||||
page?: number;
|
||||
/** Format: int32 */
|
||||
pageSize?: number;
|
||||
sortKey?: string | null;
|
||||
sortDirection?: components["schemas"]["SortDirection"];
|
||||
/** Format: int32 */
|
||||
totalRecords?: number;
|
||||
records?: components["schemas"]["LogResource"][] | null;
|
||||
};
|
||||
MediaCover: {
|
||||
coverType?: components["schemas"]["MediaCoverTypes"];
|
||||
url?: string | null;
|
||||
remoteUrl?: string | null;
|
||||
};
|
||||
/** @enum {string} */
|
||||
MediaCoverTypes: "unknown" | "poster" | "banner" | "fanart" | "screenshot" | "headshot" | "clearlogo";
|
||||
/** @enum {string} */
|
||||
MonitorTypes: "unknown" | "all" | "future" | "missing" | "existing" | "firstSeason" | "lastSeason" | "latestSeason" | "pilot" | "recent" | "monitorSpecials" | "unmonitorSpecials" | "none" | "skip";
|
||||
/** @enum {string} */
|
||||
NewItemMonitorTypes: "all" | "none";
|
||||
PingResource: {
|
||||
status?: string | null;
|
||||
};
|
||||
Ratings: {
|
||||
/** Format: int32 */
|
||||
votes?: number;
|
||||
/** Format: double */
|
||||
value?: number;
|
||||
};
|
||||
SeasonResource: {
|
||||
/** Format: int32 */
|
||||
seasonNumber?: number;
|
||||
monitored?: boolean;
|
||||
statistics?: components["schemas"]["SeasonStatisticsResource"];
|
||||
images?: components["schemas"]["MediaCover"][] | null;
|
||||
};
|
||||
SeasonStatisticsResource: {
|
||||
/** Format: date-time */
|
||||
nextAiring?: string | null;
|
||||
/** Format: date-time */
|
||||
previousAiring?: string | null;
|
||||
/** Format: int32 */
|
||||
episodeFileCount?: number;
|
||||
/** Format: int32 */
|
||||
episodeCount?: number;
|
||||
/** Format: int32 */
|
||||
totalEpisodeCount?: number;
|
||||
/** Format: int64 */
|
||||
sizeOnDisk?: number;
|
||||
releaseGroups?: string[] | null;
|
||||
/** Format: double */
|
||||
readonly percentOfEpisodes?: number;
|
||||
};
|
||||
SeriesResource: {
|
||||
/** Format: int32 */
|
||||
id?: number;
|
||||
title?: string | null;
|
||||
alternateTitles?: components["schemas"]["AlternateTitleResource"][] | null;
|
||||
sortTitle?: string | null;
|
||||
status?: components["schemas"]["SeriesStatusType"];
|
||||
readonly ended?: boolean;
|
||||
profileName?: string | null;
|
||||
overview?: string | null;
|
||||
/** Format: date-time */
|
||||
nextAiring?: string | null;
|
||||
/** Format: date-time */
|
||||
previousAiring?: string | null;
|
||||
network?: string | null;
|
||||
airTime?: string | null;
|
||||
images?: components["schemas"]["MediaCover"][] | null;
|
||||
originalLanguage?: components["schemas"]["Language"];
|
||||
remotePoster?: string | null;
|
||||
seasons?: components["schemas"]["SeasonResource"][] | null;
|
||||
/** Format: int32 */
|
||||
year?: number;
|
||||
path?: string | null;
|
||||
/** Format: int32 */
|
||||
qualityProfileId?: number;
|
||||
seasonFolder?: boolean;
|
||||
monitored?: boolean;
|
||||
monitorNewItems?: components["schemas"]["NewItemMonitorTypes"];
|
||||
useSceneNumbering?: boolean;
|
||||
/** Format: int32 */
|
||||
runtime?: number;
|
||||
/** Format: int32 */
|
||||
tvdbId?: number;
|
||||
/** Format: int32 */
|
||||
tvRageId?: number;
|
||||
/** Format: int32 */
|
||||
tvMazeId?: number;
|
||||
/** Format: int32 */
|
||||
tmdbId?: number;
|
||||
/** Format: date-time */
|
||||
firstAired?: string | null;
|
||||
/** Format: date-time */
|
||||
lastAired?: string | null;
|
||||
seriesType?: components["schemas"]["SeriesTypes"];
|
||||
cleanTitle?: string | null;
|
||||
imdbId?: string | null;
|
||||
titleSlug?: string | null;
|
||||
rootFolderPath?: string | null;
|
||||
folder?: string | null;
|
||||
certification?: string | null;
|
||||
genres?: string[] | null;
|
||||
tags?: number[] | null;
|
||||
/** Format: date-time */
|
||||
added?: string;
|
||||
addOptions?: components["schemas"]["AddSeriesOptions"];
|
||||
ratings?: components["schemas"]["Ratings"];
|
||||
statistics?: components["schemas"]["SeriesStatisticsResource"];
|
||||
episodesChanged?: boolean | null;
|
||||
};
|
||||
SeriesStatisticsResource: {
|
||||
/** Format: int32 */
|
||||
seasonCount?: number;
|
||||
/** Format: int32 */
|
||||
episodeFileCount?: number;
|
||||
/** Format: int32 */
|
||||
episodeCount?: number;
|
||||
/** Format: int32 */
|
||||
totalEpisodeCount?: number;
|
||||
/** Format: int64 */
|
||||
sizeOnDisk?: number;
|
||||
releaseGroups?: string[] | null;
|
||||
/** Format: double */
|
||||
readonly percentOfEpisodes?: number;
|
||||
};
|
||||
/** @enum {string} */
|
||||
SeriesStatusType: "continuing" | "ended" | "upcoming" | "deleted";
|
||||
/** @enum {string} */
|
||||
SeriesTypes: "standard" | "daily" | "anime";
|
||||
/** @enum {string} */
|
||||
SortDirection: "default" | "ascending" | "descending";
|
||||
UpdateChanges: {
|
||||
new?: string[] | null;
|
||||
fixed?: string[] | null;
|
||||
};
|
||||
/** @enum {string} */
|
||||
UpdateMechanism: "builtIn" | "script" | "external" | "apt" | "docker";
|
||||
UpdateResource: {
|
||||
/** Format: int32 */
|
||||
id?: number;
|
||||
version: string | null;
|
||||
branch: string | null;
|
||||
/** Format: date-time */
|
||||
releaseDate?: string;
|
||||
fileName: string | null;
|
||||
url: string | null;
|
||||
installed?: boolean;
|
||||
/** Format: date-time */
|
||||
installedOn?: string | null;
|
||||
installable?: boolean;
|
||||
latest?: boolean;
|
||||
changes: components["schemas"]["UpdateChanges"];
|
||||
hash: string | null;
|
||||
};
|
||||
UpdateSettingsResource: {
|
||||
/** Format: int32 */
|
||||
id?: number;
|
||||
branch?: string | null;
|
||||
updateAutomatically?: boolean;
|
||||
updateMechanism?: components["schemas"]["UpdateMechanism"];
|
||||
updateScriptPath?: string | null;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export type operations = Record<string, never>;
|
1260
src/features/content/apis/sonarr.v5.json
Normal file
1260
src/features/content/apis/sonarr.v5.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
|||
import createClient from "openapi-fetch";
|
||||
import { query } from "@solidjs/router";
|
||||
import { Entry, SearchResult } from "../types";
|
||||
import { paths as pathsV3, operations } from "./tmdb.generated";
|
||||
import { paths as pathsV3 } from "./tmdb.v3.generated";
|
||||
import { paths as pathsV4 } from "./tmdb.not.generated";
|
||||
|
||||
interface TMDBItem {
|
||||
|
@ -58,8 +58,6 @@ export const getEntry = query(
|
|||
tv: { series_id: Number.parseInt(id.slice(1)) },
|
||||
} as const)[mediaType];
|
||||
|
||||
console.log(`going to fetch from '${endpoint}' with id '${id}'`)
|
||||
|
||||
const { data } = await clientV3.GET(endpoint, {
|
||||
params: {
|
||||
path: params,
|
||||
|
|
|
@ -9,16 +9,41 @@ import {
|
|||
getEntry as getTmdbEntry,
|
||||
searchMulti,
|
||||
} from "./apis/tmdb";
|
||||
import { listIds as listSerieIds } from "./apis/sonarr";
|
||||
import { listIds as listMovieIds } from "./apis/radarr";
|
||||
import { merge } from "~/utilities";
|
||||
|
||||
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||
|
||||
const lookupTable = query(async () => listItemIds(), 'content.lookupTable');
|
||||
const lookupTable = query(async () => {
|
||||
'use server';
|
||||
const [items, sonarr, radarr] = await Promise.all([
|
||||
listItemIds(), listSerieIds(), listMovieIds() ]);
|
||||
|
||||
return merge(items, sonarr, radarr);
|
||||
}, 'content.lookupTable');
|
||||
|
||||
export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
||||
export const getStream = query(async (id: string, range: string) => {
|
||||
const table = await lookupTable();
|
||||
const table = await lookupTable();
|
||||
const ids = table[id];
|
||||
|
||||
if (ids.jellyfin) {
|
||||
return getItemStream(ids.jellyfin, range);
|
||||
}
|
||||
|
||||
// - If the lookup table has no entry
|
||||
// than this means that we do not have the requested entry at all,
|
||||
// neither in trackers nor in the media server
|
||||
//
|
||||
// - If the lookup table contains a jellyfin id,
|
||||
// than we have the content and can stream straight away
|
||||
//
|
||||
// - If we have the radarr or sonarr id,
|
||||
// than we are tracking the entry,
|
||||
// but it is not available for use yet
|
||||
console.log(ids);
|
||||
|
||||
return getItemStream(table[id].jellyfin, range);
|
||||
}, 'content.stream');
|
||||
|
||||
export const listCategories = query(async (): Promise<Category[]> => {
|
||||
|
@ -35,7 +60,7 @@ export const listCategories = query(async (): Promise<Category[]> => {
|
|||
|
||||
export const getEntryFromSlug = query(
|
||||
async (slug: string): Promise<Entry | undefined> => {
|
||||
const { id } = slug.match(/^.+-(?<id>\w+)$/)?.groups ?? {};
|
||||
const id = slug.match(/\w+$/)![0];
|
||||
|
||||
return getTmdbEntry(id);
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
background: linear-gradient(to bottom, black, transparent) top left / 100% 20% no-repeat;
|
||||
|
||||
& > header {
|
||||
z-index: 1;
|
||||
display: block grid;
|
||||
place-items: center;
|
||||
|
||||
|
@ -48,6 +49,7 @@
|
|||
}
|
||||
|
||||
& > section {
|
||||
z-index: 2;
|
||||
display: block grid;
|
||||
place-items: center;
|
||||
|
||||
|
@ -59,6 +61,7 @@
|
|||
}
|
||||
|
||||
& > footer {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
display: block grid;
|
||||
grid: auto auto / auto auto auto;
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
RouteDefinition,
|
||||
useParams,
|
||||
} from "@solidjs/router";
|
||||
import { Show } from "solid-js";
|
||||
import { createEffect, Show } from "solid-js";
|
||||
import { Details } from "~/components/details";
|
||||
import {
|
||||
createSlug,
|
||||
|
|
23
src/routes/experimental.tsx
Normal file
23
src/routes/experimental.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { A } from "@solidjs/router";
|
||||
import { ParentProps } from "solid-js";
|
||||
|
||||
export default function Experimental(props: ParentProps) {
|
||||
return (
|
||||
<div style={{ overflow: "auto" }}>
|
||||
<nav
|
||||
style={{
|
||||
position: "sticky",
|
||||
"inset-block-start": 0,
|
||||
display: "flex",
|
||||
gap: "var(--size-2)",
|
||||
}}
|
||||
>
|
||||
<A href="/">Home</A>
|
||||
<A href="/experimental/sonarr">Sonarr</A>
|
||||
<A href="/experimental/radarr">Radarr</A>
|
||||
</nav>
|
||||
|
||||
<main>{props.children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
3
src/routes/experimental/[...404].tsx
Normal file
3
src/routes/experimental/[...404].tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function NotFound() {
|
||||
return <>NOT FOUND</>;
|
||||
}
|
3
src/routes/experimental/index.tsx
Normal file
3
src/routes/experimental/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Index() {
|
||||
return <></>;
|
||||
}
|
13
src/routes/experimental/sonarr.tsx
Normal file
13
src/routes/experimental/sonarr.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { createAsync } from "@solidjs/router";
|
||||
import { createEffect } from "solid-js";
|
||||
import { TEST } from "~/features/content/apis/sonarr";
|
||||
|
||||
export default function Sonarr() {
|
||||
const result = createAsync(() => TEST());
|
||||
|
||||
createEffect(() => {
|
||||
console.log("the merged lookup table", result());
|
||||
});
|
||||
|
||||
return <pre>{JSON.stringify(result(), null, 2)}</pre>;
|
||||
}
|
|
@ -44,3 +44,19 @@ export const hash = (
|
|||
|
||||
return hash;
|
||||
};
|
||||
|
||||
export const merge = (...objects: Record<string, any>[]): Record<string, any> => {
|
||||
if (objects.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const target = objects[0];
|
||||
|
||||
for (const key of new Set(objects.map(o => Object.keys(o)).flat())) {
|
||||
const values = objects.filter(o => Object.hasOwn(o, key)).map(o => o[key]);
|
||||
|
||||
target[key] = values.every(v => v && typeof v === 'object' && !Array.isArray(v)) ? merge(...values) : values.at(-1);
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue