mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-17 21:53:12 +02:00
WIP
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddPlayStateAndMyList1721052160110 implements MigrationInterface {
|
||||
name = 'AddPlayStateAndMyList1721052160110'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "play_state" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "seasonNumber" integer, "episodeNumber" integer, "progress" double NOT NULL, "watched" boolean NOT NULL DEFAULT (0), "showInUpNext" boolean NOT NULL DEFAULT (1), "userId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "my_list_item" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "userId" varchar, CONSTRAINT "UQ_a14dd1370f05c5feffe21f043e9" UNIQUE ("tmdbId"))`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_play_state" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "seasonNumber" integer, "episodeNumber" integer, "progress" double NOT NULL, "watched" boolean NOT NULL DEFAULT (0), "showInUpNext" boolean NOT NULL DEFAULT (1), "userId" varchar, CONSTRAINT "FK_76a6e68bbf655b2a2bc54916803" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_play_state"("id", "tmdbId", "seasonNumber", "episodeNumber", "progress", "watched", "showInUpNext", "userId") SELECT "id", "tmdbId", "seasonNumber", "episodeNumber", "progress", "watched", "showInUpNext", "userId" FROM "play_state"`);
|
||||
await queryRunner.query(`DROP TABLE "play_state"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_play_state" RENAME TO "play_state"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_my_list_item" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "userId" varchar, CONSTRAINT "UQ_a14dd1370f05c5feffe21f043e9" UNIQUE ("tmdbId"), CONSTRAINT "FK_3657174b1c5a1bbcb4e75a237bd" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_my_list_item"("id", "tmdbId", "userId") SELECT "id", "tmdbId", "userId" FROM "my_list_item"`);
|
||||
await queryRunner.query(`DROP TABLE "my_list_item"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_my_list_item" RENAME TO "my_list_item"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "my_list_item" RENAME TO "temporary_my_list_item"`);
|
||||
await queryRunner.query(`CREATE TABLE "my_list_item" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "userId" varchar, CONSTRAINT "UQ_a14dd1370f05c5feffe21f043e9" UNIQUE ("tmdbId"))`);
|
||||
await queryRunner.query(`INSERT INTO "my_list_item"("id", "tmdbId", "userId") SELECT "id", "tmdbId", "userId" FROM "temporary_my_list_item"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_my_list_item"`);
|
||||
await queryRunner.query(`ALTER TABLE "play_state" RENAME TO "temporary_play_state"`);
|
||||
await queryRunner.query(`CREATE TABLE "play_state" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "seasonNumber" integer, "episodeNumber" integer, "progress" double NOT NULL, "watched" boolean NOT NULL DEFAULT (0), "showInUpNext" boolean NOT NULL DEFAULT (1), "userId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "play_state"("id", "tmdbId", "seasonNumber", "episodeNumber", "progress", "watched", "showInUpNext", "userId") SELECT "id", "tmdbId", "seasonNumber", "episodeNumber", "progress", "watched", "showInUpNext", "userId" FROM "temporary_play_state"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_play_state"`);
|
||||
await queryRunner.query(`DROP TABLE "my_list_item"`);
|
||||
await queryRunner.query(`DROP TABLE "play_state"`);
|
||||
}
|
||||
|
||||
}
|
||||
42
backend/migrations/1721337734378-add-titles.ts
Normal file
42
backend/migrations/1721337734378-add-titles.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddTitles1721337734378 implements MigrationInterface {
|
||||
name = 'AddTitles1721337734378'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "my_list_item" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "userId" varchar, CONSTRAINT "UQ_a14dd1370f05c5feffe21f043e9" UNIQUE ("tmdbId"))`);
|
||||
await queryRunner.query(`CREATE TABLE "media" ("id" varchar PRIMARY KEY NOT NULL, "progress" integer NOT NULL DEFAULT (0), "watched" boolean NOT NULL DEFAULT (0), "seasonNumber" integer, "episodeNumber" integer, "titleId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "title" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "upNext" boolean NOT NULL DEFAULT (0), "isInLibrary" boolean NOT NULL DEFAULT (0), "userId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_my_list_item" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "userId" varchar, CONSTRAINT "UQ_a14dd1370f05c5feffe21f043e9" UNIQUE ("tmdbId"), CONSTRAINT "FK_3657174b1c5a1bbcb4e75a237bd" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_my_list_item"("id", "tmdbId", "userId") SELECT "id", "tmdbId", "userId" FROM "my_list_item"`);
|
||||
await queryRunner.query(`DROP TABLE "my_list_item"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_my_list_item" RENAME TO "my_list_item"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_media" ("id" varchar PRIMARY KEY NOT NULL, "progress" integer NOT NULL DEFAULT (0), "watched" boolean NOT NULL DEFAULT (0), "seasonNumber" integer, "episodeNumber" integer, "titleId" varchar, CONSTRAINT "FK_7dec680069dc12a77e96c78e27e" FOREIGN KEY ("titleId") REFERENCES "title" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_media"("id", "progress", "watched", "seasonNumber", "episodeNumber", "titleId") SELECT "id", "progress", "watched", "seasonNumber", "episodeNumber", "titleId" FROM "media"`);
|
||||
await queryRunner.query(`DROP TABLE "media"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_title" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "upNext" boolean NOT NULL DEFAULT (0), "isInLibrary" boolean NOT NULL DEFAULT (0), "userId" varchar, CONSTRAINT "FK_feed27b6c765803cc09d45dd6d0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_title"("id", "tmdbId", "upNext", "isInLibrary", "userId") SELECT "id", "tmdbId", "upNext", "isInLibrary", "userId" FROM "title"`);
|
||||
await queryRunner.query(`DROP TABLE "title"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_title" RENAME TO "title"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "title" RENAME TO "temporary_title"`);
|
||||
await queryRunner.query(`CREATE TABLE "title" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "upNext" boolean NOT NULL DEFAULT (0), "isInLibrary" boolean NOT NULL DEFAULT (0), "userId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "title"("id", "tmdbId", "upNext", "isInLibrary", "userId") SELECT "id", "tmdbId", "upNext", "isInLibrary", "userId" FROM "temporary_title"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_title"`);
|
||||
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
|
||||
await queryRunner.query(`CREATE TABLE "media" ("id" varchar PRIMARY KEY NOT NULL, "progress" integer NOT NULL DEFAULT (0), "watched" boolean NOT NULL DEFAULT (0), "seasonNumber" integer, "episodeNumber" integer, "titleId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "media"("id", "progress", "watched", "seasonNumber", "episodeNumber", "titleId") SELECT "id", "progress", "watched", "seasonNumber", "episodeNumber", "titleId" FROM "temporary_media"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_media"`);
|
||||
await queryRunner.query(`ALTER TABLE "my_list_item" RENAME TO "temporary_my_list_item"`);
|
||||
await queryRunner.query(`CREATE TABLE "my_list_item" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "userId" varchar, CONSTRAINT "UQ_a14dd1370f05c5feffe21f043e9" UNIQUE ("tmdbId"))`);
|
||||
await queryRunner.query(`INSERT INTO "my_list_item"("id", "tmdbId", "userId") SELECT "id", "tmdbId", "userId" FROM "temporary_my_list_item"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_my_list_item"`);
|
||||
await queryRunner.query(`DROP TABLE "title"`);
|
||||
await queryRunner.query(`DROP TABLE "media"`);
|
||||
await queryRunner.query(`DROP TABLE "my_list_item"`);
|
||||
}
|
||||
|
||||
}
|
||||
20
backend/migrations/1721338340748-add-title-watched.ts
Normal file
20
backend/migrations/1721338340748-add-title-watched.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddTitleWatched1721338340748 implements MigrationInterface {
|
||||
name = 'AddTitleWatched1721338340748'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "temporary_title" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "upNext" boolean NOT NULL DEFAULT (0), "isInLibrary" boolean NOT NULL DEFAULT (0), "userId" varchar, "watched" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_feed27b6c765803cc09d45dd6d0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_title"("id", "tmdbId", "upNext", "isInLibrary", "userId") SELECT "id", "tmdbId", "upNext", "isInLibrary", "userId" FROM "title"`);
|
||||
await queryRunner.query(`DROP TABLE "title"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_title" RENAME TO "title"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "title" RENAME TO "temporary_title"`);
|
||||
await queryRunner.query(`CREATE TABLE "title" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "upNext" boolean NOT NULL DEFAULT (0), "isInLibrary" boolean NOT NULL DEFAULT (0), "userId" varchar, CONSTRAINT "FK_feed27b6c765803cc09d45dd6d0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "title"("id", "tmdbId", "upNext", "isInLibrary", "userId") SELECT "id", "tmdbId", "upNext", "isInLibrary", "userId" FROM "temporary_title"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_title"`);
|
||||
}
|
||||
|
||||
}
|
||||
20
backend/migrations/1721342594807-add-title-type.ts
Normal file
20
backend/migrations/1721342594807-add-title-type.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddTitleType1721342594807 implements MigrationInterface {
|
||||
name = 'AddTitleType1721342594807'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "temporary_title" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "upNext" boolean NOT NULL DEFAULT (0), "isInLibrary" boolean NOT NULL DEFAULT (0), "userId" varchar, "watched" boolean NOT NULL DEFAULT (0), "type" varchar CHECK( "type" IN ('movie','series') ) NOT NULL DEFAULT ('movie'), CONSTRAINT "FK_feed27b6c765803cc09d45dd6d0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_title"("id", "tmdbId", "upNext", "isInLibrary", "userId", "watched") SELECT "id", "tmdbId", "upNext", "isInLibrary", "userId", "watched" FROM "title"`);
|
||||
await queryRunner.query(`DROP TABLE "title"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_title" RENAME TO "title"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "title" RENAME TO "temporary_title"`);
|
||||
await queryRunner.query(`CREATE TABLE "title" ("id" varchar PRIMARY KEY NOT NULL, "tmdbId" integer NOT NULL, "upNext" boolean NOT NULL DEFAULT (0), "isInLibrary" boolean NOT NULL DEFAULT (0), "userId" varchar, "watched" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_feed27b6c765803cc09d45dd6d0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "title"("id", "tmdbId", "upNext", "isInLibrary", "userId", "watched") SELECT "id", "tmdbId", "upNext", "isInLibrary", "userId", "watched" FROM "temporary_title"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_title"`);
|
||||
}
|
||||
|
||||
}
|
||||
13
backend/migrations/1721377870204-add-tmdb-list-id.ts
Normal file
13
backend/migrations/1721377870204-add-tmdb-list-id.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTmdbListId1721377870204 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`UPDATE "user"
|
||||
SET "settings" = json_set("settings", '$.tmdb.libraryListId', json('""'));`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`UPDATE "user"
|
||||
SET "settings" = json_remove("settings", '$.tmdb.libraryListId');`);
|
||||
}
|
||||
}
|
||||
144
backend/package-lock.json
generated
144
backend/package-lock.json
generated
@@ -10,13 +10,17 @@
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11",
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/express-http-proxy": "^1.6.6",
|
||||
"axios": "^1.7.2",
|
||||
"cache-manager": "^5.7.3",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -1622,6 +1626,17 @@
|
||||
"@nestjs/swagger": "^4.8.1 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cache-manager": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz",
|
||||
"integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^9.0.0 || ^10.0.0",
|
||||
"cache-manager": "<=5",
|
||||
"rxjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "10.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz",
|
||||
@@ -1849,15 +1864,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.5.tgz",
|
||||
"integrity": "sha512-IhVomwLvdLlv4zCdQK2ROT/nInk1i8m4K48lAUHJV5UVktgVmg0WbQga2/9KywaTjNbx+eWhZXXFii+vtFRAOw==",
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz",
|
||||
"integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==",
|
||||
"dependencies": {
|
||||
"body-parser": "1.20.2",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.18.3",
|
||||
"express": "4.19.2",
|
||||
"multer": "1.4.4-lts.1",
|
||||
"tslib": "2.6.2"
|
||||
"tslib": "2.6.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -1868,6 +1883,11 @@
|
||||
"@nestjs/core": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express/node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz",
|
||||
@@ -2208,6 +2228,15 @@
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/axios": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
|
||||
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
|
||||
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
|
||||
"dependencies": {
|
||||
"axios": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -3141,8 +3170,17 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
@@ -3372,12 +3410,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3616,6 +3654,25 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/cache-manager": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.3.tgz",
|
||||
"integrity": "sha512-Vp2gd2aDm/MXdEWD0FLdOflvcVj4rdJ1FFmPUeOKq+fuL7MEUcezbTWxQmVB1TTN5Ig92CabMfi5z+HyQwVg9A==",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lru-cache": "^10.2.2",
|
||||
"promise-coalesce": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
@@ -3975,7 +4032,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -4074,9 +4130,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -4279,7 +4335,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -4818,6 +4873,11 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -4890,16 +4950,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.3",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
|
||||
"integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==",
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -5088,9 +5148,9 @@
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -5222,6 +5282,25 @@
|
||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
||||
@@ -5291,7 +5370,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -7040,6 +7118,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -8293,6 +8376,14 @@
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/promise-coalesce": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz",
|
||||
"integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-inflight": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||
@@ -8337,6 +8428,11 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
|
||||
@@ -22,18 +22,22 @@
|
||||
"typeorm": "ts-node ./node_modules/typeorm/cli",
|
||||
"typeorm:run-migrations": "npm run build && npm run typeorm migration:run -- -d ./dist/data-source.js",
|
||||
"typeorm:generate-migration": "npm run build && npm run typeorm -- -d ./dist/data-source.js migration:generate",
|
||||
"typeorm:create-migration": "npm run build && npm run typeorm -- migration:create ./migrations/$npm_config_name",
|
||||
"typeorm:create-migration": "npm run build && npm run typeorm -- migration:create",
|
||||
"typeorm:revert-migration": "npm run build && npm run typeorm -- -d ./dist/data-source.js migration:revert"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11",
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/express-http-proxy": "^1.6.6",
|
||||
"axios": "^1.7.2",
|
||||
"cache-manager": "^5.7.3",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -7,7 +7,8 @@ import { AuthModule } from './auth/auth.module';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
import { MyListModule } from './users/my-list/my-list.module';
|
||||
import { PlayStateModule } from './users/play-state/play-state.module';
|
||||
import { TitleModule } from './users/titles/title.module';
|
||||
import { ProxyModule } from './proxy/proxy.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,10 +16,11 @@ import { PlayStateModule } from './users/play-state/play-state.module';
|
||||
UsersModule,
|
||||
AuthModule,
|
||||
MyListModule,
|
||||
PlayStateModule,
|
||||
TitleModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '../dist'),
|
||||
}),
|
||||
ProxyModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const TMDB_API_KEY =
|
||||
'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0YTZiMDIxZTE5Y2YxOTljMTM1NGFhMGRiMDZiOTkzMiIsInN1YiI6IjY0ODYzYWRmMDI4ZjE0MDExZTU1MDkwMiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.yyMkZlhGOGBHtw1yvpBVUUHhu7IKVYho49MvNNKt_wY';
|
||||
export const JWT_SECRET =
|
||||
process.env.SECRET || Math.random().toString(36).substring(2, 15);
|
||||
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME;
|
||||
|
||||
8
backend/src/proxy/proxy.module.ts
Normal file
8
backend/src/proxy/proxy.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TmdbModule } from './tmdb/tmdb.module';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
|
||||
@Module({
|
||||
imports: [TmdbModule],
|
||||
})
|
||||
export class ProxyModule {}
|
||||
62
backend/src/proxy/tmdb/tmdb.controller.ts
Normal file
62
backend/src/proxy/tmdb/tmdb.controller.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { All, Controller, Inject, Query, Req, Res } from '@nestjs/common';
|
||||
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import axios from 'axios';
|
||||
import { Request, Response } from 'express';
|
||||
import { TMDB_API_KEY } from '../../consts';
|
||||
import * as url from 'node:url';
|
||||
|
||||
@Controller('proxy/tmdb')
|
||||
export class TmdbController {
|
||||
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
@All('*')
|
||||
async proxyRequest(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Query() queryParams,
|
||||
) {
|
||||
// const externalApiUrl = 'https://external.api'; // Base URL of the external API
|
||||
// const url = `${externalApiUrl}${req.url}`;
|
||||
// const method = req.method;
|
||||
//
|
||||
// let data;
|
||||
// if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
||||
// data = req.body;
|
||||
// }
|
||||
//
|
||||
// const headers = { ...req.headers };
|
||||
// const uri = Reflect.getMetadata(
|
||||
// PATH_METADATA,
|
||||
// TmdbController.prototype.staticServe,
|
||||
// );
|
||||
const uri = url.parse(req.url).pathname.replace('/api/proxy/tmdb', '');
|
||||
|
||||
const cacheValue = await this.cacheManager.get(uri);
|
||||
|
||||
if (!cacheValue) {
|
||||
console.log('not cached', uri);
|
||||
|
||||
const r = await axios(uri, {
|
||||
method: req.method,
|
||||
params: queryParams,
|
||||
// data: req.body,
|
||||
baseURL: 'https://api.themoviedb.org',
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
// ...req.headers,
|
||||
Authorization: `Bearer ${TMDB_API_KEY}`,
|
||||
},
|
||||
})
|
||||
.then((r) => r.data)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return 'err';
|
||||
// return 'err';
|
||||
});
|
||||
await this.cacheManager.set(uri, r);
|
||||
res.send(r);
|
||||
} else {
|
||||
res.send(cacheValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
backend/src/proxy/tmdb/tmdb.module.ts
Normal file
9
backend/src/proxy/tmdb/tmdb.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TmdbController } from './tmdb.controller';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
|
||||
@Module({
|
||||
imports: [CacheModule.register({ ttl: 1000 * 60 * 60 * 36 })],
|
||||
controllers: [TmdbController],
|
||||
})
|
||||
export class TmdbModule {}
|
||||
@@ -1,114 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Put,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard, GetUser } from '../../auth/auth.guard';
|
||||
import { User } from '../user.entity';
|
||||
import { PlayStateDto, UpdatePlayStateDto } from './play-state.dtos';
|
||||
import { PlayStateService } from './play-state.service';
|
||||
import { ApiOkResponse } from '@nestjs/swagger';
|
||||
|
||||
@Controller('play-state')
|
||||
@UseGuards(AuthGuard)
|
||||
export class PlayStateController {
|
||||
constructor(private playStateService: PlayStateService) {}
|
||||
|
||||
@Get(':tmdbId')
|
||||
@ApiOkResponse({ type: PlayStateDto, isArray: true })
|
||||
async getPlayState(
|
||||
@GetUser() user: User,
|
||||
@Param('tmdbId') tmdbId: number,
|
||||
): Promise<PlayStateDto[]> {
|
||||
const tmdbIdNumber = Number(tmdbId);
|
||||
|
||||
if (isNaN(tmdbIdNumber)) {
|
||||
throw new Error('Invalid tmdbId');
|
||||
}
|
||||
|
||||
return this.playStateService.getPlayState(user, tmdbIdNumber);
|
||||
}
|
||||
|
||||
@Get(':tmdbId/season/:seasonNumber/episode/:episodeNumber')
|
||||
@ApiOkResponse({ type: PlayStateDto })
|
||||
async getEpisodePlayState(
|
||||
@GetUser() user: User,
|
||||
@Param('tmdbId') tmdbId: number,
|
||||
@Param('seasonNumber') seasonNumber: number,
|
||||
@Param('episodeNumber') episodeNumber: number,
|
||||
): Promise<PlayStateDto> {
|
||||
const tmdbIdNumber = Number(tmdbId);
|
||||
const seasonNumberNumber = Number(seasonNumber);
|
||||
const episodeNumberNumber = Number(episodeNumber);
|
||||
|
||||
if (
|
||||
isNaN(tmdbIdNumber) ||
|
||||
isNaN(seasonNumberNumber) ||
|
||||
isNaN(episodeNumberNumber)
|
||||
) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return this.playStateService.getEpisodePlayState(
|
||||
user,
|
||||
tmdbIdNumber,
|
||||
seasonNumberNumber,
|
||||
episodeNumberNumber,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':tmdbId')
|
||||
@ApiOkResponse({ type: PlayStateDto })
|
||||
async updatePlayState(
|
||||
@GetUser() user: User,
|
||||
@Param('tmdbId') tmdbId: number,
|
||||
@Body() playStateDto: UpdatePlayStateDto,
|
||||
): Promise<PlayStateDto> {
|
||||
const tmdbIdNumber = Number(tmdbId);
|
||||
|
||||
if (isNaN(tmdbIdNumber)) {
|
||||
throw new Error('Invalid tmdbId');
|
||||
}
|
||||
|
||||
return this.playStateService.updateOrCreatePlayState({
|
||||
user,
|
||||
tmdbId: tmdbIdNumber,
|
||||
updatePlayStateDto: playStateDto,
|
||||
});
|
||||
}
|
||||
|
||||
@Put(':tmdbId/season/:seasonNumber/episode/:episodeNumber')
|
||||
@ApiOkResponse({ type: PlayStateDto })
|
||||
async updateEpisodePlayState(
|
||||
@GetUser() user: User,
|
||||
@Param('tmdbId') tmdbId: number,
|
||||
@Param('seasonNumber') seasonNumber: number,
|
||||
@Param('episodeNumber') episodeNumber: number,
|
||||
@Body() playStateDto: UpdatePlayStateDto,
|
||||
): Promise<PlayStateDto> {
|
||||
const tmdbIdNumber = Number(tmdbId);
|
||||
const seasonNumberNumber = Number(seasonNumber);
|
||||
const episodeNumberNumber = Number(episodeNumber);
|
||||
|
||||
if (
|
||||
isNaN(tmdbIdNumber) ||
|
||||
isNaN(seasonNumberNumber) ||
|
||||
isNaN(episodeNumberNumber)
|
||||
) {
|
||||
throw new Error('Invalid tmdbId, seasonNumber, or episodeNumber');
|
||||
}
|
||||
|
||||
return this.playStateService.updateOrCreatePlayState({
|
||||
user,
|
||||
tmdbId: tmdbIdNumber,
|
||||
seasonNumber: seasonNumberNumber,
|
||||
episodeNumber: episodeNumberNumber,
|
||||
updatePlayStateDto: playStateDto,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { PlayState } from './play-state.entity';
|
||||
import { OmitType, PartialType } from '@nestjs/swagger';
|
||||
|
||||
export class PlayStateDto extends PlayState {}
|
||||
|
||||
export class UpdatePlayStateDto extends PartialType(
|
||||
OmitType(PlayState, [
|
||||
'user',
|
||||
'id',
|
||||
'episodeNumber',
|
||||
'tmdbId',
|
||||
'seasonNumber',
|
||||
]),
|
||||
) {}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PlayStateService } from './play-state.service';
|
||||
import { playStateProviders } from './play-state.providers';
|
||||
import { PlayStateController } from './play-state.controller';
|
||||
import { DatabaseModule } from '../../database/database.module';
|
||||
import { UsersModule } from '../users.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, UsersModule],
|
||||
providers: [PlayStateService, ...playStateProviders],
|
||||
controllers: [PlayStateController],
|
||||
})
|
||||
export class PlayStateModule {}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DATA_SOURCE } from '../../database/database.providers';
|
||||
import { PlayState } from './play-state.entity';
|
||||
|
||||
export const PLAY_STATE_REPOSITORY = 'PLAY_STATE_REPOSITORY';
|
||||
|
||||
export const playStateProviders = [
|
||||
{
|
||||
provide: PLAY_STATE_REPOSITORY,
|
||||
useFactory: (dataSource: DataSource) => dataSource.getRepository(PlayState),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
];
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { User } from '../user.entity';
|
||||
import { PLAY_STATE_REPOSITORY } from './play-state.providers';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PlayState } from './play-state.entity';
|
||||
import { UpdatePlayStateDto } from './play-state.dtos';
|
||||
|
||||
@Injectable()
|
||||
export class PlayStateService {
|
||||
constructor(
|
||||
@Inject(PLAY_STATE_REPOSITORY)
|
||||
private readonly playStateRepository: Repository<PlayState>,
|
||||
) {}
|
||||
|
||||
getPlayState(user: User, tmdbId: number) {
|
||||
return this.playStateRepository.find({
|
||||
where: { user: { id: user.id }, tmdbId },
|
||||
});
|
||||
}
|
||||
|
||||
getEpisodePlayState(
|
||||
user: User,
|
||||
tmdbId: number,
|
||||
seasonNumber: number,
|
||||
episodeNumber: number,
|
||||
) {
|
||||
return this.playStateRepository.findOne({
|
||||
where: { user: { id: user.id }, tmdbId, seasonNumber, episodeNumber },
|
||||
});
|
||||
}
|
||||
|
||||
async updateOrCreatePlayState({
|
||||
user,
|
||||
tmdbId,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
updatePlayStateDto,
|
||||
}: {
|
||||
user: User;
|
||||
tmdbId: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
updatePlayStateDto: UpdatePlayStateDto;
|
||||
}) {
|
||||
let playState = await this.playStateRepository.findOne({
|
||||
where: { user: { id: user.id }, tmdbId, seasonNumber, episodeNumber },
|
||||
});
|
||||
|
||||
console.log('playState:', playState);
|
||||
|
||||
if (playState) {
|
||||
playState.showInUpNext =
|
||||
updatePlayStateDto.showInUpNext ?? playState.showInUpNext;
|
||||
playState.progress = updatePlayStateDto.progress ?? playState.progress;
|
||||
playState.watched = updatePlayStateDto.watched ?? playState.watched;
|
||||
} else {
|
||||
playState = this.playStateRepository.create({
|
||||
tmdbId,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
progress: updatePlayStateDto.progress,
|
||||
watched: updatePlayStateDto.watched,
|
||||
showInUpNext: updatePlayStateDto.showInUpNext,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
return this.playStateRepository.save(playState);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,30 @@
|
||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { User } from '../user.entity';
|
||||
import { Title } from './title.entity';
|
||||
|
||||
@Entity()
|
||||
export class PlayState {
|
||||
@ApiProperty({ required: true })
|
||||
export class Media {
|
||||
@ApiProperty({ required: false })
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@Column()
|
||||
tmdbId: number;
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ nullable: true })
|
||||
seasonNumber?: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ nullable: true })
|
||||
seasonNumber: number;
|
||||
episodeNumber?: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ nullable: true })
|
||||
episodeNumber: number;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@Column({ type: 'double' })
|
||||
@Column({ default: 0 })
|
||||
progress: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ default: false })
|
||||
watched: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ default: true })
|
||||
showInUpNext: boolean;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.playStates)
|
||||
user: User;
|
||||
@ApiProperty({ required: true, type: () => Title })
|
||||
@ManyToOne(() => Title, (title) => title.media)
|
||||
title: Title;
|
||||
}
|
||||
29
backend/src/users/titles/title.dtos.ts
Normal file
29
backend/src/users/titles/title.dtos.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Title } from './title.entity';
|
||||
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
|
||||
import { Media } from './media.entity';
|
||||
|
||||
export class TitleDto extends Title {
|
||||
static fromEntity(title: Title): TitleDto {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateTitleDto extends PartialType(
|
||||
OmitType(Title, ['user', 'id', 'tmdbId']),
|
||||
) {}
|
||||
|
||||
export class UpdateProgressDto {
|
||||
@ApiProperty({ required: false })
|
||||
progress?: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
watched?: boolean;
|
||||
}
|
||||
|
||||
export class ContinueWatchingDto {
|
||||
@ApiProperty({ required: false, type: Media, nullable: true })
|
||||
nextEpisode?: Media;
|
||||
|
||||
@ApiProperty({ type: Title })
|
||||
title: Title;
|
||||
}
|
||||
83
backend/src/users/titles/title.entity.ts
Normal file
83
backend/src/users/titles/title.entity.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { User } from '../user.entity';
|
||||
import { Media } from './media.entity';
|
||||
|
||||
export enum TitleType {
|
||||
movie = 'movie',
|
||||
series = 'series',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Title {
|
||||
@ApiProperty({ required: false })
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@Column()
|
||||
tmdbId: number;
|
||||
|
||||
@ApiProperty({ required: true, enum: TitleType })
|
||||
@Column({ type: 'simple-enum', enum: TitleType, default: TitleType.movie })
|
||||
type: TitleType;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ default: false })
|
||||
upNext: boolean; // TODO: Rename to continueWatching
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ default: false })
|
||||
isInLibrary: boolean = false; // TODO: Remove
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@Column({ default: false })
|
||||
watched: boolean = false;
|
||||
|
||||
@ApiProperty({ required: false, type: () => Media, isArray: true })
|
||||
@OneToMany(() => Media, (media) => media.title)
|
||||
media: Media[];
|
||||
|
||||
@ManyToOne(() => User, (user) => user.titles)
|
||||
user: User;
|
||||
}
|
||||
|
||||
// @Entity()
|
||||
// export class PlayState {
|
||||
// @ApiProperty({ required: true })
|
||||
// @PrimaryGeneratedColumn('uuid')
|
||||
// id: string;
|
||||
//
|
||||
// @ApiProperty({ required: true })
|
||||
// @Column()
|
||||
// tmdbId: number;
|
||||
//
|
||||
// @ApiProperty({ required: false })
|
||||
// @Column({ nullable: true })
|
||||
// seasonNumber: number;
|
||||
//
|
||||
// @ApiProperty({ required: false })
|
||||
// @Column({ nullable: true })
|
||||
// episodeNumber: number;
|
||||
//
|
||||
// @ApiProperty({ required: true })
|
||||
// @Column({ type: 'double' })
|
||||
// progress: number;
|
||||
//
|
||||
// @ApiProperty({ required: false })
|
||||
// @Column({ default: false })
|
||||
// watched: boolean;
|
||||
//
|
||||
// @ApiProperty({ required: false })
|
||||
// @Column({ default: true })
|
||||
// showInUpNext: boolean;
|
||||
//
|
||||
// @ManyToOne(() => User, (user) => user.playStates)
|
||||
// user: User;
|
||||
// }
|
||||
13
backend/src/users/titles/title.module.ts
Normal file
13
backend/src/users/titles/title.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TitleService } from './title.service';
|
||||
import { titleProviders } from './title.providers';
|
||||
import { TitlesController } from './titles.controller';
|
||||
import { DatabaseModule } from '../../database/database.module';
|
||||
import { UsersModule } from '../users.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, UsersModule],
|
||||
providers: [TitleService, ...titleProviders],
|
||||
controllers: [TitlesController],
|
||||
})
|
||||
export class TitleModule {}
|
||||
20
backend/src/users/titles/title.providers.ts
Normal file
20
backend/src/users/titles/title.providers.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DATA_SOURCE } from '../../database/database.providers';
|
||||
import { Title } from './title.entity';
|
||||
import { Media } from './media.entity';
|
||||
|
||||
export const TITLE_REPOSITORY = 'PLAY_STATE_REPOSITORY';
|
||||
export const MEDIA_REPOSITORY = 'MEDIA_REPOSITORY';
|
||||
|
||||
export const titleProviders = [
|
||||
{
|
||||
provide: TITLE_REPOSITORY,
|
||||
useFactory: (dataSource: DataSource) => dataSource.getRepository(Title),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
{
|
||||
provide: MEDIA_REPOSITORY,
|
||||
useFactory: (dataSource: DataSource) => dataSource.getRepository(Media),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
];
|
||||
185
backend/src/users/titles/title.service.ts
Normal file
185
backend/src/users/titles/title.service.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { User } from '../user.entity';
|
||||
import { MEDIA_REPOSITORY, TITLE_REPOSITORY } from './title.providers';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Title, TitleType } from './title.entity';
|
||||
import { UpdateProgressDto, UpdateTitleDto } from './title.dtos';
|
||||
import { Media } from './media.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TitleService {
|
||||
constructor(
|
||||
@Inject(TITLE_REPOSITORY)
|
||||
private readonly titleRepository: Repository<Title>,
|
||||
@Inject(MEDIA_REPOSITORY)
|
||||
private readonly mediaRepository: Repository<Media>,
|
||||
) {}
|
||||
|
||||
// getPlayState(user: User, tmdbId: number) {
|
||||
// return this.playStateRepository.find({
|
||||
// where: { user: { id: user.id }, tmdbId },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// getEpisodePlayState(
|
||||
// user: User,
|
||||
// tmdbId: number,
|
||||
// seasonNumber: number,
|
||||
// episodeNumber: number,
|
||||
// ) {
|
||||
// return this.playStateRepository.findOne({
|
||||
// where: { user: { id: user.id }, tmdbId, seasonNumber, episodeNumber },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// async updateOrCreatePlayState({
|
||||
// user,
|
||||
// tmdbId,
|
||||
// seasonNumber,
|
||||
// episodeNumber,
|
||||
// updatePlayStateDto,
|
||||
// }: {
|
||||
// user: User;
|
||||
// tmdbId: number;
|
||||
// seasonNumber?: number;
|
||||
// episodeNumber?: number;
|
||||
// updatePlayStateDto: UpdateTitleDto;
|
||||
// }) {
|
||||
// let playState = await this.playStateRepository.findOne({
|
||||
// where: { user: { id: user.id }, tmdbId, seasonNumber, episodeNumber },
|
||||
// });
|
||||
//
|
||||
// console.log('playState:', playState);
|
||||
//
|
||||
// if (playState) {
|
||||
// playState.showInUpNext =
|
||||
// updatePlayStateDto.showInUpNext ?? playState.showInUpNext;
|
||||
// playState.progress = updatePlayStateDto.progress ?? playState.progress;
|
||||
// playState.watched = updatePlayStateDto.watched ?? playState.watched;
|
||||
// } else {
|
||||
// playState = this.playStateRepository.create({
|
||||
// tmdbId,
|
||||
// seasonNumber,
|
||||
// episodeNumber,
|
||||
// progress: updatePlayStateDto.progress,
|
||||
// watched: updatePlayStateDto.watched,
|
||||
// showInUpNext: updatePlayStateDto.showInUpNext,
|
||||
// user,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return this.playStateRepository.save(playState);
|
||||
// }
|
||||
getLibrary(user: User) {
|
||||
return this.titleRepository.find({
|
||||
where: { user: { id: user.id }, isInLibrary: true },
|
||||
relations: ['media'],
|
||||
});
|
||||
}
|
||||
|
||||
getContinueWatching(user: User) {
|
||||
return this.titleRepository.find({
|
||||
where: { user: { id: user.id }, upNext: true },
|
||||
relations: ['media'],
|
||||
});
|
||||
}
|
||||
|
||||
getTitle(
|
||||
user: User,
|
||||
tmdbId: number,
|
||||
type: TitleType,
|
||||
): Promise<Title | undefined> {
|
||||
return this.titleRepository.findOne({
|
||||
where: { user: { id: user.id }, tmdbId, type },
|
||||
relations: ['media'],
|
||||
});
|
||||
}
|
||||
|
||||
async updateTitle(
|
||||
user: User,
|
||||
tmdbId: number,
|
||||
type: TitleType,
|
||||
updateTitleDto: UpdateTitleDto,
|
||||
) {
|
||||
const title = await this.getOrCreateTitle(user, tmdbId, type);
|
||||
|
||||
title.upNext = updateTitleDto.upNext ?? title.upNext;
|
||||
title.isInLibrary = updateTitleDto.isInLibrary ?? title.isInLibrary;
|
||||
|
||||
await this.titleRepository.save(title);
|
||||
|
||||
return this.getTitle(user, tmdbId, type);
|
||||
}
|
||||
|
||||
private async getOrCreateTitle(user: User, tmdbId: number, type: TitleType) {
|
||||
let title = await this.getTitle(user, tmdbId, type);
|
||||
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
title = this.titleRepository.create({
|
||||
tmdbId,
|
||||
user,
|
||||
type,
|
||||
});
|
||||
|
||||
await this.titleRepository.save(title);
|
||||
|
||||
return this.getTitle(user, tmdbId, type);
|
||||
}
|
||||
|
||||
async updateProgress(
|
||||
user: User,
|
||||
tmdbId: number,
|
||||
type: TitleType,
|
||||
season: number | undefined,
|
||||
episode: number | undefined,
|
||||
updateTitleDto: UpdateProgressDto,
|
||||
) {
|
||||
const media = await this.getOrCreateMedia(
|
||||
user,
|
||||
tmdbId,
|
||||
type,
|
||||
season,
|
||||
episode,
|
||||
);
|
||||
|
||||
media.progress = updateTitleDto.progress ?? media.progress;
|
||||
media.watched = updateTitleDto.watched ?? media.watched;
|
||||
|
||||
await this.mediaRepository.save(media);
|
||||
|
||||
return this.getTitle(user, tmdbId, type);
|
||||
}
|
||||
|
||||
private async getOrCreateMedia(
|
||||
user: User,
|
||||
tmdbId: number,
|
||||
type: TitleType,
|
||||
season?: number,
|
||||
episode?: number,
|
||||
) {
|
||||
const title = await this.getOrCreateTitle(user, tmdbId, type);
|
||||
|
||||
let media = await this.mediaRepository.findOne({
|
||||
where: {
|
||||
title: { id: title.id },
|
||||
seasonNumber: season,
|
||||
episodeNumber: episode,
|
||||
},
|
||||
});
|
||||
|
||||
if (media) {
|
||||
return media;
|
||||
}
|
||||
|
||||
media = this.mediaRepository.create({
|
||||
title,
|
||||
seasonNumber: season,
|
||||
episodeNumber: episode,
|
||||
});
|
||||
|
||||
return this.mediaRepository.save(media);
|
||||
}
|
||||
}
|
||||
238
backend/src/users/titles/titles.controller.ts
Normal file
238
backend/src/users/titles/titles.controller.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard, GetUser } from '../../auth/auth.guard';
|
||||
import { User } from '../user.entity';
|
||||
import {
|
||||
ContinueWatchingDto,
|
||||
TitleDto,
|
||||
UpdateProgressDto,
|
||||
UpdateTitleDto,
|
||||
} from './title.dtos';
|
||||
import { TitleService } from './title.service';
|
||||
import { ApiOkResponse, ApiQuery } from '@nestjs/swagger';
|
||||
import { TitleType } from './title.entity';
|
||||
|
||||
@Controller('titles')
|
||||
@UseGuards(AuthGuard)
|
||||
export class TitlesController {
|
||||
constructor(private titleService: TitleService) {}
|
||||
|
||||
/*
|
||||
GET /library
|
||||
GET /continue-watching
|
||||
GET /:tmdbId
|
||||
|
||||
PUT /:tmdbId 1. Add to library -> Title created
|
||||
|
||||
2. Update progress -> Media created
|
||||
3. Set title.upNext = true (and title.watched if necessary)
|
||||
4. Set media.watched = calculate
|
||||
5. If watched = true, set progress to 0 for next episode (create)
|
||||
PUT /progress/:tmdbId
|
||||
PUT /progress/:tmdbId/season/:season/episode/:episode
|
||||
*/
|
||||
|
||||
@Get('library')
|
||||
@ApiOkResponse({ type: TitleDto, isArray: true })
|
||||
async getLibrary(@GetUser() user: User): Promise<TitleDto[]> {
|
||||
return this.titleService
|
||||
.getLibrary(user)
|
||||
.then((r) => r.map(TitleDto.fromEntity));
|
||||
}
|
||||
|
||||
@Get('continue-watching')
|
||||
@ApiOkResponse({ type: ContinueWatchingDto, isArray: true })
|
||||
async getContinueWatching(
|
||||
@GetUser() user: User,
|
||||
): Promise<ContinueWatchingDto[]> {
|
||||
const titles = await this.titleService.getContinueWatching(user);
|
||||
|
||||
return titles.map((title) => {
|
||||
title.media.sort((a, b) => {
|
||||
if (a.seasonNumber === b.seasonNumber) {
|
||||
return a.episodeNumber - b.episodeNumber;
|
||||
}
|
||||
|
||||
return a.seasonNumber - b.seasonNumber;
|
||||
});
|
||||
|
||||
const nextEpisode = title.media.find((media) => !media.watched);
|
||||
|
||||
return {
|
||||
title: TitleDto.fromEntity(title),
|
||||
nextEpisode,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':tmdbId')
|
||||
@ApiOkResponse({ type: TitleDto })
|
||||
@ApiQuery({ name: 'type', enum: TitleType })
|
||||
async getTitle(
|
||||
@GetUser() user: User,
|
||||
@Query('type') type: TitleType,
|
||||
@Param('tmdbId') tmdbId: number,
|
||||
): Promise<TitleDto> {
|
||||
const tmdbIdNumber = Number(tmdbId);
|
||||
|
||||
if (isNaN(tmdbIdNumber)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return this.titleService.getTitle(user, tmdbIdNumber, type);
|
||||
}
|
||||
|
||||
@Put(':tmdbId')
|
||||
@ApiOkResponse({ type: TitleDto })
|
||||
@ApiQuery({ name: 'type', enum: TitleType })
|
||||
async updateTitle(
|
||||
@GetUser() user: User,
|
||||
@Param('tmdbId') tmdbId: number,
|
||||
@Query('type') type: TitleType,
|
||||
@Body() updateTitleDto: UpdateTitleDto,
|
||||
): Promise<TitleDto> {
|
||||
const tmdbIdNumber = Number(tmdbId);
|
||||
|
||||
if (isNaN(tmdbIdNumber)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return this.titleService.updateTitle(
|
||||
user,
|
||||
tmdbIdNumber,
|
||||
type,
|
||||
updateTitleDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Put('progress/:tmdbId')
|
||||
@Put('progress/:tmdbId/season/:season/episode/:episode')
|
||||
@ApiOkResponse({ type: TitleDto })
|
||||
@ApiQuery({ name: 'type', enum: TitleType })
|
||||
async updateProgress(
|
||||
@GetUser() user: User,
|
||||
@Param('tmdbId') tmdbId: number,
|
||||
@Param('season') season: number | undefined,
|
||||
@Param('episode') episode: number | undefined,
|
||||
@Query('type') type: TitleType,
|
||||
@Body() updateTitleDto: UpdateProgressDto,
|
||||
): Promise<TitleDto> {
|
||||
const tmdbIdNumber = Number(tmdbId);
|
||||
const seasonNumber = Number(season) || undefined;
|
||||
const episodeNumber = Number(episode) || undefined;
|
||||
|
||||
if (isNaN(tmdbIdNumber)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return this.titleService.updateProgress(
|
||||
user,
|
||||
tmdbIdNumber,
|
||||
type,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
updateTitleDto,
|
||||
);
|
||||
}
|
||||
|
||||
// @Get(':tmdbId')
|
||||
// @ApiOkResponse({ type: TitleDto, isArray: true })
|
||||
// async getPlayState(
|
||||
// @GetUser() user: User,
|
||||
// @Param('tmdbId') tmdbId: number,
|
||||
// ): Promise<TitleDto[]> {
|
||||
// const tmdbIdNumber = Number(tmdbId);
|
||||
//
|
||||
// if (isNaN(tmdbIdNumber)) {
|
||||
// throw new Error('Invalid tmdbId');
|
||||
// }
|
||||
//
|
||||
// return this.playStateService.getPlayState(user, tmdbIdNumber);
|
||||
// }
|
||||
//
|
||||
// @Get(':tmdbId/season/:seasonNumber/episode/:episodeNumber')
|
||||
// @ApiOkResponse({ type: TitleDto })
|
||||
// async getEpisodePlayState(
|
||||
// @GetUser() user: User,
|
||||
// @Param('tmdbId') tmdbId: number,
|
||||
// @Param('seasonNumber') seasonNumber: number,
|
||||
// @Param('episodeNumber') episodeNumber: number,
|
||||
// ): Promise<TitleDto> {
|
||||
// const tmdbIdNumber = Number(tmdbId);
|
||||
// const seasonNumberNumber = Number(seasonNumber);
|
||||
// const episodeNumberNumber = Number(episodeNumber);
|
||||
//
|
||||
// if (
|
||||
// isNaN(tmdbIdNumber) ||
|
||||
// isNaN(seasonNumberNumber) ||
|
||||
// isNaN(episodeNumberNumber)
|
||||
// ) {
|
||||
// throw new NotFoundException();
|
||||
// }
|
||||
//
|
||||
// return this.playStateService.getEpisodePlayState(
|
||||
// user,
|
||||
// tmdbIdNumber,
|
||||
// seasonNumberNumber,
|
||||
// episodeNumberNumber,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// @Put(':tmdbId')
|
||||
// @ApiOkResponse({ type: TitleDto })
|
||||
// async updatePlayState(
|
||||
// @GetUser() user: User,
|
||||
// @Param('tmdbId') tmdbId: number,
|
||||
// @Body() playStateDto: UpdateTitleDto,
|
||||
// ): Promise<TitleDto> {
|
||||
// const tmdbIdNumber = Number(tmdbId);
|
||||
//
|
||||
// if (isNaN(tmdbIdNumber)) {
|
||||
// throw new Error('Invalid tmdbId');
|
||||
// }
|
||||
//
|
||||
// return this.playStateService.updateOrCreatePlayState({
|
||||
// user,
|
||||
// tmdbId: tmdbIdNumber,
|
||||
// updatePlayStateDto: playStateDto,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// @Put(':tmdbId/season/:seasonNumber/episode/:episodeNumber')
|
||||
// @ApiOkResponse({ type: TitleDto })
|
||||
// async updateEpisodePlayState(
|
||||
// @GetUser() user: User,
|
||||
// @Param('tmdbId') tmdbId: number,
|
||||
// @Param('seasonNumber') seasonNumber: number,
|
||||
// @Param('episodeNumber') episodeNumber: number,
|
||||
// @Body() playStateDto: UpdateTitleDto,
|
||||
// ): Promise<TitleDto> {
|
||||
// const tmdbIdNumber = Number(tmdbId);
|
||||
// const seasonNumberNumber = Number(seasonNumber);
|
||||
// const episodeNumberNumber = Number(episodeNumber);
|
||||
//
|
||||
// if (
|
||||
// isNaN(tmdbIdNumber) ||
|
||||
// isNaN(seasonNumberNumber) ||
|
||||
// isNaN(episodeNumberNumber)
|
||||
// ) {
|
||||
// throw new Error('Invalid tmdbId, seasonNumber, or episodeNumber');
|
||||
// }
|
||||
//
|
||||
// return this.playStateService.updateOrCreatePlayState({
|
||||
// user,
|
||||
// tmdbId: tmdbIdNumber,
|
||||
// seasonNumber: seasonNumberNumber,
|
||||
// episodeNumber: episodeNumberNumber,
|
||||
// updatePlayStateDto: playStateDto,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger';
|
||||
import { User } from './user.entity';
|
||||
import { MyListItemDto } from './my-list/my-list.dtos';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
|
||||
export class UserDto extends OmitType(User, [
|
||||
'password',
|
||||
'profilePicture',
|
||||
'playStates',
|
||||
'titles',
|
||||
'myListItems',
|
||||
] as const) {
|
||||
@ApiProperty({ type: 'string' })
|
||||
profilePicture: string | null;
|
||||
|
||||
@ApiProperty({ type: MyListItemDto, isArray: true })
|
||||
myList: MyListItemDto[];
|
||||
|
||||
static fromEntity(entity: User): UserDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
@@ -19,6 +24,7 @@ export class UserDto extends OmitType(User, [
|
||||
onboardingDone: entity.onboardingDone,
|
||||
profilePicture:
|
||||
'data:image;base64,' + entity.profilePicture?.toString('base64'),
|
||||
myList: entity.myListItems?.map((i) => plainToInstance(MyListItemDto, i)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PlayState } from './play-state/play-state.entity';
|
||||
import { Title } from './titles/title.entity';
|
||||
import { MyListItem } from './my-list/my-list-item.entity';
|
||||
|
||||
export class SonarrSettings {
|
||||
@@ -47,6 +47,9 @@ export class TmdbSettings {
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
libraryListId: string; // TODO: Change to number?
|
||||
}
|
||||
|
||||
export class Settings {
|
||||
@@ -106,6 +109,7 @@ const DEFAULT_SETTINGS: Settings = {
|
||||
tmdb: {
|
||||
sessionId: '',
|
||||
userId: '',
|
||||
libraryListId: '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -140,8 +144,8 @@ export class User {
|
||||
@Column('json', { default: JSON.stringify(DEFAULT_SETTINGS) })
|
||||
settings = DEFAULT_SETTINGS;
|
||||
|
||||
@OneToMany(() => PlayState, (playState) => playState.user)
|
||||
playStates: PlayState[];
|
||||
@OneToMany(() => Title, (playState) => playState.user)
|
||||
titles: Title[];
|
||||
|
||||
@OneToMany(() => MyListItem, (myListItem) => myListItem.user)
|
||||
myListItems: MyListItem[];
|
||||
|
||||
@@ -18,15 +18,21 @@ export class UsersService {
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.userRepository.find();
|
||||
return this.userRepository.find({ relations: ['myListItems'] });
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<User> {
|
||||
return this.userRepository.findOne({ where: { id } });
|
||||
return this.userRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['myListItems'],
|
||||
});
|
||||
}
|
||||
|
||||
async findOneByName(name: string): Promise<User> {
|
||||
return this.userRepository.findOne({ where: { name } });
|
||||
return this.userRepository.findOne({
|
||||
where: { name },
|
||||
relations: ['myListItems'],
|
||||
});
|
||||
}
|
||||
|
||||
async create(userCreateDto: CreateUserDto): Promise<User> {
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "reiverr",
|
||||
"version": "2.0.0-alpha.6",
|
||||
"dependencies": {
|
||||
"class-transformer": "^0.5.1",
|
||||
"gsap": "^3.12.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3391,6 +3392,11 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.4.0.tgz",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"class-transformer": "^0.5.1",
|
||||
"gsap": "^3.12.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,12 @@ export type CreateReiverrUser = components['schemas']['CreateUserDto'];
|
||||
export type UpdateReiverrUser = components['schemas']['UpdateUserDto'];
|
||||
export type ReiverrSettings = ReiverrUser['settings'];
|
||||
|
||||
export type PlayStateDto = components['schemas']['PlayStateDto'];
|
||||
export type UpdatePlayStateDto = components['schemas']['UpdatePlayStateDto'];
|
||||
// export type PlayStateDto = components['schemas']['PlayStateDto'];
|
||||
// export type UpdatePlayStateDto = components['schemas']['UpdatePlayStateDto'];
|
||||
|
||||
export type MyListDto = components['schemas']['MyListItemDto'];
|
||||
export type TitleDto = components['schemas']['TitleDto'];
|
||||
export type TitleType = 'movie' | 'series';
|
||||
|
||||
export class ReiverrApi implements Api<paths> {
|
||||
getClient(basePath?: string, _token?: string) {
|
||||
@@ -62,65 +66,90 @@ export class ReiverrApi implements Api<paths> {
|
||||
})
|
||||
.then((res) => ({ user: res.data, error: res.error?.message }));
|
||||
|
||||
getMyList = () =>
|
||||
getLibrary = () =>
|
||||
this.getClient()
|
||||
?.GET('/my-list')
|
||||
?.GET('/titles/library')
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
addToMyList = (tmdbId: number) =>
|
||||
addToLibrary = (tmdbId: number, type: TitleType) =>
|
||||
this.getClient()
|
||||
?.POST('/my-list/{tmdbId}', { params: { path: { tmdbId } } })
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
removeFromMyList = (tmdbId: number) =>
|
||||
this.getClient()
|
||||
?.DELETE('/my-list/{tmdbId}', { params: { path: { tmdbId } } })
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
getPlayState = (tmdbId: number, seasonNumber?: number, episodeNumber?: number) => {
|
||||
if (seasonNumber !== undefined && episodeNumber !== undefined) {
|
||||
return this.getClient()
|
||||
?.GET('/play-state/{tmdbId}/season/{seasonNumber}/episode/{episodeNumber}', {
|
||||
params: {
|
||||
path: {
|
||||
tmdbId,
|
||||
seasonNumber,
|
||||
episodeNumber
|
||||
}
|
||||
?.PUT('fin', {
|
||||
params: {
|
||||
path: { tmdbId },
|
||||
query: {
|
||||
type
|
||||
}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
} else {
|
||||
return this.getClient()
|
||||
?.GET('/play-state/{tmdbId}', { params: { path: { tmdbId } } })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
};
|
||||
},
|
||||
body: {
|
||||
isInLibrary: true
|
||||
}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
|
||||
setPlayState = (
|
||||
tmdbId: number,
|
||||
seasonNumber: number | undefined,
|
||||
episodeNumber: number | undefined,
|
||||
playState: UpdatePlayStateDto
|
||||
) => {
|
||||
if (seasonNumber !== undefined && episodeNumber !== undefined) {
|
||||
return this.getClient()
|
||||
?.PUT('/play-state/{tmdbId}/season/{seasonNumber}/episode/{episodeNumber}', {
|
||||
body: playState,
|
||||
params: {
|
||||
path: { tmdbId, seasonNumber, episodeNumber }
|
||||
removeFromLibrary = (tmdbId: number, type: TitleType) =>
|
||||
this.getClient()
|
||||
?.PUT('/titles/{tmdbId}', {
|
||||
params: {
|
||||
path: { tmdbId },
|
||||
query: {
|
||||
type
|
||||
}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
} else {
|
||||
return this.getClient()
|
||||
?.PUT('/play-state/{tmdbId}', {
|
||||
body: playState,
|
||||
params: { path: { tmdbId } }
|
||||
})
|
||||
.then((r) => r.data);
|
||||
}
|
||||
};
|
||||
},
|
||||
body: {
|
||||
isInLibrary: false
|
||||
}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
|
||||
getContinueWatching = () =>
|
||||
this.getClient()
|
||||
?.GET('/titles/continue-watching')
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
// getPlayState = (tmdbId: number, seasonNumber?: number, episodeNumber?: number) => {
|
||||
// if (seasonNumber !== undefined && episodeNumber !== undefined) {
|
||||
// return this.getClient()
|
||||
// ?.GET('/play-state/{tmdbId}/season/{seasonNumber}/episode/{episodeNumber}', {
|
||||
// params: {
|
||||
// path: {
|
||||
// tmdbId,
|
||||
// seasonNumber,
|
||||
// episodeNumber
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .then((r) => r.data);
|
||||
// } else {
|
||||
// return this.getClient()
|
||||
// ?.GET('/play-state/{tmdbId}', { params: { path: { tmdbId } } })
|
||||
// .then((r) => r.data);
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// setPlayState = (
|
||||
// tmdbId: number,
|
||||
// seasonNumber: number | undefined,
|
||||
// episodeNumber: number | undefined,
|
||||
// playState: UpdatePlayStateDto
|
||||
// ) => {
|
||||
// if (seasonNumber !== undefined && episodeNumber !== undefined) {
|
||||
// return this.getClient()
|
||||
// ?.PUT('/play-state/{tmdbId}/season/{seasonNumber}/episode/{episodeNumber}', {
|
||||
// body: playState,
|
||||
// params: {
|
||||
// path: { tmdbId, seasonNumber, episodeNumber }
|
||||
// }
|
||||
// })
|
||||
// .then((r) => r.data);
|
||||
// } else {
|
||||
// return this.getClient()
|
||||
// ?.PUT('/play-state/{tmdbId}', {
|
||||
// body: playState,
|
||||
// params: { path: { tmdbId } }
|
||||
// })
|
||||
// .then((r) => r.data);
|
||||
// }
|
||||
// };
|
||||
}
|
||||
|
||||
export const reiverrApi = new ReiverrApi();
|
||||
|
||||
134
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
134
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
@@ -24,13 +24,18 @@ export interface paths {
|
||||
post: operations["MyListController_addToMyList"];
|
||||
delete: operations["MyListController_removeFromMyList"];
|
||||
};
|
||||
"/play-state/{tmdbId}": {
|
||||
get: operations["PlayStateController_getPlayState"];
|
||||
put: operations["PlayStateController_updatePlayState"];
|
||||
"/titles/library": {
|
||||
get: operations["TitlesController_getLibrary"];
|
||||
};
|
||||
"/play-state/{tmdbId}/season/{seasonNumber}/episode/{episodeNumber}": {
|
||||
get: operations["PlayStateController_getEpisodePlayState"];
|
||||
put: operations["PlayStateController_updateEpisodePlayState"];
|
||||
"/titles/continue-watching": {
|
||||
get: operations["TitlesController_getContinueWatching"];
|
||||
};
|
||||
"/titles/{tmdbId}": {
|
||||
get: operations["TitlesController_getTitle"];
|
||||
put: operations["TitlesController_updateTitle"];
|
||||
};
|
||||
"/titles/progress/{tmdbId}": {
|
||||
put: operations["TitlesController_updateProgress"];
|
||||
};
|
||||
"/": {
|
||||
get: operations["AppController_getHello"];
|
||||
@@ -65,6 +70,7 @@ export interface components {
|
||||
TmdbSettings: {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
libraryListId: string;
|
||||
};
|
||||
Settings: {
|
||||
autoplayTrailers: boolean;
|
||||
@@ -76,6 +82,10 @@ export interface components {
|
||||
peerflix: components["schemas"]["PeerflixSettings"];
|
||||
tmdb: components["schemas"]["TmdbSettings"];
|
||||
};
|
||||
MyListItemDto: {
|
||||
id: string;
|
||||
tmdbId: number;
|
||||
};
|
||||
UserDto: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -83,6 +93,7 @@ export interface components {
|
||||
onboardingDone?: boolean;
|
||||
settings: components["schemas"]["Settings"];
|
||||
profilePicture: string;
|
||||
myList: components["schemas"]["MyListItemDto"][];
|
||||
};
|
||||
CreateUserDto: {
|
||||
name: string;
|
||||
@@ -107,23 +118,49 @@ export interface components {
|
||||
accessToken: string;
|
||||
user: components["schemas"]["UserDto"];
|
||||
};
|
||||
MyListItemDto: {
|
||||
id: string;
|
||||
Title: {
|
||||
id?: string;
|
||||
tmdbId: number;
|
||||
/** @enum {string} */
|
||||
type: "movie" | "series";
|
||||
upNext?: boolean;
|
||||
isInLibrary?: boolean;
|
||||
watched?: boolean;
|
||||
media?: components["schemas"]["Media"][];
|
||||
};
|
||||
PlayStateDto: {
|
||||
id: string;
|
||||
tmdbId: number;
|
||||
Media: {
|
||||
id?: string;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
progress: number;
|
||||
watched?: boolean;
|
||||
showInUpNext?: boolean;
|
||||
};
|
||||
UpdatePlayStateDto: {
|
||||
progress?: number;
|
||||
watched?: boolean;
|
||||
showInUpNext?: boolean;
|
||||
title: components["schemas"]["Title"];
|
||||
};
|
||||
TitleDto: {
|
||||
id?: string;
|
||||
tmdbId: number;
|
||||
/** @enum {string} */
|
||||
type: "movie" | "series";
|
||||
upNext?: boolean;
|
||||
isInLibrary?: boolean;
|
||||
watched?: boolean;
|
||||
media?: components["schemas"]["Media"][];
|
||||
};
|
||||
ContinueWatchingDto: {
|
||||
nextEpisode?: components["schemas"]["Media"] | null;
|
||||
title: components["schemas"]["Title"];
|
||||
};
|
||||
UpdateTitleDto: {
|
||||
/** @enum {string} */
|
||||
type?: "movie" | "series";
|
||||
upNext?: boolean;
|
||||
isInLibrary?: boolean;
|
||||
watched?: boolean;
|
||||
media?: components["schemas"]["Media"][];
|
||||
};
|
||||
UpdateProgressDto: {
|
||||
progress?: number;
|
||||
watched?: boolean;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
@@ -336,8 +373,29 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
PlayStateController_getPlayState: {
|
||||
TitlesController_getLibrary: {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["TitleDto"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
TitlesController_getContinueWatching: {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ContinueWatchingDto"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
TitlesController_getTitle: {
|
||||
parameters: {
|
||||
query: {
|
||||
type: "movie" | "series";
|
||||
};
|
||||
path: {
|
||||
tmdbId: number;
|
||||
};
|
||||
@@ -345,63 +403,53 @@ export interface operations {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PlayStateDto"];
|
||||
"application/json": components["schemas"]["TitleDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
PlayStateController_updatePlayState: {
|
||||
TitlesController_updateTitle: {
|
||||
parameters: {
|
||||
query: {
|
||||
type: "movie" | "series";
|
||||
};
|
||||
path: {
|
||||
tmdbId: number;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdatePlayStateDto"];
|
||||
"application/json": components["schemas"]["UpdateTitleDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PlayStateDto"];
|
||||
"application/json": components["schemas"]["TitleDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
PlayStateController_getEpisodePlayState: {
|
||||
TitlesController_updateProgress: {
|
||||
parameters: {
|
||||
query: {
|
||||
type: "movie" | "series";
|
||||
};
|
||||
path: {
|
||||
tmdbId: number;
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PlayStateDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
PlayStateController_updateEpisodePlayState: {
|
||||
parameters: {
|
||||
path: {
|
||||
tmdbId: number;
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
season: number;
|
||||
episode: number;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdatePlayStateDto"];
|
||||
"application/json": components["schemas"]["UpdateProgressDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PlayStateDto"];
|
||||
"application/json": components["schemas"]["TitleDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ export class SonarrApi implements ApiAsync<paths> {
|
||||
tmdbToTvdb = async (tmdbId: number) => {
|
||||
if (!get(tmdbToTvdbCache)[tmdbId]) {
|
||||
const tvdbId = await tmdbApi
|
||||
.getTmdbSeries(tmdbId)
|
||||
.getSeries(tmdbId)
|
||||
.then((series) => series?.external_ids.tvdb_id || 0);
|
||||
tmdbToTvdbCache.update((prev) => ({ ...prev, [tmdbId]: tvdbId }));
|
||||
return tvdbId;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { settings } from '../../stores/settings.store';
|
||||
import type { TitleType } from '../../types';
|
||||
import type { Api } from '../api.interface';
|
||||
import { user } from '../../stores/user.store';
|
||||
import { sessions } from '../../stores/session.store';
|
||||
|
||||
const CACHE_ONE_DAY = 'max-age=86400';
|
||||
const CACHE_FOUR_DAYS = 'max-age=345600';
|
||||
@@ -63,6 +64,23 @@ export class TmdbApi implements Api<paths> {
|
||||
});
|
||||
}
|
||||
|
||||
static getProxyClient() {
|
||||
const session = get(sessions).activeSession;
|
||||
const token = session?.token;
|
||||
|
||||
return createClient<paths>({
|
||||
baseUrl: session?.baseUrl + '/api/proxy/tmdb',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TMDB_API_KEY}`
|
||||
}
|
||||
// ...(token && {
|
||||
// headers: {
|
||||
// Authorization: 'Bearer ' + token
|
||||
// }
|
||||
// })
|
||||
});
|
||||
}
|
||||
|
||||
static getClient4() {
|
||||
return createClient<paths4>({
|
||||
baseUrl: 'https://api.themoviedb.org',
|
||||
@@ -95,6 +113,10 @@ export class TmdbApi implements Api<paths> {
|
||||
return TmdbApi.getClient4l();
|
||||
}
|
||||
|
||||
getProxyClient() {
|
||||
return TmdbApi.getProxyClient();
|
||||
}
|
||||
|
||||
getSessionId() {
|
||||
return get(user)?.settings.tmdb.sessionId;
|
||||
}
|
||||
@@ -105,8 +127,8 @@ export class TmdbApi implements Api<paths> {
|
||||
|
||||
// MOVIES
|
||||
|
||||
getTmdbMovie = async (tmdbId: number) => {
|
||||
return this.getClient()
|
||||
getMovie = async (tmdbId: number) => {
|
||||
return this.getProxyClient()
|
||||
?.GET('/3/movie/{movie_id}', {
|
||||
params: {
|
||||
path: {
|
||||
@@ -159,8 +181,8 @@ export class TmdbApi implements Api<paths> {
|
||||
return id;
|
||||
});
|
||||
|
||||
getTmdbSeries = async (tmdbId: number): Promise<TmdbSeriesFull2 | undefined> =>
|
||||
await this.getClient()
|
||||
getSeries = async (tmdbId: number): Promise<TmdbSeriesFull2 | undefined> =>
|
||||
await this.getProxyClient()
|
||||
?.GET('/3/tv/{series_id}', {
|
||||
params: {
|
||||
path: {
|
||||
@@ -491,6 +513,98 @@ export class TmdbApi implements Api<paths> {
|
||||
.then((res) => res.data)
|
||||
);
|
||||
};
|
||||
|
||||
getLibraryListId = async () => {
|
||||
let listId = get(user)?.settings.tmdb.libraryListId || '';
|
||||
|
||||
if (!listId) {
|
||||
listId = await this.getClient4l()
|
||||
?.POST('/4/list', {
|
||||
body: {
|
||||
// @ts-ignore
|
||||
description: 'Items added to library in Reiverr',
|
||||
name: 'reiverr-library',
|
||||
iso_3166_1: 'US',
|
||||
iso_639_1: 'en',
|
||||
public: false,
|
||||
private: true
|
||||
}
|
||||
})
|
||||
.then((r) => String(r.data?.id || ''));
|
||||
|
||||
await user.updateUser((u) => ({
|
||||
...u,
|
||||
settings: { ...u.settings, tmdb: { ...u.settings.tmdb, libraryListId: listId } }
|
||||
}));
|
||||
|
||||
console.assert(!!listId, 'Failed to create library list');
|
||||
}
|
||||
|
||||
return listId;
|
||||
};
|
||||
|
||||
getLibrary = async () => {
|
||||
const listId = await this.getLibraryListId();
|
||||
|
||||
return (
|
||||
this.getClient4l()
|
||||
?.GET('/4/list/{list_id}', {
|
||||
params: {
|
||||
path: {
|
||||
list_id: Number(listId)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => (res.data?.results as (TmdbMovie2 | TmdbSeries2)[]) || []) ||
|
||||
Promise.resolve([])
|
||||
);
|
||||
};
|
||||
|
||||
addToLibrary = async (tmdbId: number, type: TitleType) => {
|
||||
const listId = await this.getLibraryListId();
|
||||
|
||||
return this.getClient4l()
|
||||
.POST('/4/list/{list_id}/items', {
|
||||
params: {
|
||||
path: {
|
||||
list_id: Number(listId)
|
||||
}
|
||||
},
|
||||
body: {
|
||||
// @ts-ignore
|
||||
items: [
|
||||
{
|
||||
media_type: type === 'movie' ? 'movie' : 'tv',
|
||||
media_id: tmdbId
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
};
|
||||
|
||||
removeFromLibrary = async (tmdbId: number, type: TitleType) => {
|
||||
const listId = await this.getLibraryListId();
|
||||
|
||||
return this.getClient4l()
|
||||
.DELETE('/4/list/{list_id}/items', {
|
||||
params: {
|
||||
path: {
|
||||
list_id: Number(listId)
|
||||
}
|
||||
},
|
||||
body: {
|
||||
// @ts-ignore
|
||||
items: [
|
||||
{
|
||||
media_type: type === 'movie' ? 'movie' : 'tv',
|
||||
media_id: tmdbId
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
};
|
||||
}
|
||||
|
||||
export const tmdbApi = new TmdbApi();
|
||||
|
||||
@@ -6,7 +6,16 @@
|
||||
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants';
|
||||
import classNames from 'classnames';
|
||||
import { Cross1, DotFilled, ExternalLink, Play, Plus, Trash } from 'radix-icons-svelte';
|
||||
import {
|
||||
Check,
|
||||
Cross1,
|
||||
DotFilled,
|
||||
ExternalLink,
|
||||
Minus,
|
||||
Play,
|
||||
Plus,
|
||||
Trash
|
||||
} from 'radix-icons-svelte';
|
||||
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
|
||||
import {
|
||||
type EpisodeDownload,
|
||||
@@ -29,13 +38,11 @@
|
||||
import MMAddToSonarrDialog from '../MediaManagerModal/MMAddToSonarrDialog.svelte';
|
||||
import ConfirmDialog from '../Dialog/ConfirmDialog.svelte';
|
||||
import DownloadDetailsDialog from './DownloadDetailsDialog.svelte';
|
||||
import { myList } from '../../stores/library.store';
|
||||
|
||||
export let id: string;
|
||||
|
||||
const { promise: tmdbSeries, data: tmdbSeriesData } = useRequest(
|
||||
tmdbApi.getTmdbSeries,
|
||||
Number(id)
|
||||
);
|
||||
const { promise: tmdbSeries, data: tmdbSeriesData } = useRequest(tmdbApi.getSeries, Number(id));
|
||||
let sonarrItem = sonarrApi.getSeriesByTmdbId(Number(id));
|
||||
const { promise: recommendations } = useRequest(tmdbApi.getSeriesRecommendations, Number(id));
|
||||
|
||||
@@ -211,14 +218,13 @@
|
||||
{#await nextJellyfinEpisode then nextJellyfinEpisode}
|
||||
<Container
|
||||
direction="horizontal"
|
||||
class="flex mt-8"
|
||||
class="flex mt-8 space-x-4"
|
||||
focusOnMount
|
||||
on:back={handleGoBack}
|
||||
on:mount={registrar}
|
||||
>
|
||||
{#if nextJellyfinEpisode}
|
||||
<Button
|
||||
class="mr-4"
|
||||
on:clickOrSelect={() =>
|
||||
nextJellyfinEpisode?.Id && playerState.streamJellyfinId(nextJellyfinEpisode.Id)}
|
||||
>
|
||||
@@ -227,18 +233,28 @@
|
||||
<Play size={19} slot="icon" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="mr-4" action={() => handleRequestSeason(1)}>
|
||||
<Button action={() => handleRequestSeason(1)}>
|
||||
Request
|
||||
<Plus size={19} slot="icon" />
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if $myList?.series[id]}
|
||||
<Button icon={Check} action={() => myList.remove(Number(id), 'series')}>
|
||||
On My List
|
||||
</Button>
|
||||
{:else if $myList}
|
||||
<Button icon={Plus} action={() => myList.add(Number(id), 'series')}>
|
||||
Add to My List
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if PLATFORM_WEB}
|
||||
<Button class="mr-4">
|
||||
<Button>
|
||||
Open In TMDB
|
||||
<ExternalLink size={19} slot="icon-after" />
|
||||
</Button>
|
||||
<Button class="mr-4">
|
||||
<Button>
|
||||
Open In Jellyfin
|
||||
<ExternalLink size={19} slot="icon-after" />
|
||||
</Button>
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
audioStreamIndex: 0,
|
||||
audioTracks: [],
|
||||
selectAudioTrack: () => {},
|
||||
startTime: playState?.progress || 0
|
||||
// startTime: playState?.progress || 0
|
||||
startTime: 0
|
||||
};
|
||||
|
||||
console.log(tmdbId, seasonNumber, episodeNumber);
|
||||
|
||||
@@ -13,6 +13,32 @@
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import TmdbCard from '../components/Card/TmdbCard.svelte';
|
||||
import { tmdbApi, type TmdbMovie2, type TmdbSeries2 } from '../apis/tmdb/tmdb-api';
|
||||
import { reiverrApi } from '../apis/reiverr/reiverr-api';
|
||||
import { myList } from '../stores/library.store';
|
||||
|
||||
let tmdbLibraryItems: (TmdbSeries2 | TmdbMovie2)[] | undefined = undefined;
|
||||
|
||||
myList.subscribe((v) => {
|
||||
if (v) {
|
||||
tmdbLibraryItems = [
|
||||
...(Object.values(v.series) as TmdbSeries2[]),
|
||||
...(Object.values(v.movies) as TmdbMovie2[])
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// const reiverrLibraryItemsP = reiverrApi
|
||||
// .getLibrary()
|
||||
// .then(async (items) => {
|
||||
// return items.slice(0, 20);
|
||||
// })
|
||||
// .then((items) =>
|
||||
// Promise.all(
|
||||
// items.map((i) =>
|
||||
// i.type === 'movie' ? tmdbApi.getTmdbMovie(i.tmdbId) : tmdbApi.getTmdbSeries(i.tmdbId)
|
||||
// )
|
||||
// ).then((i) => i.filter((i) => !!i) as (TmdbMovie2 | TmdbSeries2)[])
|
||||
// );
|
||||
|
||||
const libraryItemsP = jellyfinApi.getLibraryItems();
|
||||
const sonarrDownloads: Promise<TmdbSeries2[]> = sonarrApi
|
||||
@@ -29,7 +55,7 @@
|
||||
let radarrDownloads: Promise<TmdbMovie2[]> = radarrApi
|
||||
.getDownloads()
|
||||
.then((items) =>
|
||||
Promise.all(items.map((i) => tmdbApi.getTmdbMovie(i.movie.tmdbId || -1))).then(
|
||||
Promise.all(items.map((i) => tmdbApi.getMovie(i.movie.tmdbId || -1))).then(
|
||||
(i) => i.filter((i) => !!i) as TmdbMovie2[]
|
||||
)
|
||||
);
|
||||
@@ -61,9 +87,9 @@
|
||||
</Carousel>
|
||||
{/if}
|
||||
{/await}
|
||||
<div class="px-32">
|
||||
<div class="mb-6">
|
||||
<div class="header2">Library</div>
|
||||
<div class="px-32 space-y-8">
|
||||
<div class="">
|
||||
<div class="header2">On Deck</div>
|
||||
</div>
|
||||
<CardGrid>
|
||||
{#await libraryItemsP}
|
||||
@@ -79,5 +105,25 @@
|
||||
{/each}
|
||||
{/await}
|
||||
</CardGrid>
|
||||
<div class="">
|
||||
<div class="header2">Your Library</div>
|
||||
</div>
|
||||
<CardGrid>
|
||||
{#if tmdbLibraryItems === undefined}
|
||||
<CarouselPlaceholderItems />
|
||||
{:else}
|
||||
{#each tmdbLibraryItems as item}
|
||||
<TmdbCard {item} on:enter={scrollIntoView({ all: 64 })} size="dynamic" navigateWithType />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!--{#await reiverrLibraryItemsP}-->
|
||||
<!-- <CarouselPlaceholderItems />-->
|
||||
<!--{:then items}-->
|
||||
<!-- {#each items as item}-->
|
||||
<!-- <TmdbCard {item} on:enter={scrollIntoView({ all: 64 })} size="dynamic" navigateWithType />-->
|
||||
<!-- {/each}-->
|
||||
<!--{/await}-->
|
||||
</CardGrid>
|
||||
</div>
|
||||
</DetachedPage>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
export let id: string;
|
||||
const tmdbId = Number(id);
|
||||
|
||||
const tmdbMovie = tmdbApi.getTmdbMovie(tmdbId);
|
||||
const tmdbMovie = tmdbApi.getMovie(tmdbId);
|
||||
$: recommendations = tmdbApi.getMovieRecommendations(tmdbId);
|
||||
const { promise: jellyfinItemP } = useRequest(
|
||||
(id: string) => jellyfinApi.getLibraryItemFromTmdbId(id),
|
||||
|
||||
161
src/lib/stores/library.store.ts
Normal file
161
src/lib/stores/library.store.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { reiverrApi, type TitleDto, type TitleType } from '../apis/reiverr/reiverr-api';
|
||||
import type { MyListDto } from '../apis/reiverr/reiverr-api';
|
||||
import {
|
||||
tmdbApi,
|
||||
type TmdbMovie2,
|
||||
type TmdbMovieFull2,
|
||||
type TmdbSeries2,
|
||||
type TmdbSeriesFull2
|
||||
} from '../apis/tmdb/tmdb-api';
|
||||
|
||||
// type TmdbList = {
|
||||
// movies: {
|
||||
// [key: string]: TmdbMovie2;
|
||||
// };
|
||||
// series: {
|
||||
// [key: string]: TmdbSeries2;
|
||||
// };
|
||||
// };
|
||||
type MyList = {
|
||||
movies: {
|
||||
[key: string]: TitleDto;
|
||||
};
|
||||
series: {
|
||||
[key: string]: TitleDto;
|
||||
};
|
||||
};
|
||||
|
||||
function useLibrary() {
|
||||
const store = writable<MyList | undefined>(undefined);
|
||||
|
||||
// reiverrApi.getLibrary().then((myList) => {
|
||||
// store.set(listToObject(myList));
|
||||
// });
|
||||
setTimeout(refreshLibrary, 2000); //TODO: Fix this
|
||||
// refreshLibrary().then();
|
||||
|
||||
async function refreshLibrary() {
|
||||
// return tmdbApi.getLibrary().then((myList) => {
|
||||
// const obj: TmdbList = {
|
||||
// movies: {},
|
||||
// series: {}
|
||||
// };
|
||||
//
|
||||
// myList.forEach((item) => {
|
||||
// if (item?.type === 'movie') {
|
||||
// obj.movies[String(item.id) || ''] = item;
|
||||
// } else {
|
||||
// obj.series[String(item.id) || ''] = item;
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// store.set(obj);
|
||||
// });
|
||||
|
||||
return reiverrApi.getLibrary().then(async (myList) => {
|
||||
const obj: MyList = {
|
||||
movies: {},
|
||||
series: {}
|
||||
};
|
||||
|
||||
// const movies: Promise<TmdbMovieFull2 | undefined>[] = [];
|
||||
// const series: Promise<TmdbSeriesFull2 | undefined>[] = [];
|
||||
|
||||
myList.forEach((item) => {
|
||||
if (item?.type === 'movie') {
|
||||
// movies.push(tmdbApi.getMovie(item.tmdbId));
|
||||
// obj.movies[String(item.id) || ''] = item;
|
||||
obj.movies[String(item.id) || ''] = item;
|
||||
} else {
|
||||
// series.push(tmdbApi.getSeries(item.tmdbId));
|
||||
// obj.series[String(item.id) || ''] = item;
|
||||
obj.series[String(item.id) || ''] = item;
|
||||
}
|
||||
});
|
||||
|
||||
// await Promise.all(movies).then((movies) => {
|
||||
// movies.forEach((m) => {
|
||||
// if (m) obj.movies[String(m.id) || ''] = m;
|
||||
// });
|
||||
// });
|
||||
// await Promise.all(series).then((series) => {
|
||||
// series.forEach((s) => {
|
||||
// if (s) obj.series[String(s.id) || ''] = s;
|
||||
// });
|
||||
// });
|
||||
|
||||
store.set(obj);
|
||||
});
|
||||
}
|
||||
|
||||
// function listToObject(list: TitleDto[]) {
|
||||
// const obj: MyList = {};
|
||||
// list.forEach((item) => {
|
||||
// obj[item.tmdbId] = true;
|
||||
// });
|
||||
// return obj;
|
||||
// }
|
||||
|
||||
async function add(tmdbId: number, type: TitleType) {
|
||||
// const res = await tmdbApi.addToLibrary(tmdbId, type);
|
||||
//
|
||||
// if (res?.success) {
|
||||
// return refreshLibrary();
|
||||
// }
|
||||
|
||||
const res = await reiverrApi.addToLibrary(tmdbId, type);
|
||||
|
||||
if (res !== undefined) {
|
||||
if (type === 'movie') {
|
||||
// const m = await tmdbApi.getMovie(tmdbId);
|
||||
|
||||
store.update((obj) => {
|
||||
// if (obj && m) obj.movies[tmdbId] = m;
|
||||
if (obj) obj.movies[tmdbId] = res;
|
||||
return obj;
|
||||
});
|
||||
} else {
|
||||
// const s = await tmdbApi.getSeries(tmdbId);
|
||||
|
||||
store.update((obj) => {
|
||||
// if (obj && s) obj.series[tmdbId] = s;
|
||||
if (obj) obj.series[tmdbId] = res;
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(tmdbId: number, type: TitleType) {
|
||||
// const res = await tmdbApi.removeFromLibrary(tmdbId, type);
|
||||
//
|
||||
// if (res?.success) {
|
||||
// return refreshLibrary();
|
||||
// }
|
||||
|
||||
const res = await reiverrApi.removeFromLibrary(tmdbId, type);
|
||||
|
||||
if (res !== undefined) {
|
||||
if (type === 'movie') {
|
||||
store.update((obj) => {
|
||||
if (obj) delete obj.movies[tmdbId];
|
||||
return obj;
|
||||
});
|
||||
} else {
|
||||
store.update((obj) => {
|
||||
if (obj) delete obj.series[tmdbId];
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
add,
|
||||
remove
|
||||
};
|
||||
}
|
||||
|
||||
export const myList = useLibrary();
|
||||
Reference in New Issue
Block a user