diff --git a/backend/src/user-data/library/library.dto.ts b/backend/src/user-data/library/library.dto.ts index e2afc7e..a40e69f 100644 --- a/backend/src/user-data/library/library.dto.ts +++ b/backend/src/user-data/library/library.dto.ts @@ -12,6 +12,7 @@ export enum MyListOrder { Name = 'name', FirstReleaseDate = 'first-release-date', LastReleaseDate = 'last-release-date', + LastPlayed = 'last-played', } export enum MyListStatusFilter { @@ -19,7 +20,7 @@ export enum MyListStatusFilter { Upcoming = 'upcoming', Unwatched = 'unwatched', Watched = 'watched', - ContinueWatching = 'continueWatching', + ContinueWatching = 'continue-watching', } export enum MyListTypeFilter { diff --git a/backend/src/user-data/library/library.entity.ts b/backend/src/user-data/library/library.entity.ts index 62d9acf..acd83b4 100644 --- a/backend/src/user-data/library/library.entity.ts +++ b/backend/src/user-data/library/library.entity.ts @@ -25,13 +25,17 @@ export class LibraryItem { id: string; @ApiProperty({ required: true }) - @Column({ unique: true }) + @Column() tmdbId: string; @ApiProperty({ required: true, enum: MediaType }) @Column() mediaType: MediaType; + @ApiProperty({ type: 'string', required: false }) + @Column({ nullable: true }) + lastPlayedAt?: Date; + @ApiProperty({ required: false, type: MovieMetadata }) @ManyToOne(() => MovieMetadata, { createForeignKeyConstraints: false, diff --git a/backend/src/user-data/library/library.service.ts b/backend/src/user-data/library/library.service.ts index cae7b87..dc00302 100644 --- a/backend/src/user-data/library/library.service.ts +++ b/backend/src/user-data/library/library.service.ts @@ -57,66 +57,12 @@ export class LibraryService { direction = OrderDirection.Desc, } = options; - // const order = { - // [MyListOrder.DateAdded]: { createdAt: directon } as const, - // [MyListOrder.Name]: { - // seriesMetadata: { - // name: directon, - // }, - // movieMetadata: { - // name: directon, - // }, - // } as const, - // [MyListOrder.FirstReleaseDate]: { - // movieMetadata: { - // releaseDate: directon, - // }, - // seriesMetadata: { - // firstReleaseDate: directon, - // }, - // } as const, - // [MyListOrder.LastReleaseDate]: { - // movieMetadata: { - // releaseDate: directon, - // }, - // seriesMetadata: { - // lastReleaseDate: directon, - // }, - // } as const, - // }[sortBy]; - const mediaType = type ? type === MyListTypeFilter.Movies ? MediaType.Movie : MediaType.Series : undefined; - // const [items, total] = await this.libraryRepository.findAndCount({ - // relations: { - // playStates: true, - // seriesMetadata: true, - // movieMetadata: true, - // }, - // select: { - // seriesMetadata: { - // firstReleaseDate: true, - // lastReleaseDate: true, - // name: true, - // }, - // movieMetadata: { - // releaseDate: true, - // name: true, - // }, - // }, - // where: { - // userId, - // ...(mediaType ? { mediaType } : {}), - // }, - // order, - // take: pagination.itemsPerPage, - // skip: pagination.itemsPerPage * (pagination.page - 1), - // }); - let builder = this.libraryRepository .createQueryBuilder('libraryItem') .leftJoinAndSelect('libraryItem.playStates', 'playStates') @@ -176,7 +122,10 @@ export class LibraryService { builder = builder.andWhere(upcoming); } else if (status === MyListStatusFilter.Watched) { builder = builder.andWhere(watchedAndNotUpcoming); - } else if (status === MyListStatusFilter.Unwatched) { + } else if ( + status === MyListStatusFilter.Unwatched || + status === MyListStatusFilter.ContinueWatching + ) { builder = builder.andWhere( new Brackets((qb) => qb @@ -210,6 +159,12 @@ export class LibraryService { ), ), ); + + if (status === MyListStatusFilter.ContinueWatching) { + builder = builder.andWhere( + "libraryItem.lastPlayedAt IS NOT NULL AND libraryItem.lastPlayedAt > date('now', '-1 month')", + ); + } } const DIRECTION = direction === OrderDirection.Asc ? 'ASC' : 'DESC'; @@ -224,6 +179,8 @@ export class LibraryService { } else if (order === MyListOrder.LastReleaseDate) { builder.addOrderBy('movieMetadata.releaseDate', DIRECTION); builder.addOrderBy('seriesMetadata.lastReleaseDate', DIRECTION); + } else if (order === MyListOrder.LastPlayed) { + builder.addOrderBy('libraryItem.lastPlayedAt', DIRECTION); } const [items, total] = await builder diff --git a/backend/src/user-data/play-state/play-states.module.ts b/backend/src/user-data/play-state/play-states.module.ts index 7a984ba..6a20632 100644 --- a/backend/src/user-data/play-state/play-states.module.ts +++ b/backend/src/user-data/play-state/play-states.module.ts @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common'; import { playStateProviders } from './play-state.providers'; import { PlayStatesController } from './play-states.controller'; import { PlayStatesService } from './play-states.service'; +import { libraryProviders } from '../library/library.providers'; @Module({ imports: [], - providers: [...playStateProviders, PlayStatesService], + providers: [...playStateProviders, ...libraryProviders, PlayStatesService], controllers: [PlayStatesController], exports: [PlayStatesService], }) 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 8e23fa8..b4a08e0 100644 --- a/backend/src/user-data/play-state/play-states.service.ts +++ b/backend/src/user-data/play-state/play-states.service.ts @@ -1,15 +1,19 @@ import { Inject, Injectable } from '@nestjs/common'; import { MediaTypeFull } from 'src/common/common.dto'; import { Repository } from 'typeorm'; -import { BulkUpdatePlayStateDto, UpdatePlayStateDto } from './play-state.dto'; +import { UpdatePlayStateDto } from './play-state.dto'; import { PlayState } from './play-state.entity'; import { USER_PLAY_STATE_REPOSITORY } from './play-state.providers'; +import { USER_LIBRARY_REPOSITORY } from '../library/library.providers'; +import { LibraryItem } from '../library/library.entity'; @Injectable() export class PlayStatesService { constructor( @Inject(USER_PLAY_STATE_REPOSITORY) private readonly playStateRepository: Repository, + @Inject(USER_LIBRARY_REPOSITORY) + private readonly libraryRepository: Repository, ) {} async findMoviePlayState(userId: string, tmdbId: string) { @@ -106,7 +110,14 @@ export class PlayStatesService { if (playState.progress !== undefined) state.progress = playState.progress; if (playState.watched !== undefined) state.watched = playState.watched; - return this.playStateRepository.save(state); + return this.playStateRepository.save(state).then(async (state) => { + await this.libraryRepository.update( + { tmdbId, userId }, + { lastPlayedAt: new Date() }, + ); + + return state; + }); } async updateOrCreateEpisodePlayState( @@ -125,7 +136,14 @@ export class PlayStatesService { if (playState.progress !== undefined) state.progress = playState.progress; if (playState.watched !== undefined) state.watched = playState.watched; - return this.playStateRepository.save(state); + return this.playStateRepository.save(state).then(async (state) => { + await this.libraryRepository.update( + { tmdbId, userId }, + { lastPlayedAt: new Date() }, + ); + + return state; + }); } async deleteMoviePlayState(userId: string, tmdbId: string) { diff --git a/src/lib/apis/reiverr/reiverr.openapi.ts b/src/lib/apis/reiverr/reiverr.openapi.ts index d56f51a..3fe148f 100644 --- a/src/lib/apis/reiverr/reiverr.openapi.ts +++ b/src/lib/apis/reiverr/reiverr.openapi.ts @@ -1617,9 +1617,9 @@ export class Api extends HttpClient import type { - MediaSource, + MediaSourceDto, StreamDto, SubtitlesDto as Subtitles } from '$lib/apis/reiverr/reiverr.openapi'; import { episodeUserDataStore, - libraryItemsDataStore, movieUserDataStore, seriesUserDataStore, tmdbMovieDataStore, @@ -28,7 +27,7 @@ export let tmdbId: string; export let season: number | undefined = undefined; export let episode: number | undefined = undefined; - export let source: MediaSource; + export let source: MediaSourceDto; export let key: string = ''; export let progress: number = 0; @@ -228,6 +227,7 @@ movieUserDataStore.refresh(tmdbId); } libraryItemsDataStore.refreshIn(1500); + continuewa }); }); diff --git a/src/lib/pages/MoviesHomePage.svelte b/src/lib/pages/MoviesHomePage.svelte index d605a1e..a0fb45b 100644 --- a/src/lib/pages/MoviesHomePage.svelte +++ b/src/lib/pages/MoviesHomePage.svelte @@ -1,53 +1,28 @@ @@ -87,12 +62,12 @@ />
- {#if $libraryContinueWatching.length} + {#if $continueWatching?.items.length} Continue Watching {#key libraryContinueWatchingKey} - {#each $libraryContinueWatching as item (item.id)} - + {#each $continueWatching?.items ?? [] as item (item.tmdbId)} + {/each} {/key} diff --git a/src/lib/pages/SeriesHomePage.svelte b/src/lib/pages/SeriesHomePage.svelte index 3f6e796..22bfad4 100644 --- a/src/lib/pages/SeriesHomePage.svelte +++ b/src/lib/pages/SeriesHomePage.svelte @@ -1,47 +1,26 @@ @@ -63,12 +42,12 @@ />
- {#if $libraryContinueWatching.length} + {#if $continueWatching?.items?.length} Continue Watching {#key libraryContinueWatchingKey} - {#each $libraryContinueWatching as item (item.id)} - + {#each $continueWatching?.items ?? [] as item (item.tmdbId)} + {/each} {/key} diff --git a/src/lib/stores/data.store.ts b/src/lib/stores/data.store.ts index 749cb05..a29c989 100644 --- a/src/lib/stores/data.store.ts +++ b/src/lib/stores/data.store.ts @@ -195,7 +195,6 @@ export function usePaginatedRequest( if (options.loadFirstPage !== false) requestNextPage(); async function requestNextPage() { - console.log('herer'); if (get(loadingPage) === get(nextPage)) return; if (!hasNextPage) return; @@ -204,8 +203,6 @@ export function usePaginatedRequest( const currentPage = get(nextPage); const id = requestId; - console.log('requesting page', currentPage, id); - if (promise) await promise; if (!hasNextPage) return; @@ -217,7 +214,6 @@ export function usePaginatedRequest( if (res.items.length < res.itemsPerPage) { hasNextPage = false; - console.log('no more pages', res); } data.update((d) => [...d, ...res.items]); @@ -296,37 +292,33 @@ export const episodeUserDataStore = useRequestsStore( .then((r) => r.data) ); -export const libraryItemsDataStore = useRequestsStore( - () => reiverrApi.library.getMyList(get(user)?.id as string).then((r) => r.data.items), - { persistant: true } +export const continueWatchingMoviesDataStore = useRequestsStore(() => + reiverrApi.library + .getMyList(String(get(user)?.id), { + type: 'movies', + order: 'last-played', + status: 'continue-watching' + }) + .then((r) => r.data) ); -// const continueWatchingDataStore = useDerivedRequestsStore( -// libraryItemsDataStore, -// async (libraryData) => { -// if (!libraryData) return []; - -// const movies = libraryData.filter( -// (i) => i.mediaType === 'Movie' && i.playStates?.length && !i.watched -// ); - -// 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); -// } -// ); +export const continueWatchingSeriesDataStore = useRequestsStore(() => + reiverrApi.library + .getMyList(String(get(user)?.id), { + type: 'series', + order: 'last-played', + status: 'continue-watching' + }) + .then((r) => r.data) +); export const mediaSourcesDataStore = useRequestsStore(() => reiverrApi.users .findUserById(get(user)?.id || '') .then((r) => r.data.mediaSources?.sort((a, b) => a.priority - b.priority) ?? []) ); + +export function refreshLibraryDerivatives(timeout = 0) { + continueWatchingMoviesDataStore.refreshIn(timeout); + continueWatchingSeriesDataStore.refreshIn(timeout); +} diff --git a/src/lib/stores/media-user-data.store.ts b/src/lib/stores/media-user-data.store.ts index aa5ddf3..f2bf11d 100644 --- a/src/lib/stores/media-user-data.store.ts +++ b/src/lib/stores/media-user-data.store.ts @@ -10,7 +10,6 @@ import type { SeriesUserDataDto, StreamCandidateDto } from '../apis/reiverr/reiverr.openapi'; -import type { MediaType } from '../types'; import { episodeUserDataStore, libraryItemsDataStore, @@ -76,7 +75,7 @@ async function getAutoplayStream(options: { tmdbId: string; season?: number; epi } function useUserLibrary( - mediaType: MediaType, + mediaType: 'movie' | 'series', tmdbId: string, userDataP: Readable ) { @@ -94,7 +93,7 @@ function useUserLibrary( return; } - const success = await reiverrApi.users + const success = await reiverrApi.library .addLibraryItem(userId, tmdbId, { mediaType }) .then((r) => r.data.success); if (success) { @@ -111,7 +110,7 @@ function useUserLibrary( return; } - const success = await reiverrApi.users + const success = await reiverrApi.library .removeLibraryItem(userId, tmdbId) .then((r) => r.data.success); if (success) { @@ -170,7 +169,7 @@ export function useSeriesUserData(tmdbId: string) { const userDataRequest = seriesUserDataStore.subscribe(tmdbId); const tmdbSeriesRequest = tmdbSeriesDataStore.subscribe(Number(tmdbId)); - const libraryStore = useUserLibrary('Series', tmdbId, userDataRequest); + const libraryStore = useUserLibrary('series', tmdbId, userDataRequest); const canStreamStore = useCanStream(); const episodesUserData = writable([]); const nextEpisode = writable({ @@ -323,7 +322,7 @@ export function useSeriesUserData(tmdbId: string) { export function useMovieUserData(tmdbId: string) { const background = getBackgroundPage(); const userData = movieUserDataStore.subscribe(tmdbId); - const libraryStore = useUserLibrary('Movie', tmdbId, userData); + const libraryStore = useUserLibrary('movie', tmdbId, userData); const canStreamStore = useCanStream(); const isWatchedStore = useIsWatched(userData, (userId, watched) => reiverrApi.users.updateMoviePlayStateByTmdbId(userId, tmdbId, {