Compare commits
12 Commits
2acb4fc4a4
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc143faf7 | ||
|
|
206f586a11 | ||
|
|
e4874f5792 | ||
|
|
6005422cea | ||
|
|
d8404496b8 | ||
|
|
41bb2c3a2c | ||
|
|
53d7b56265 | ||
|
|
b012ccb500 | ||
|
|
8eb5dddd7b | ||
|
|
e87b093ea5 | ||
|
|
3fb73e4e62 | ||
|
|
246e40a508 |
@@ -5,7 +5,7 @@ import { getSettings, MetadataProviderType } from '@server/lib/settings';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
|
||||||
export const getMetadataProvider = async (
|
export const getMetadataProvider = async (
|
||||||
mediaType: 'movie' | 'tv' | 'anime'
|
mediaType: 'movie' | 'tv' | 'anime' | 'music' | 'book'
|
||||||
): Promise<TvShowProvider> => {
|
): Promise<TvShowProvider> => {
|
||||||
try {
|
try {
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
|
|||||||
@@ -250,7 +250,8 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply overrides if the user is not an admin or has the "advanced request" permission
|
// Apply overrides if the user is not an admin or has the "advanced request" permission
|
||||||
const useOverrides = !user.hasPermission([Permission.MANAGE_REQUESTS], {
|
// Override rules only apply to movie/TV (they use TMDB keywords and Radarr/Sonarr)
|
||||||
|
const useOverrides = !isMusicOrBook && !user.hasPermission([Permission.MANAGE_REQUESTS], {
|
||||||
type: 'or',
|
type: 'or',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,7 +259,7 @@ export class MediaRequest {
|
|||||||
let profileId = requestBody.profileId;
|
let profileId = requestBody.profileId;
|
||||||
let tags = requestBody.tags;
|
let tags = requestBody.tags;
|
||||||
|
|
||||||
if (useOverrides) {
|
if (useOverrides && tmdbMedia) {
|
||||||
const defaultRadarrId = requestBody.is4k
|
const defaultRadarrId = requestBody.is4k
|
||||||
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||||
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||||
@@ -307,7 +308,7 @@ export class MediaRequest {
|
|||||||
.split(',')
|
.split(',')
|
||||||
.some((genreId) =>
|
.some((genreId) =>
|
||||||
tmdbMedia.genres
|
tmdbMedia.genres
|
||||||
.map((genre) => genre.id)
|
.map((genre: { id: number }) => genre.id)
|
||||||
.includes(Number(genreId))
|
.includes(Number(genreId))
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -377,6 +378,11 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle music/book requests via simplified flow
|
||||||
|
if (isMusicOrBook) {
|
||||||
|
return MediaRequest.requestMusicOrBook(requestBody, requestUser, options);
|
||||||
|
}
|
||||||
|
|
||||||
if (requestBody.mediaType === MediaType.MOVIE) {
|
if (requestBody.mediaType === MediaType.MOVIE) {
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface WatchlistItem {
|
|||||||
id: number;
|
id: number;
|
||||||
ratingKey: string;
|
ratingKey: string;
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv' | 'music' | 'book';
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export interface QuotaStatus {
|
|||||||
export interface QuotaResponse {
|
export interface QuotaResponse {
|
||||||
movie: QuotaStatus;
|
movie: QuotaStatus;
|
||||||
tv: QuotaStatus;
|
tv: QuotaStatus;
|
||||||
|
music?: QuotaStatus;
|
||||||
|
book?: QuotaStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserWatchDataResponse {
|
export interface UserWatchDataResponse {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export type AvailableCacheIds =
|
|||||||
| 'tmdb'
|
| 'tmdb'
|
||||||
| 'radarr'
|
| 'radarr'
|
||||||
| 'sonarr'
|
| 'sonarr'
|
||||||
|
| 'lidarr'
|
||||||
|
| 'readarr'
|
||||||
| 'rt'
|
| 'rt'
|
||||||
| 'imdb'
|
| 'imdb'
|
||||||
| 'github'
|
| 'github'
|
||||||
@@ -50,6 +52,8 @@ class CacheManager {
|
|||||||
}),
|
}),
|
||||||
radarr: new Cache('radarr', 'Radarr API'),
|
radarr: new Cache('radarr', 'Radarr API'),
|
||||||
sonarr: new Cache('sonarr', 'Sonarr API'),
|
sonarr: new Cache('sonarr', 'Sonarr API'),
|
||||||
|
lidarr: new Cache('lidarr', 'Lidarr API'),
|
||||||
|
readarr: new Cache('readarr', 'Readarr API'),
|
||||||
rt: new Cache('rt', 'Rotten Tomatoes API', {
|
rt: new Cache('rt', 'Rotten Tomatoes API', {
|
||||||
stdTtl: 43200,
|
stdTtl: 43200,
|
||||||
checkPeriod: 60 * 30,
|
checkPeriod: 60 * 30,
|
||||||
|
|||||||
@@ -703,7 +703,7 @@ discoverRoutes.get('/trending', async (req, res, next) => {
|
|||||||
const tmdb = createTmdbWithRegionLanguage(req.user);
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mediaType = (req.query.mediaType as 'all' | 'movie' | 'tv') ?? 'all';
|
const mediaType = (req.query.mediaType as 'all' | 'movie' | 'tv' | 'music' | 'book') ?? 'all';
|
||||||
const timeWindow =
|
const timeWindow =
|
||||||
(req.query.timeWindow as 'day' | 'week') === 'week' ? 'week' : 'day';
|
(req.query.timeWindow as 'day' | 'week') === 'week' ? 'week' : 'day';
|
||||||
const language = (req.query.language as string) ?? req.locale;
|
const language = (req.query.language as string) ?? req.locale;
|
||||||
@@ -737,7 +737,9 @@ discoverRoutes.get('/trending', async (req, res, next) => {
|
|||||||
}),
|
}),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const { data, mapper, type } = await trendingFetchers[mediaType]();
|
// Music/book don't have TMDB trending - fall back to 'all'
|
||||||
|
const fetcherKey = (mediaType === 'music' || mediaType === 'book') ? 'all' : mediaType;
|
||||||
|
const { data, mapper, type } = await trendingFetchers[fetcherKey]();
|
||||||
|
|
||||||
const media = await Media.getRelatedMedia(
|
const media = await Media.getRelatedMedia(
|
||||||
req.user,
|
req.user,
|
||||||
|
|||||||
@@ -244,6 +244,14 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
?.profiles?.find((profile) => profile.id === r.profileId)?.name,
|
?.profiles?.find((profile) => profile.id === r.profileId)?.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case MediaType.MUSIC:
|
||||||
|
case MediaType.BOOK:
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
profileName: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -273,6 +281,14 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case MediaType.MUSIC:
|
||||||
|
case MediaType.BOOK:
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
canRemove: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useIntl } from 'react-intl';
|
|||||||
|
|
||||||
interface BlocklistModalProps {
|
interface BlocklistModalProps {
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
type: 'movie' | 'tv' | 'collection';
|
type: 'movie' | 'tv' | 'music' | 'book' | 'collection';
|
||||||
show: boolean;
|
show: boolean;
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const ListView = ({
|
|||||||
<TmdbTitleCard
|
<TmdbTitleCard
|
||||||
id={title.tmdbId}
|
id={title.tmdbId}
|
||||||
tmdbId={title.tmdbId}
|
tmdbId={title.tmdbId}
|
||||||
type={title.mediaType}
|
type={title.mediaType as 'movie' | 'tv'}
|
||||||
isAddedToWatchlist={true}
|
isAddedToWatchlist={true}
|
||||||
canExpand
|
canExpand
|
||||||
mutateParent={mutateParent}
|
mutateParent={mutateParent}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const messages = defineMessages('components.Discover.FilterSlideover', {
|
|||||||
type FilterSlideoverProps = {
|
type FilterSlideoverProps = {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv' | 'music' | 'book';
|
||||||
currentFilters: FilterOptions;
|
currentFilters: FilterOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const PlexWatchlistSlider = () => {
|
|||||||
id={item.tmdbId}
|
id={item.tmdbId}
|
||||||
key={`watchlist-slider-item-${item.ratingKey}`}
|
key={`watchlist-slider-item-${item.ratingKey}`}
|
||||||
tmdbId={item.tmdbId}
|
tmdbId={item.tmdbId}
|
||||||
type={item.mediaType}
|
type={item.mediaType as 'movie' | 'tv'}
|
||||||
isAddedToWatchlist={true}
|
isAddedToWatchlist={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const RecentlyAddedSlider = () => {
|
|||||||
id={item.id}
|
id={item.id}
|
||||||
tmdbId={item.tmdbId}
|
tmdbId={item.tmdbId}
|
||||||
tvdbId={item.tvdbId}
|
tvdbId={item.tvdbId}
|
||||||
type={item.mediaType}
|
type={item.mediaType as 'movie' | 'tv'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const messages = defineMessages('components.Discover', {
|
|||||||
timeWindowWeek: 'Weekly',
|
timeWindowWeek: 'Weekly',
|
||||||
});
|
});
|
||||||
|
|
||||||
type MediaType = 'all' | 'movie' | 'tv';
|
type MediaType = 'all' | 'movie' | 'tv' | 'music' | 'book';
|
||||||
|
|
||||||
type TimeWindow = 'day' | 'week';
|
type TimeWindow = 'day' | 'week';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import useSettings from '@app/hooks/useSettings';
|
|||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
|
||||||
type ExternalLinkType = 'movie' | 'tv' | 'person';
|
type ExternalLinkType = 'movie' | 'tv' | 'person' | 'music' | 'book';
|
||||||
|
|
||||||
interface ExternalLinkBlockProps {
|
interface ExternalLinkBlockProps {
|
||||||
mediaType: ExternalLinkType;
|
mediaType: ExternalLinkType;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const classNames = (...classes: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface CreateIssueModalProps {
|
interface CreateIssueModalProps {
|
||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv' | 'music' | 'book';
|
||||||
tmdbId?: number;
|
tmdbId?: number;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const messages = defineMessages('components.IssueModal', {
|
|||||||
interface IssueOption {
|
interface IssueOption {
|
||||||
name: MessageDescriptor;
|
name: MessageDescriptor;
|
||||||
issueType: IssueType;
|
issueType: IssueType;
|
||||||
mediaType?: 'movie' | 'tv';
|
mediaType?: 'movie' | 'tv' | 'music' | 'book';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const issueOptions: IssueOption[] = [
|
export const issueOptions: IssueOption[] = [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Transition } from '@headlessui/react';
|
|||||||
interface IssueModalProps {
|
interface IssueModalProps {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv' | 'music' | 'book';
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
issueId?: never;
|
issueId?: never;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ManageSlideOverProps {
|
interface ManageSlideOverProps {
|
||||||
// mediaType: 'movie' | 'tv';
|
// mediaType: 'movie' | 'tv' | 'music' | 'book';
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
revalidate: () => void;
|
revalidate: () => void;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const messages = defineMessages('components.PersonDetails', {
|
|||||||
ascharacter: 'as {character}',
|
ascharacter: 'as {character}',
|
||||||
});
|
});
|
||||||
|
|
||||||
type MediaType = 'all' | 'movie' | 'tv';
|
type MediaType = 'all' | 'movie' | 'tv' | 'music' | 'book';
|
||||||
|
|
||||||
const PersonDetails = () => {
|
const PersonDetails = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const messages = defineMessages('components.QuotaSelector', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface QuotaSelectorProps {
|
interface QuotaSelectorProps {
|
||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv' | 'music' | 'book';
|
||||||
defaultDays?: number;
|
defaultDays?: number;
|
||||||
defaultLimit?: number;
|
defaultLimit?: number;
|
||||||
dayOverride?: number;
|
dayOverride?: number;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ interface ButtonOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RequestButtonProps {
|
interface RequestButtonProps {
|
||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv' | 'music' | 'book';
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
media?: Media;
|
media?: Media;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export type RequestOverrides = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface AdvancedRequesterProps {
|
interface AdvancedRequesterProps {
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv' | 'music' | 'book';
|
||||||
is4k: boolean;
|
is4k: boolean;
|
||||||
isAnime?: boolean;
|
isAnime?: boolean;
|
||||||
defaultOverrides?: RequestOverrides;
|
defaultOverrides?: RequestOverrides;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const messages = defineMessages('components.RequestModal.QuotaDisplay', {
|
|||||||
|
|
||||||
interface QuotaDisplayProps {
|
interface QuotaDisplayProps {
|
||||||
quota?: QuotaStatus;
|
quota?: QuotaStatus;
|
||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv' | 'music' | 'book';
|
||||||
userOverride?: number | null;
|
userOverride?: number | null;
|
||||||
remaining?: number;
|
remaining?: number;
|
||||||
overLimit?: number;
|
overLimit?: number;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
|||||||
|
|
||||||
interface RequestModalProps {
|
interface RequestModalProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
type: 'movie' | 'tv' | 'collection';
|
type: 'movie' | 'tv' | 'music' | 'book' | 'collection';
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
is4k?: boolean;
|
is4k?: boolean;
|
||||||
editRequest?: NonFunctionProperties<MediaRequest>;
|
editRequest?: NonFunctionProperties<MediaRequest>;
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const CompanySelector = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
type GenreSelectorProps = (BaseSelectorMultiProps | BaseSelectorSingleProps) & {
|
type GenreSelectorProps = (BaseSelectorMultiProps | BaseSelectorSingleProps) & {
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv' | 'music' | 'book';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GenreSelector = ({
|
export const GenreSelector = ({
|
||||||
@@ -369,7 +369,7 @@ export const KeywordSelector = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
type WatchProviderSelectorProps = {
|
type WatchProviderSelectorProps = {
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv' | 'music' | 'book';
|
||||||
region?: string;
|
region?: string;
|
||||||
activeProviders?: number[];
|
activeProviders?: number[];
|
||||||
onChange: (region: string, value: number[]) => void;
|
onChange: (region: string, value: number[]) => void;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ interface StatusBadgeProps {
|
|||||||
plexUrl?: string;
|
plexUrl?: string;
|
||||||
serviceUrl?: string;
|
serviceUrl?: string;
|
||||||
tmdbId?: number;
|
tmdbId?: number;
|
||||||
mediaType?: 'movie' | 'tv';
|
mediaType?: 'movie' | 'tv' | 'music' | 'book';
|
||||||
title?: string | string[];
|
title?: string | string[];
|
||||||
statusLabelOverride?: string;
|
statusLabelOverride?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface ErrorCardProps {
|
|||||||
id: number;
|
id: number;
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
tvdbId?: number;
|
tvdbId?: number;
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv' | 'music' | 'book';
|
||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface TmdbTitleCardProps {
|
|||||||
id: number;
|
id: number;
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
tvdbId?: number;
|
tvdbId?: number;
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv' | 'music' | 'book';
|
||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
isAddedToWatchlist?: boolean;
|
isAddedToWatchlist?: boolean;
|
||||||
mutateParent?: () => void;
|
mutateParent?: () => void;
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ const UserProfile = () => {
|
|||||||
id={item.tmdbId}
|
id={item.tmdbId}
|
||||||
key={`watchlist-slider-item-${item.ratingKey}`}
|
key={`watchlist-slider-item-${item.ratingKey}`}
|
||||||
tmdbId={item.tmdbId}
|
tmdbId={item.tmdbId}
|
||||||
type={item.mediaType}
|
type={item.mediaType as 'movie' | 'tv'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
@@ -395,7 +395,7 @@ const UserProfile = () => {
|
|||||||
id={item.id}
|
id={item.id}
|
||||||
tmdbId={item.tmdbId}
|
tmdbId={item.tmdbId}
|
||||||
tvdbId={item.tvdbId}
|
tvdbId={item.tvdbId}
|
||||||
type={item.mediaType}
|
type={item.mediaType as 'movie' | 'tv'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user