Skip to content

Commit 5557de0

Browse files
committed
initial commit after updating to new branch and fixing conflicts
1 parent 7c39b47 commit 5557de0

File tree

25 files changed

+983
-208
lines changed

25 files changed

+983
-208
lines changed

server/src/modules/api/plex-api/plex-api.controller.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,21 @@ export class PlexApiController {
3030
getLibraries() {
3131
return this.plexApiService.getLibraries();
3232
}
33-
@Get('library/:id/content{/:page}')
34-
getLibraryContent(
33+
@Get('library/:id/content')
34+
async getPagedContent(
3535
@Param('id') id: string,
36-
@Param('page', new ParseIntPipe()) page: number,
37-
@Query('amount') amount: number,
36+
@Query('page') page = '1',
37+
@Query('size') size = '120',
38+
@Query('sort') sort = 'addedAt:desc',
3839
) {
39-
const size = amount ? amount : 50;
40-
const offset = (page - 1) * size;
41-
return this.plexApiService.getLibraryContents(id, {
42-
offset: offset,
43-
size: size,
44-
});
40+
const offset = (parseInt(page) - 1) * parseInt(size);
41+
42+
return this.plexApiService.getLibraryContents(
43+
id,
44+
{ offset, size: parseInt(size) },
45+
undefined,
46+
sort,
47+
);
4548
}
4649
@Get('library/:id/content/search/:query')
4750
searchibraryContent(

server/src/modules/api/plex-api/plex-api.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,13 @@ export class PlexApiService {
241241
id: string,
242242
{ offset = 0, size = 50 }: { offset?: number; size?: number } = {},
243243
datatype?: EPlexDataType,
244+
sort?: string,
244245
): Promise<{ totalSize: number; items: PlexLibraryItem[] }> {
245246
try {
246247
const type = datatype ? '&type=' + datatype : '';
248+
const sortParam = sort ? '&sort=' + sort : '';
247249
const response = await this.plexClient.query<PlexLibraryResponse>({
248-
uri: `/library/sections/${id}/all?includeGuids=1${type}`,
250+
uri: `/library/sections/${id}/all?includeGuids=1${type}${sortParam}`,
249251
extraHeaders: {
250252
'X-Plex-Container-Start': `${offset}`,
251253
'X-Plex-Container-Size': `${size}`,

server/src/modules/api/tmdb-api/tmdb.controller.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
1+
import { Controller, Get, Param, ParseIntPipe, Res } from '@nestjs/common';
2+
import { Response } from 'express';
23
import { TmdbApiService } from './tmdb.service';
34

45
@Controller('api/moviedb')
@@ -24,10 +25,11 @@ export class TmdbApiController {
2425
return this.movieDbApi.getBackdropImagePath({ tmdbId: tmdbId, type: type });
2526
}
2627
@Get('/image/:type/:tmdbId')
27-
getImage(
28+
streamImage(
2829
@Param('tmdbId', new ParseIntPipe()) tmdbId: number,
2930
@Param('type') type: 'movie' | 'show',
31+
@Res() res: Response,
3032
) {
31-
return this.movieDbApi.getImagePath({ tmdbId: tmdbId, type: type });
33+
return this.movieDbApi.streamImage(tmdbId, type, res);
3234
}
3335
}

server/src/modules/api/tmdb-api/tmdb.service.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Injectable } from '@nestjs/common';
2+
import axios from 'axios';
3+
import { Response } from 'express';
24
import { MaintainerrLogger } from '../../logging/logs.service';
35
import { ExternalApiService } from '../external-api/external-api.service';
46
import cacheManager from '../lib/cache';
@@ -98,23 +100,38 @@ export class TmdbApiService extends ExternalApiService {
98100
}
99101
};
100102

101-
// TODO: ADD CACHING!!!!
102-
public getImagePath = async ({
103-
tmdbId,
104-
type,
105-
}: {
106-
tmdbId: number;
107-
type: 'movie' | 'show';
108-
}): Promise<string> => {
103+
public streamImage = async (
104+
tmdbId: number,
105+
type: 'movie' | 'show',
106+
res: Response,
107+
): Promise<void> => {
109108
try {
110-
if (type === 'movie') {
111-
return (await this.getMovie({ movieId: tmdbId }))?.poster_path;
112-
} else {
113-
return (await this.getTvShow({ tvId: tmdbId }))?.poster_path;
109+
const posterPath =
110+
type === 'movie'
111+
? (await this.getMovie({ movieId: tmdbId }))?.poster_path
112+
: (await this.getTvShow({ tvId: tmdbId }))?.poster_path;
113+
114+
if (!posterPath) {
115+
res.status(404).send('Poster not found');
116+
return;
114117
}
118+
119+
const imageUrl = `https://image.tmdb.org/t/p/w300_and_h450_face${posterPath}`;
120+
const response = await axios.get(imageUrl, {
121+
responseType: 'stream',
122+
});
123+
124+
// Set caching headers
125+
res.set({
126+
'Content-Type': response.headers['content-type'],
127+
'Cache-Control': 'public, max-age=86400',
128+
Expires: new Date(Date.now() + 86400000).toUTCString(),
129+
});
130+
131+
response.data.pipe(res);
115132
} catch (e) {
116-
this.logger.warn(`Failed to fetch image path: ${e.message}`);
117-
this.logger.debug(e);
133+
this.logger.warn(`[TMDb] Failed to stream image: ${e.message}`);
134+
res.status(500).send('Failed to stream image');
118135
}
119136
};
120137

server/src/modules/rules/rules.controller.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ export class RulesController {
5656
return this.rulesService.getExclusions(query.rulegroupId, query.plexId);
5757
}
5858

59+
@Get('/exclusion/all')
60+
async getAllExcludedItems() {
61+
const exclusions = await this.rulesService.getAllExclusions();
62+
63+
// Return array of objects like: { plexId: 12345, type: 3 }
64+
return exclusions.map((excl) => ({
65+
plexId: +excl.plexId,
66+
type: excl.type,
67+
ruleGroupId: excl.ruleGroupId,
68+
id: excl.id,
69+
parent: excl.parent,
70+
}));
71+
}
72+
5973
@Get('/count')
6074
async getRuleGroupCount() {
6175
return this.rulesService.getRuleGroupCount();

ui/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
"next": "^15.3.2",
3030
"react": "^19.1.0",
3131
"react-dom": "^19.1.0",
32-
"react-hook-form": "^7.56.4",
32+
"react-hook-form": "^7.55.0",
33+
"react-intersection-observer": "^9.16.0",
3334
"react-markdown": "10.1.0",
3435
"react-select": "^5.10.1",
3536
"react-toastify": "^11.0.5",
37+
"react-tooltip": "^5.28.1",
3638
"reconnecting-eventsource": "^1.6.4",
3739
"yaml": "^2.8.0",
3840
"zod": "^3.24.4"

ui/src/components/Collection/CollectionDetail/Exclusions/index.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { useEffect, useRef, useState } from 'react'
2-
import OverviewContent, { IPlexMetadata } from '../../../Overview/Content'
31
import _ from 'lodash'
2+
import { useEffect, useRef, useState } from 'react'
43
import { ICollection } from '../..'
54
import GetApiHandler from '../../../../utils/ApiHandler'
5+
import OverviewContent, { IPlexMetadata } from '../../../Overview/Content'
66

77
interface ICollectionExclusions {
88
collection: ICollection
@@ -116,18 +116,12 @@ const CollectionExcludions = (props: ICollectionExclusions) => {
116116

117117
return (
118118
<OverviewContent
119-
dataFinished={true}
120-
fetchData={() => {}}
121119
loading={loadingRef.current}
120+
viewMode="poster"
122121
data={data}
123122
libraryId={props.libraryId}
124123
collectionPage={true}
125124
collectionId={props.collection.id}
126-
extrasLoading={
127-
loadingExtraRef &&
128-
!loadingRef.current &&
129-
totalSize >= pageData.current * fetchAmount
130-
}
131125
onRemove={(id: string) =>
132126
setTimeout(() => {
133127
setData(dataRef.current.filter((el) => +el.ratingKey !== +id))

ui/src/components/Collection/CollectionDetail/index.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,11 @@ const CollectionDetail: React.FC<ICollectionDetail> = (
183183

184184
{selectedTab === 'media' ? (
185185
<OverviewContent
186-
dataFinished={true}
187-
fetchData={() => {}}
188186
loading={loadingRef.current}
187+
viewMode="poster"
189188
data={data}
190189
libraryId={props.libraryId}
191190
collectionPage={true}
192-
extrasLoading={
193-
loadingExtraRef &&
194-
!loadingRef.current &&
195-
totalSize >= pageData.current * fetchAmount
196-
}
197191
onRemove={(id: string) =>
198192
setTimeout(() => {
199193
setData(dataRef.current.filter((el) => +el.ratingKey !== +id))

ui/src/components/Collection/CollectionItem/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ const CollectionItem = (props: ICollectionItem) => {
2525
className="backdrop-image"
2626
width="600"
2727
height="800"
28-
src={`https://image.tmdb.org/t/p/w500${props.collection.media[0].image_path}`}
28+
src={`/api/moviedb/image/movie/${props.collection.media[0].tmdbId}`}
2929
alt="img"
3030
/>
3131
<CachedImage
3232
className="backdrop-image"
3333
width="600"
3434
height="800"
35-
src={`https://image.tmdb.org/t/p/w500/${props.collection.media[1].image_path}`}
35+
src={`/api/moviedb/image/movie/${props.collection.media[1].tmdbId}`}
3636
alt="img"
3737
/>
3838
<div className="collection-backdrop"></div>

ui/src/components/Collection/CollectionOverview/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const CollectionOverview = (props: ICollectionOverview) => {
1818
<div>
1919
<LibrarySwitcher onSwitch={props.onSwitchLibrary} />
2020

21-
<div className="m-auto mb-3 flex">
21+
<div className="m-auto mb-3 mt-4 flex">
2222
<div className="m-auto sm:m-0">
2323
<ExecuteButton
2424
onClick={props.doActions}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { FilterIcon } from '@heroicons/react/solid'
2+
import React, { useEffect, useRef, useState } from 'react'
3+
4+
export type FilterOption = 'all' | 'excluded' | 'nonExcluded'
5+
6+
interface FilterDropdownProps {
7+
value: FilterOption
8+
onChange: (value: FilterOption) => void
9+
}
10+
11+
const FILTER_OPTIONS: { label: string; value: FilterOption }[] = [
12+
{ label: 'All Items', value: 'all' },
13+
{ label: 'Excluded Only', value: 'excluded' },
14+
{ label: 'Non-Excluded Only', value: 'nonExcluded' },
15+
]
16+
17+
const FilterDropdown: React.FC<FilterDropdownProps> = ({ value, onChange }) => {
18+
const [open, setOpen] = useState(false)
19+
20+
const dropdownRef = useRef<HTMLDivElement>(null)
21+
22+
useEffect(() => {
23+
const handleClickOutside = (event: MouseEvent) => {
24+
if (
25+
dropdownRef.current &&
26+
!dropdownRef.current.contains(event.target as Node)
27+
) {
28+
setOpen(false)
29+
}
30+
}
31+
32+
if (open) {
33+
document.addEventListener('mousedown', handleClickOutside)
34+
} else {
35+
document.removeEventListener('mousedown', handleClickOutside)
36+
}
37+
38+
return () => {
39+
document.removeEventListener('mousedown', handleClickOutside)
40+
}
41+
}, [open])
42+
43+
return (
44+
<div ref={dropdownRef} className="relative ml-2 inline-block text-left">
45+
<button
46+
type="button"
47+
aria-label="Filter"
48+
onClick={() => setOpen(!open)}
49+
className="relative inline-flex items-center rounded-md bg-zinc-700 p-2 text-sm font-medium text-white hover:bg-zinc-600 focus:outline-none"
50+
>
51+
<FilterIcon className="mr-2 h-5 w-5" />
52+
{'Filter'}
53+
{/* Active Filter Dot */}
54+
{value !== 'all' && (
55+
<span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-amber-400" />
56+
)}
57+
<svg className="ml-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
58+
<path
59+
fillRule="evenodd"
60+
d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.24 4.25a.75.75 0 01-1.06 0L5.23 8.29a.75.75 0 01.02-1.08z"
61+
clipRule="evenodd"
62+
/>
63+
</svg>
64+
</button>
65+
66+
{open && (
67+
<div className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-zinc-800">
68+
<div className="py-1">
69+
{FILTER_OPTIONS.map((option) => (
70+
<button
71+
key={option.value}
72+
onClick={() => {
73+
onChange(option.value)
74+
setOpen(false)
75+
}}
76+
className={`w-full px-4 py-2 text-left text-sm ${
77+
value === option.value
78+
? 'bg-zinc-100 font-medium dark:bg-zinc-700'
79+
: 'hover:bg-zinc-100 dark:hover:bg-zinc-700'
80+
} text-gray-800 dark:text-white`}
81+
>
82+
{option.label}
83+
</button>
84+
))}
85+
</div>
86+
</div>
87+
)}
88+
</div>
89+
)
90+
}
91+
92+
export default FilterDropdown

ui/src/components/Common/LibrarySwitcher/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import GetApiHandler from '../../../utils/ApiHandler'
44

55
interface ILibrarySwitcher {
66
onSwitch: (libraryId: number) => void
7+
value?: number
78
allPossible?: boolean
89
}
910

@@ -31,11 +32,12 @@ const LibrarySwitcher = (props: ILibrarySwitcher) => {
3132

3233
return (
3334
<>
34-
<div className="mb-5 w-full">
35+
<div className="max-w-full">
3536
<form>
3637
<select
3738
className="border-zinc-600 hover:border-zinc-500 focus:border-zinc-500 focus:bg-opacity-100 focus:placeholder-zinc-400 focus:outline-none focus:ring-0"
3839
onChange={onSwitchLibrary}
40+
value={props.value}
3941
>
4042
{props.allPossible === undefined || props.allPossible ? (
4143
<option value={9999}>All</option>

0 commit comments

Comments
 (0)