From 9c1ec7f6ea436eec12235e8258752ee807b00a2b Mon Sep 17 00:00:00 2001 From: Aleksi Lassila Date: Sat, 8 Feb 2025 05:40:27 +0200 Subject: [PATCH] feat: show recently played from library on front pages --- backend/src/metadata/metadata.entity.ts | 2 + .../src/users/library/library.controller.ts | 5 +- backend/src/users/library/library.dto.ts | 6 +- backend/src/users/library/library.service.ts | 28 +++++++++- backend/src/users/users.module.ts | 7 ++- src/lib/apis/reiverr/reiverr.openapi.ts | 10 +++- src/lib/pages/LibraryPage.svelte | 2 +- src/lib/pages/MoviesHomePage.svelte | 55 ++++++++++++++----- src/lib/pages/SeriesHomePage.svelte | 39 +++++++++++-- src/lib/stores/data.store.ts | 25 +++++---- 10 files changed, 144 insertions(+), 35 deletions(-) diff --git a/backend/src/metadata/metadata.entity.ts b/backend/src/metadata/metadata.entity.ts index 26b2a46..a2a3303 100644 --- a/backend/src/metadata/metadata.entity.ts +++ b/backend/src/metadata/metadata.entity.ts @@ -17,6 +17,7 @@ export class Movie { @Column({ unique: true }) tmdbId: string; + @ApiProperty({ required: false, type: 'object' }) @Column('json') tmdbMovie: TmdbMovieFull; @@ -34,6 +35,7 @@ export class Series { @Column({ unique: true }) tmdbId: string; + @ApiProperty({ required: false, type: 'object' }) @Column('json') tmdbSeries: TmdbSeriesFull; diff --git a/backend/src/users/library/library.controller.ts b/backend/src/users/library/library.controller.ts index 5ff714b..4aed53a 100644 --- a/backend/src/users/library/library.controller.ts +++ b/backend/src/users/library/library.controller.ts @@ -43,7 +43,10 @@ export class LibraryController { ): Promise> { // const user = await this.userService.findOne(userId); - const items = await this.libraryService.getLibraryItems(userId, pagination); + const items = await this.libraryService.getLibraryItemsWithMetadata( + userId, + pagination, + ); return { items, diff --git a/backend/src/users/library/library.dto.ts b/backend/src/users/library/library.dto.ts index e9c6d8d..e8e5cae 100644 --- a/backend/src/users/library/library.dto.ts +++ b/backend/src/users/library/library.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { MovieDto } from 'src/metadata/metadata.dto'; import { PlayStateDto } from '../play-state/play-state.dto'; import { MediaType } from 'src/common/common.dto'; +import { Series } from 'src/metadata/metadata.entity'; export class LibraryItemDto { @ApiProperty() @@ -14,5 +15,8 @@ export class LibraryItemDto { playStates?: PlayStateDto[]; @ApiProperty({ type: MovieDto, required: false }) - metadata?: MovieDto; // TODO + movieMetadata?: MovieDto; + + @ApiProperty({ type: Series, required: false }) + seriesMetadata?: Series; } diff --git a/backend/src/users/library/library.service.ts b/backend/src/users/library/library.service.ts index 579f7d5..a7911d2 100644 --- a/backend/src/users/library/library.service.ts +++ b/backend/src/users/library/library.service.ts @@ -3,15 +3,41 @@ import { USER_LIBRARY_REPOSITORY } from '../user.providers'; import { Repository } from 'typeorm'; import { LibraryItem } from './library.entity'; import { MediaType, PaginationParamsDto } from 'src/common/common.dto'; +import { LibraryItemDto } from './library.dto'; +import { MetadataService } from 'src/metadata/metadata.service'; @Injectable() export class LibraryService { constructor( @Inject(USER_LIBRARY_REPOSITORY) private readonly libraryRepository: Repository, + private readonly metadataService: MetadataService, ) {} - async getLibraryItems( + async getLibraryItemsWithMetadata( + userId: string, + pagination: PaginationParamsDto, + ): Promise { + const items = await this.getLibraryItems(userId, pagination); + + return Promise.all( + items.map(async (item) => { + return { + ...item, + movieMetadata: + item.mediaType === MediaType.Movie + ? await this.metadataService.getMovieByTmdbId(item.tmdbId) + : undefined, + seriesMetadata: + item.mediaType === MediaType.Series + ? await this.metadataService.getSeriesByTmdbId(item.tmdbId) + : undefined, + }; + }), + ); + } + + private async getLibraryItems( userId: string, pagination: PaginationParamsDto, ): Promise { diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index b106935..a260a73 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -10,9 +10,14 @@ import { PlayStateService } from './play-state/play-state.service'; import { LibraryController } from './library/library.controller'; import { PlayStateController } from './play-state/play-state.controller'; import { SourcePluginsModule } from 'src/source-plugins/source-plugins.module'; +import { MetadataModule } from 'src/metadata/metadata.module'; @Module({ - imports: [DatabaseModule, forwardRef(() => SourcePluginsModule)], + imports: [ + DatabaseModule, + forwardRef(() => SourcePluginsModule), + forwardRef(() => MetadataModule), + ], providers: [ ...userProviders, UsersService, diff --git a/src/lib/apis/reiverr/reiverr.openapi.ts b/src/lib/apis/reiverr/reiverr.openapi.ts index 74fe651..2509f5b 100644 --- a/src/lib/apis/reiverr/reiverr.openapi.ts +++ b/src/lib/apis/reiverr/reiverr.openapi.ts @@ -166,13 +166,21 @@ export interface PaginatedResponseDto { export interface MovieDto { id?: string; tmdbId: string; + tmdbMovie?: object; +} + +export interface Series { + id?: string; + tmdbId: string; + tmdbSeries?: object; } export interface LibraryItemDto { tmdbId: string; mediaType: 'Movie' | 'Series' | 'Episode'; playStates?: PlayStateDto[]; - metadata?: MovieDto; + movieMetadata?: MovieDto; + seriesMetadata?: Series; } export interface SuccessResponseDto { diff --git a/src/lib/pages/LibraryPage.svelte b/src/lib/pages/LibraryPage.svelte index 3b1cec3..7e3cfc9 100644 --- a/src/lib/pages/LibraryPage.svelte +++ b/src/lib/pages/LibraryPage.svelte @@ -140,7 +140,7 @@ {:then items} --> {#each items as item} - import Container from '$components/Container.svelte'; - import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte'; - import { TMDB_MOVIE_GENRES, TmdbApi, tmdbApi } from '../apis/tmdb/tmdb-api'; - import { getShowcasePropsFromTmdbMovie } from '../components/HeroShowcase/HeroShowcase'; - import Carousel from '../components/Carousel/Carousel.svelte'; - import { scrollIntoView } from '../selectable'; + import { libraryItemsDataStore } from '$lib/stores/data.store'; + import { derived } from 'svelte/store'; import { jellyfinApi } from '../apis/jellyfin/jellyfin-api'; - import JellyfinCard from '../components/Card/JellyfinCard.svelte'; - import { formatDateToYearMonthDay } from '../utils'; + import { TMDB_MOVIE_GENRES, TmdbApi, tmdbApi } from '../apis/tmdb/tmdb-api'; import TmdbCard from '../components/Card/TmdbCard.svelte'; - import { navigate } from '../components/StackRouter/StackRouter'; + import Carousel from '../components/Carousel/Carousel.svelte'; import DetachedPage from '../components/DetachedPage/DetachedPage.svelte'; + import { getShowcasePropsFromTmdbMovie } from '../components/HeroShowcase/HeroShowcase'; + import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte'; + import { navigate } from '../components/StackRouter/StackRouter'; + import { scrollIntoView } from '../selectable'; + import { formatDateToYearMonthDay } from '../utils'; - const continueWatching = jellyfinApi.getContinueWatching('movie'); - const recentlyAdded = jellyfinApi.getRecentlyAdded('movie'); + const { ...libraryData } = libraryItemsDataStore.getRequest(); + const libraryContinueWatching = derived(libraryData, (libraryData) => { + if (!libraryData) return []; + + const movies = libraryData.filter((i) => i.mediaType === 'Movie' && i.playStates?.length); + + movies.sort((a, b) => { + const aMax = Math.max( + ...(a.playStates?.map((p) => new Date(p.lastPlayedAt).getTime()) || [0]) + ); + const bMax = Math.max( + ...(b.playStates?.map((p) => new Date(p.lastPlayedAt).getTime()) || [0]) + ); + + return bMax - aMax; + }); + + return movies.map((i) => i.metadata); + }); + + // const continueWatching = jellyfinApi.getContinueWatching('movie'); + // const recentlyAdded = jellyfinApi.getRecentlyAdded('movie'); const popularMovies = tmdbApi.getPopularMovies(); @@ -69,7 +89,16 @@ />
- {#await continueWatching then continueWatching} + {#if $libraryContinueWatching.length} + + Continue Watching + {#each $libraryContinueWatching as item} + + {/each} + + {/if} + + {#await popularMovies then popularMovies} diff --git a/src/lib/pages/SeriesHomePage.svelte b/src/lib/pages/SeriesHomePage.svelte index 60d09a8..3e73068 100644 --- a/src/lib/pages/SeriesHomePage.svelte +++ b/src/lib/pages/SeriesHomePage.svelte @@ -12,9 +12,31 @@ import { navigate } from '../components/StackRouter/StackRouter'; import { TMDB_SERIES_GENRES } from '../apis/tmdb/tmdb-api.js'; import DetachedPage from '../components/DetachedPage/DetachedPage.svelte'; + import { libraryItemsDataStore } from '$lib/stores/data.store'; + import { derived } from 'svelte/store'; - const continueWatching = jellyfinApi.getContinueWatchingSeries(); - const recentlyAdded = jellyfinApi.getRecentlyAdded('series'); + const { ...libraryData } = libraryItemsDataStore.getRequest(); + const libraryContinueWatching = derived(libraryData, (libraryData) => { + if (!libraryData) return []; + + const series = libraryData.filter((i) => i.mediaType === 'Series' && i.playStates?.length); + + series.sort((a, b) => { + const aMax = Math.max( + ...(a.playStates?.map((p) => new Date(p.lastPlayedAt).getTime()) || [0]) + ); + const bMax = Math.max( + ...(b.playStates?.map((p) => new Date(p.lastPlayedAt).getTime()) || [0]) + ); + + return bMax - aMax; + }); + + return series.map((i) => i.metadata); + }); + + // const continueWatching = jellyfinApi.getContinueWatchingSeries(); + // const recentlyAdded = jellyfinApi.getRecentlyAdded('series'); const nowStreaming = getNowStreaming(); const upcomingSeries = fetchUpcomingSeries(); @@ -62,7 +84,16 @@ />
- {#await continueWatching then continueWatching} + {#if $libraryContinueWatching.length} + + Continue Watching + {#each $libraryContinueWatching as item} + + {/each} + + {/if} + + {#await popular then popular} diff --git a/src/lib/stores/data.store.ts b/src/lib/stores/data.store.ts index 9ce3686..c0fee31 100644 --- a/src/lib/stores/data.store.ts +++ b/src/lib/stores/data.store.ts @@ -1,6 +1,11 @@ import { derived, get, type Readable, writable } from 'svelte/store'; import { jellyfinApi } from '../apis/jellyfin/jellyfin-api'; -import { tmdbApi, type TmdbMovieFull2, type TmdbSeries2 } from '../apis/tmdb/tmdb-api'; +import { + tmdbApi, + type TmdbMovieFull2, + type TmdbSeries2, + type TmdbSeriesFull2 +} from '../apis/tmdb/tmdb-api'; import { awaitAppInitialization, reiverrApiNew, user } from './user.store'; type AwaitableStoreValue = { @@ -236,16 +241,12 @@ export const libraryItemsDataStore = useRequestsStore(() => reiverrApiNew.users .getLibraryItems(get(user)?.id as string) .then((r) => - Promise.all( - r.data.items.map((i) => - i.mediaType === 'Movie' - ? tmdbApi - .getTmdbMovie(Number(i.tmdbId)) - .then((movie) => ({ tmdbMovie: movie!, playStates: i.playStates })) - : tmdbApi - .getTmdbSeries(Number(i.tmdbId)) - .then((series) => ({ tmdbSeries: series!, playStates: i.playStates })) - ) - ).then((i) => i.filter((i) => ('tmdbMovie' in i ? !!i.tmdbMovie : !!i.tmdbSeries))) + r.data.items.map((i) => ({ + ...i, + metadata: + (i.movieMetadata?.tmdbMovie as TmdbMovieFull2) || + (i.seriesMetadata?.tmdbSeries as TmdbSeriesFull2) + })) ) + .then((i) => i.filter((i) => !!i.metadata)) );