diff --git a/backend/package-lock.json b/backend/package-lock.json index 8b608c3..b04a661 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -42,6 +42,7 @@ "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "cross-env": "^7.0.3", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", @@ -3866,6 +3867,25 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "license": "MIT", diff --git a/backend/package.json b/backend/package.json index 0406bcb..b820dbf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,7 +9,7 @@ "build": "npm run build -w packages/reiverr-plugin && nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", - "start:dev": "export NODE_ENV=development || set NODE_ENV=development&& nest start --watch", + "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "npm run typeorm:run-migrations && node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", @@ -58,6 +58,7 @@ "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "cross-env": "^7.0.3", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", @@ -99,4 +100,4 @@ "packages/jellyfin.plugin", "packages/torrent-stream.plugin" ] -} +} \ No newline at end of file diff --git a/backend/src/metadata/metadata.entity.ts b/backend/src/metadata/metadata.entity.ts index 4512090..77ee386 100644 --- a/backend/src/metadata/metadata.entity.ts +++ b/backend/src/metadata/metadata.entity.ts @@ -46,21 +46,11 @@ export class Series { updatedAt: Date; isStale() { - console.log(this.updatedAt); if (!this.updatedAt) return true; - console.log( - new Date().getTime() - this.updatedAt.getTime(), - TMDB_CACHE_TTL, - ); if (new Date().getTime() - this.updatedAt.getTime() > TMDB_CACHE_TTL) return true; - console.log( - 'Checking if series is stale', - this.tmdbSeries.name, - this.tmdbSeries.next_episode_to_air?.air_date, - ); if ( this.tmdbSeries?.next_episode_to_air?.air_date && new Date() > new Date(this.tmdbSeries.next_episode_to_air.air_date) diff --git a/backend/src/user-data/play-state/play-state.dto.ts b/backend/src/user-data/play-state/play-state.dto.ts index bfa0e10..ec1104e 100644 --- a/backend/src/user-data/play-state/play-state.dto.ts +++ b/backend/src/user-data/play-state/play-state.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger'; +import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger'; import { PlayState } from './play-state.entity'; export class PlayStateDto extends PlayState {} @@ -22,9 +22,14 @@ export class PlayStateDto extends PlayState {} // } export class UpdatePlayStateDto extends PartialType( - OmitType(PlayStateDto, ['userId']), + PickType(PlayStateDto, ['season', 'episode', 'watched', 'progress']), ) {} +export class BulkUpdatePlayStateDto { + @ApiProperty({ type: UpdatePlayStateDto, isArray: true }) + playStates: UpdatePlayStateDto[]; +} + export class MovieUserDataDto { @ApiProperty() tmdbId: string; diff --git a/backend/src/user-data/play-state/play-states.controller.ts b/backend/src/user-data/play-state/play-states.controller.ts index de9dfd2..2fe8e24 100644 --- a/backend/src/user-data/play-state/play-states.controller.ts +++ b/backend/src/user-data/play-state/play-states.controller.ts @@ -9,7 +9,7 @@ import { } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { UserAccessControl } from 'src/auth/auth.guard'; -import { UpdatePlayStateDto } from './play-state.dto'; +import { BulkUpdatePlayStateDto, UpdatePlayStateDto } from './play-state.dto'; import { PlayStatesService } from './play-states.service'; @ApiTags('users') @@ -42,7 +42,7 @@ export class PlayStatesController { return this.playStateService.deleteMoviePlayState(userId, tmdbId); } - @Put('show/tmdb/:tmdbId/season/:season/episode/:episode') + @Put('series/tmdb/:tmdbId/season/:season/episode/:episode') async updateEpisodePlayStateByTmdbId( @Param('userId') userId: string, @Param('tmdbId') tmdbId: string, @@ -59,7 +59,7 @@ export class PlayStatesController { ); } - @Delete('show/tmdb/:tmdbId/season/:season/episode/:episode') + @Delete('series/tmdb/:tmdbId/season/:season/episode/:episode') async deleteEpisodePlayStateByTmdbId( @Param('userId') userId: string, @Param('tmdbId') tmdbId: string, @@ -73,4 +73,17 @@ export class PlayStatesController { episode, ); } + + @Put('series/tmdb/:tmdbId') + async updateSeriesPlayStatesByTmdbId( + @Param('userId') userId: string, + @Param('tmdbId') tmdbId: string, + @Body() body: BulkUpdatePlayStateDto, + ) { + return this.playStateService.updateOrCreateSeriesPlayStates( + userId, + tmdbId, + body.playStates, + ); + } } diff --git a/backend/src/user-data/play-state/play-states.service.ts b/backend/src/user-data/play-state/play-states.service.ts index 940cf5c..3ba056c 100644 --- a/backend/src/user-data/play-state/play-states.service.ts +++ b/backend/src/user-data/play-state/play-states.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { MediaType } from 'src/common/common.dto'; import { Repository } from 'typeorm'; -import { UpdatePlayStateDto } from './play-state.dto'; +import { BulkUpdatePlayStateDto, UpdatePlayStateDto } from './play-state.dto'; import { PlayState } from './play-state.entity'; import { USER_PLAY_STATE_REPOSITORY } from './play-state.providers'; @@ -45,20 +45,64 @@ export class PlayStatesService { return playStates; } - async updateOrCreateMoviePlayState( + async getPlayState( userId: string, tmdbId: string, - playState: UpdatePlayStateDto, + season?: number, + episode?: number, + ): Promise { + return this.playStateRepository.findOne({ + where: { + userId, + tmdbId, + ...(season ? { season } : {}), + ...(episode ? { episode } : {}), + }, + }); + } + + private async getOrCreatePlayState( + userId: string, + tmdbId: string, + options: { + season?: number; + episode?: number; + mediaType: MediaType; + save?: boolean; + }, ) { - let state = await this.findMoviePlayState(userId, tmdbId); + let state = await this.getPlayState( + userId, + tmdbId, + options.season, + options.episode, + ); if (!state) { state = this.playStateRepository.create(); state.userId = userId; state.tmdbId = tmdbId; - state.mediaType = MediaType.Movie; + state.season = options.season; + state.episode = options.episode; + state.mediaType = options.mediaType; } + if (options.save) { + return this.playStateRepository.save(state); + } + + return state; + } + + async updateOrCreateMoviePlayState( + userId: string, + tmdbId: string, + playState: UpdatePlayStateDto, + ) { + const state = await this.getOrCreatePlayState(userId, tmdbId, { + mediaType: MediaType.Movie, + }); + state.progress = playState.progress; state.watched = playState.watched; @@ -72,28 +116,14 @@ export class PlayStatesService { episode: number, playState: UpdatePlayStateDto, ) { - let state = await this.findSeriesPlayStates( - userId, - tmdbId, + const state = await this.getOrCreatePlayState(userId, tmdbId, { season, episode, - ).then((states) => - states.find( - (state) => state.season === season && state.episode === episode, - ), - ); + mediaType: MediaType.Episode, + }); - if (!state) { - state = this.playStateRepository.create(); - state.userId = userId; - state.tmdbId = tmdbId; - state.season = season; - state.episode = episode; - state.mediaType = MediaType.Episode; - } - - state.progress = playState.progress; - state.watched = playState.watched; + state.progress = playState.progress ?? state.progress; + state.watched = playState.watched ?? state.watched; return this.playStateRepository.save(state); } @@ -109,12 +139,33 @@ export class PlayStatesService { season: number, episode: number, ) { - const state = await this.findSeriesPlayStates( - userId, - tmdbId, - season, - episode, + const state = await this.getPlayState(userId, tmdbId, season, episode); + + if (state) { + return await this.playStateRepository.remove(state); + } + } + + async updateOrCreateSeriesPlayStates( + userId: string, + tmdbId: string, + playStates: UpdatePlayStateDto[], + ) { + const states = await Promise.all( + playStates.map(async (updatedState) => { + const state = await this.getOrCreatePlayState(userId, tmdbId, { + season: updatedState.season, + episode: updatedState.episode, + mediaType: MediaType.Episode, + }); + + state.progress = updatedState.progress ?? state.progress; + state.watched = updatedState.watched ?? state.watched; + + return this.playStateRepository.save(state); + }), ); - return await this.playStateRepository.remove(state); + + return states; } } diff --git a/backend/src/user-data/user-data.controller.ts b/backend/src/user-data/user-data.controller.ts index 71dbf6d..b8f2c2d 100644 --- a/backend/src/user-data/user-data.controller.ts +++ b/backend/src/user-data/user-data.controller.ts @@ -81,11 +81,12 @@ export class UserDataController { @Param('episode', ParseIntPipe) episode: number, ): Promise { const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId); - const playState = await this.playStateService - .findSeriesPlayStates(userId, tmdbId, season, episode) - .then((states) => - states.find((s) => s.season === season && s.episode === episode), - ); + const playState = await this.playStateService.getPlayState( + userId, + tmdbId, + season, + episode, + ); return { tmdbId, diff --git a/package-lock.json b/package-lock.json index 3efafbc..c238ffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "reiverr", - "version": "2.0.0-alpha.6", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reiverr", - "version": "2.0.0-alpha.6", + "version": "2.1.1", "dependencies": { "gsap": "^3.12.5", "swagger-typescript-api": "^13.0.23" diff --git a/src/lib/apis/reiverr/reiverr.openapi.ts b/src/lib/apis/reiverr/reiverr.openapi.ts index a6190f5..b6c1af4 100644 --- a/src/lib/apis/reiverr/reiverr.openapi.ts +++ b/src/lib/apis/reiverr/reiverr.openapi.ts @@ -477,9 +477,6 @@ export interface SeriesUserDataDto { } export interface UpdatePlayStateDto { - id?: string; - tmdbId?: number; - mediaType?: 'Movie' | 'Series' | 'Episode'; season?: number; episode?: number; /** @@ -493,8 +490,10 @@ export interface UpdatePlayStateDto { * @example 0.5 */ progress?: number; - /** Last time the user played this media */ - lastPlayedAt?: string; +} + +export interface BulkUpdatePlayStateDto { + playStates: UpdatePlayStateDto[]; } export interface MovieDto { @@ -937,7 +936,7 @@ export class Api extends HttpClient extends HttpClient this.request({ - path: `/api/users/${userId}/play-state/show/tmdb/${tmdbId}/season/${season}/episode/${episode}`, + path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, method: 'PUT', body: data, type: ContentType.Json, @@ -960,7 +959,7 @@ export class Api extends HttpClient extends HttpClient this.request({ - path: `/api/users/${userId}/play-state/show/tmdb/${tmdbId}/season/${season}/episode/${episode}`, + path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, method: 'DELETE', ...params }), + /** + * No description + * + * @tags users + * @name UpdateSeriesPlayStatesByTmdbId + * @request PUT:/api/users/{userId}/play-state/series/tmdb/{tmdbId} + */ + updateSeriesPlayStatesByTmdbId: ( + userId: string, + tmdbId: string, + data: BulkUpdatePlayStateDto, + params: RequestParams = {} + ) => + this.request({ + path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}`, + method: 'PUT', + body: data, + type: ContentType.Json, + ...params + }), + /** * No description * diff --git a/src/lib/components/Carousel/Carousel.svelte b/src/lib/components/Carousel/Carousel.svelte index 8eb2232..9154b61 100644 --- a/src/lib/components/Carousel/Carousel.svelte +++ b/src/lib/components/Carousel/Carousel.svelte @@ -12,6 +12,7 @@ let carousel: HTMLDivElement | undefined; let scrollX = 0; export let scrollClass = ''; + export let header = ''; function handleOnBack({ detail }: BackEvent) { const focusIndex = get(detail.selectable.focusIndex); @@ -26,9 +27,13 @@
-
- -
+ {#if header} +
{header}
+ {:else} +
+ +
+ {/if}
{#if sidebar} {/if} - + diff --git a/src/lib/components/SelectButtonGroup.svelte b/src/lib/components/SelectButtonGroup.svelte index 49ae456..ba7c8de 100644 --- a/src/lib/components/SelectButtonGroup.svelte +++ b/src/lib/components/SelectButtonGroup.svelte @@ -39,18 +39,18 @@ {@const first = index === 0} {@const last = index === options.length - 1} {#if !first} -
+
{/if} dispatch('select', option.value ?? option.label)} let:hasFocus + class="group" >
bReleaseDate ? 1 : -1; }); @@ -83,6 +81,8 @@ } ); + let didMount = false; + function sortItems( items: LibraryItemWithMetadata[] | undefined, viewSettings: LibraryViewSettings @@ -142,7 +142,7 @@ }); - + {#if !$isLoading}
@@ -153,12 +153,12 @@
{#if $libraryItemsCategorized.main.length + $libraryItemsCategorized.upcoming.length + $libraryItemsCategorized.watched.length} {#if $libraryItemsCategorized.upcoming.length} -
-
-
Upcoming
-
-
- +
+ {#key viewSettingsKey} {#each $libraryItemsCategorized.upcoming as item} {/if} {#if $libraryItemsCategorized.main.length} -
-
My Library
- +
+
My Library
+ (didMount = true)} + > {#key viewSettingsKey} {#each $libraryItemsCategorized.main as item, index (item.tmdbId)} {/if} {#if $libraryItemsCategorized.watched.length} -
-
Watched
- +
+
Watched
+ {#key viewSettingsKey} {#each $libraryItemsCategorized.watched as item (item.tmdbId)} - - {#if !$inLibrary} {/if} + + + {#if PLATFORM_WEB} + {#if !$inLibrary} {/if} + + + {#if PLATFORM_WEB}