diff --git a/client/src/api/items.ts b/client/src/api/items.ts index 6194f0a8..3c01aa04 100644 --- a/client/src/api/items.ts +++ b/client/src/api/items.ts @@ -39,10 +39,15 @@ export const useItems = (args: Items.Paginated.RequestQuery) => { }; }; -export const useRandomItem = (args: Items.Random.RequestQuery) => { +export const useRandomItemFromWatchlist = (args: Items.Random.RequestQuery) => { const { error, data, isFetched } = useQuery( ['randomItems', args], - async () => mediaTrackerApi.items.random(args), + async () => + mediaTrackerApi.items.random({ + ...args, + onlyOnWatchlist: true, + onlyReleased: true, + }), { keepPreviousData: true, } diff --git a/client/src/components/PaginatedGridItems.tsx b/client/src/components/PaginatedGridItems.tsx index ab7b7a54..630be5da 100644 --- a/client/src/components/PaginatedGridItems.tsx +++ b/client/src/components/PaginatedGridItems.tsx @@ -18,7 +18,6 @@ import { GridItemAppearanceArgs, GridItem } from 'src/components/GridItem'; import { useOrderByComponent } from 'src/components/OrderBy'; import { useFilterBy } from 'src/components/FilterBy'; import { useUpdateSearchParams } from 'src/hooks/updateSearchParamsHook'; -import { CheckboxWithTitleAndDescription } from './Checkbox'; import { useShowRepeated } from './ShowRepeated'; const Search: FunctionComponent<{ @@ -85,8 +84,15 @@ export const PaginatedGridItems: FunctionComponent<{ isStatisticsPage?: boolean; showSearch?: boolean; gridItemAppearance?: GridItemAppearanceArgs; + disableShowRepeated?: boolean; }> = (props) => { - const { args, showSortOrderControls, showSearch, gridItemAppearance } = props; + const { + args, + showSortOrderControls, + showSearch, + gridItemAppearance, + disableShowRepeated, + } = props; const [searchParams, setSearchParams] = useSearchParams(); const [searchQuery, setSearchQuery] = useState(); @@ -120,7 +126,8 @@ export const PaginatedGridItems: FunctionComponent<{ const { showRepeated, ShowRepeatedComponenet } = useShowRepeated( props.isStatisticsPage, filter, - orderBy + orderBy, + disableShowRepeated ?? false ); const mainContainerRef = useRef(); diff --git a/client/src/components/ShowRepeated.tsx b/client/src/components/ShowRepeated.tsx index 7767f980..e25eb02b 100644 --- a/client/src/components/ShowRepeated.tsx +++ b/client/src/components/ShowRepeated.tsx @@ -7,7 +7,8 @@ import { MediaItemOrderBy } from 'mediatracker-api'; export const useShowRepeated = ( isStatisticsPage: boolean, filter: unknown, - orderBy: MediaItemOrderBy + orderBy: MediaItemOrderBy, + disable: boolean ) => { const { currentValue, updateSearchParams } = useUpdateSearchParams({ filterParam: 'showRepeated', @@ -26,7 +27,8 @@ export const useShowRepeated = ( if ( orderBy !== 'lastSeen' || isStatisticsPage || - filter['onlyOnWatchlist'] === true + filter['onlyOnWatchlist'] === true || + disable ) { handleToggle(false); return true; @@ -35,7 +37,7 @@ export const useShowRepeated = ( }; return { - showRepeated: showRepeated, + showRepeated: disable ? disable : showRepeated, ShowRepeatedComponenet: () => { if (hideComponenet()) { return null; diff --git a/client/src/pages/Random.tsx b/client/src/pages/Random.tsx index d97586aa..19261fcd 100644 --- a/client/src/pages/Random.tsx +++ b/client/src/pages/Random.tsx @@ -1,6 +1,6 @@ import { MediaType } from 'mediatracker-api'; import React, { FunctionComponent, useEffect, useState } from 'react'; -import { useItems, useRandomItem } from 'src/api/items'; +import { useItems, useRandomItemFromWatchlist } from 'src/api/items'; import { Segment } from './Home'; import { t } from '@lingui/macro'; @@ -13,19 +13,19 @@ const mediaTypes: { [mediaType in MediaType]: string } = { }; export const Random: FunctionComponent = () => { - const { items: randomMovie } = useRandomItem({ + const { items: randomMovie } = useRandomItemFromWatchlist({ mediaType: 'movie', }); - const { items: randomTv } = useRandomItem({ + const { items: randomTv } = useRandomItemFromWatchlist({ mediaType: 'tv', }); - const { items: randomVideoGame } = useRandomItem({ + const { items: randomVideoGame } = useRandomItemFromWatchlist({ mediaType: 'video_game', }); - const { items: randomBook } = useRandomItem({ + const { items: randomBook } = useRandomItemFromWatchlist({ mediaType: 'book', }); - const { items: randomAudioBook } = useRandomItem({ + const { items: randomAudioBook } = useRandomItemFromWatchlist({ mediaType: 'audiobook', }); diff --git a/client/src/pages/WatchlistPage.tsx b/client/src/pages/WatchlistPage.tsx index dba896f4..5c78bde0 100644 --- a/client/src/pages/WatchlistPage.tsx +++ b/client/src/pages/WatchlistPage.tsx @@ -10,6 +10,7 @@ export const WatchlistPage: FunctionComponent = () => { onlyOnWatchlist: true, }} showSortOrderControls={true} + disableShowRepeated={true} showSearch={false} gridItemAppearance={{ showRating: true, diff --git a/server/__tests__/__utils__/data.ts b/server/__tests__/__utils__/data.ts index 6e6feb8d..7943e3b1 100644 --- a/server/__tests__/__utils__/data.ts +++ b/server/__tests__/__utils__/data.ts @@ -34,15 +34,15 @@ export class Data { return result; }; - static generateMovies = (count: number, start: number) => { + static generateMovies = (count: number, start: number, inFuture = false) => { const result = []; for (let i = start; i < count + start; i++) { - result.push(this.generateMovie(i)); + result.push(this.generateMovie(i, inFuture)); } return result; }; - static generateMovie = (id: number) => { + static generateMovie = (id: number, inFuture: boolean) => { const new_id = id; return { id: new_id, @@ -52,7 +52,9 @@ export class Data { title: 'movie' + new_id, externalPosterUrl: 'posterUrl', externalBackdropUrl: 'backdropUrl', - releaseDate: `${1971 + new_id}-04-12`, + releaseDate: `${ + inFuture ? new Date().getFullYear() + new_id : 1971 + new_id + }-04-12`, tmdbId: 123456 + new_id, runtime: 124 + new_id, }; @@ -271,6 +273,20 @@ export class Data { addedAt: new Date().getTime(), }; + static addMoviesToWatchlist = (count: number, start: number) => { + const result = []; + for (let i = start; i < count + start; i++) { + const item = { + id: i + 1, + listId: Data.watchlist.id, + mediaItemId: i, + addedAt: new Date().getTime(), + }; + result.push(item); + } + return result; + }; + static list: List = { id: 1, createdAt: new Date().getTime(), diff --git a/server/__tests__/controllers/itmes.test.ts b/server/__tests__/controllers/itmes.test.ts index f4758429..a64bb4b0 100644 --- a/server/__tests__/controllers/itmes.test.ts +++ b/server/__tests__/controllers/itmes.test.ts @@ -470,4 +470,29 @@ describe('listItemController', () => { 5 ); }); + + test('should show items before today', async () => { + const itemsController = new ItemsController(); + await Database.knex('list').insert(Data.watchlist); + await Database.knex('mediaItem').insert(Data.generateMovies(10, 0)); + await Database.knex('listItem').insert(Data.addMoviesToWatchlist(10, 0)); + await Database.knex('mediaItem').insert(Data.generateMovies(5, 10, true)); + await Database.knex('listItem').insert(Data.addMoviesToWatchlist(5, 10)); + + const res = await request(itemsController.getPaginated, { + userId: Data.user.id, + requestQuery: { + mediaType: 'movie', + orderBy: 'lastSeen', + sortOrder: 'desc', + page: 1, + onlyReleased: true, + onlyOnWatchlist: true, + }, + }); + + expect((res.data as Pagination).data.length).toBe( + 10 + ); + }); }); diff --git a/server/openapi.json b/server/openapi.json index 83667af7..2d1ba435 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "MediaTracker", - "version": "0.1.0", + "version": "0.2.0", "license": { "name": "MIT", "url": "https://opensource.org/licenses/MIT" @@ -408,6 +408,15 @@ "nullable": true } }, + { + "name": "onlyReleased", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + }, { "name": "year", "in": "query", @@ -622,6 +631,15 @@ "nullable": true } }, + { + "name": "onlyReleased", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + }, { "name": "year", "in": "query", @@ -696,6 +714,24 @@ ], "nullable": true } + }, + { + "name": "onlyOnWatchlist", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + }, + { + "name": "onlyReleased", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } } ], "responses": { @@ -3878,6 +3914,10 @@ "type": "boolean", "nullable": true }, + "onlyReleased": { + "type": "boolean", + "nullable": true + }, "year": { "type": "string", "nullable": true @@ -3946,6 +3986,14 @@ } ], "nullable": true + }, + "onlyOnWatchlist": { + "type": "boolean", + "nullable": true + }, + "onlyReleased": { + "type": "boolean", + "nullable": true } } }, diff --git a/server/package-lock.json b/server/package-lock.json index 855eb493..3d929fa6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediatracker-plus", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mediatracker-plus", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@lingui/core": "^3.17.2", diff --git a/server/package.json b/server/package.json index ebb5e339..13e361c0 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "mediatracker-plus", - "version": "0.2.0", + "version": "0.2.1", "description": "Self hosted media tracker for movies, tv shows, video games, books and audiobooks", "repository": { "type": "git", diff --git a/server/src/controllers/items.ts b/server/src/controllers/items.ts index 8142177d..e4ed03ab 100644 --- a/server/src/controllers/items.ts +++ b/server/src/controllers/items.ts @@ -15,7 +15,7 @@ export type GetItemsRequest = Omit< export type GetRandomItemsRequest = Omit< GetRandomItemsArgs, - 'userId' | 'selectRandom' | 'onlyOnWatchlist' + 'userId' | 'selectRandom' >; export class ItemsController { @@ -46,6 +46,7 @@ export class ItemsController { onlyWithoutUserRating, onlyWithProgress, showRepeated, + onlyReleased, } = req.query; const orderBy = req.query.orderBy || 'title'; @@ -74,6 +75,7 @@ export class ItemsController { onlyWithProgress: onlyWithProgress, showRepeated: showRepeated, selectRandom: false, + onlyReleased: onlyReleased, }); res.send(result); @@ -102,6 +104,7 @@ export class ItemsController { onlyWithUserRating, onlyWithoutUserRating, onlyWithProgress, + onlyReleased, } = req.query; const orderBy = req.query.orderBy || 'title'; @@ -120,6 +123,7 @@ export class ItemsController { onlyWithUserRating: onlyWithUserRating, onlyWithoutUserRating: onlyWithoutUserRating, onlyWithProgress: onlyWithProgress, + onlyReleased: onlyReleased, selectRandom: false, }); @@ -139,13 +143,14 @@ export class ItemsController { }>(async (req, res) => { const userId = Number(req.user); - const { mediaType } = req.query; + const { mediaType, onlyOnWatchlist, onlyReleased } = req.query; const result = await mediaItemRepository.items({ userId: userId, mediaType: mediaType, selectRandom: true, - onlyOnWatchlist: true, + onlyOnWatchlist: onlyOnWatchlist, + onlyReleased: onlyReleased, }); res.send(result); diff --git a/server/src/generated/routes/routes.ts b/server/src/generated/routes/routes.ts index 691a4b2a..db2d6309 100644 --- a/server/src/generated/routes/routes.ts +++ b/server/src/generated/routes/routes.ts @@ -387,6 +387,7 @@ router.get( onlyWithNextAiring: { type: ['boolean', 'null'] }, onlyWithUserRating: { type: ['boolean', 'null'] }, onlyWithoutUserRating: { type: ['boolean', 'null'] }, + onlyReleased: { type: ['boolean', 'null'] }, year: { type: ['string', 'null'] }, genre: { type: ['string', 'null'] }, showRepeated: { type: ['boolean', 'null'] }, @@ -476,6 +477,7 @@ router.get( onlyWithNextAiring: { type: ['boolean', 'null'] }, onlyWithUserRating: { type: ['boolean', 'null'] }, onlyWithoutUserRating: { type: ['boolean', 'null'] }, + onlyReleased: { type: ['boolean', 'null'] }, year: { type: ['string', 'null'] }, genre: { type: ['string', 'null'] }, showRepeated: { type: ['boolean', 'null'] }, @@ -497,6 +499,8 @@ router.get( mediaType: { oneOf: [{ $ref: '#/definitions/MediaType' }, { type: 'null' }], }, + onlyOnWatchlist: { type: ['boolean', 'null'] }, + onlyReleased: { type: ['boolean', 'null'] }, }, }, MediaType: { diff --git a/server/src/knex/queries/items.ts b/server/src/knex/queries/items.ts index 6d7b0257..3115f130 100644 --- a/server/src/knex/queries/items.ts +++ b/server/src/knex/queries/items.ts @@ -67,6 +67,7 @@ const getItemsKnexSql = async (args: GetItemsArgs & { year: string }) => { sortOrder, onlyWithNextEpisodesToWatch, onlyWithNextAiring, + onlyReleased, mediaItemIds, onlyWithUserRating, onlyWithoutUserRating, @@ -355,6 +356,10 @@ const getItemsKnexSql = async (args: GetItemsArgs & { year: string }) => { ); } + if (onlyReleased === true) { + query.where('mediaItem.releaseDate', '<=', currentDateString); + } + // Media type if (mediaType) { query.andWhere('mediaItem.mediaType', mediaType); diff --git a/server/src/repository/mediaItem.ts b/server/src/repository/mediaItem.ts index cbe66a8a..be6c844c 100644 --- a/server/src/repository/mediaItem.ts +++ b/server/src/repository/mediaItem.ts @@ -82,6 +82,10 @@ export type GetItemsArgs = { * @description Return only items without user rating */ onlyWithoutUserRating?: boolean; + /** + * @description Return only items where release Date is before today + */ + onlyReleased?: boolean; /** * @description Filter by Year */ @@ -107,7 +111,8 @@ export type GetRandomItemsArgs = { userId: number; mediaType?: MediaType; selectRandom?: true; - onlyOnWatchlist?: true; + onlyOnWatchlist?: boolean; + onlyReleased?: boolean; }; class MediaItemRepository extends repository({