mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Add livetv view
This commit is contained in:
parent
c37783479e
commit
e41436552e
44 changed files with 1396 additions and 749 deletions
|
@ -1,5 +1,5 @@
|
|||
import { AxiosRequestConfig } from 'axios';
|
||||
import type { BaseItemDto, ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto, ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest, TimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||
|
@ -15,6 +15,7 @@ import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
|
|||
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import datetime from 'scripts/datetime';
|
||||
|
@ -22,9 +23,10 @@ import globalize from 'scripts/globalize';
|
|||
|
||||
import { JellyfinApiContext, useApi } from './useApi';
|
||||
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
|
||||
import { Sections, SectionsViewType } from 'types/suggestionsSections';
|
||||
import { getProgramSections, getSuggestionSections } from 'utils/sections';
|
||||
import { LibraryViewSettings, ParentId } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { Section, SectionApiMethod, SectionType } from 'types/sections';
|
||||
|
||||
const fetchGetItem = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
|
@ -48,10 +50,11 @@ const fetchGetItem = async (
|
|||
|
||||
export const useGetItem = (parentId: ParentId) => {
|
||||
const currentApi = useApi();
|
||||
const isLivetv = parentId === 'livetv';
|
||||
return useQuery({
|
||||
queryKey: ['Item', parentId],
|
||||
queryFn: ({ signal }) => fetchGetItem(currentApi, parentId, { signal }),
|
||||
enabled: !!parentId
|
||||
enabled: !!parentId && !isLivetv
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -116,142 +119,12 @@ const fetchGetMovieRecommendations = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const useGetMovieRecommendations = (parentId: ParentId) => {
|
||||
export const useGetMovieRecommendations = (isMovieRecommendationEnabled: boolean, parentId: ParentId) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['MovieRecommendations', parentId],
|
||||
queryKey: ['MovieRecommendations', isMovieRecommendationEnabled, parentId],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetMovieRecommendations(currentApi, parentId, { signal }),
|
||||
enabled: !!parentId
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGetItemsBySuggestionsType = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
sections: Sections,
|
||||
parentId: ParentId,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const { api, user } = currentApi;
|
||||
if (api && user?.Id) {
|
||||
let response;
|
||||
switch (sections.viewType) {
|
||||
case SectionsViewType.NextUp: {
|
||||
response = (
|
||||
await getTvShowsApi(api).getNextUp(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 25,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop,
|
||||
ImageType.Thumb
|
||||
],
|
||||
enableTotalRecordCount: false,
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionsViewType.ResumeItems: {
|
||||
response = (
|
||||
await getItemsApi(api).getResumeItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Thumb],
|
||||
enableTotalRecordCount: false,
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionsViewType.LatestMedia: {
|
||||
response = (
|
||||
await getUserLibraryApi(api).getLatestMedia(
|
||||
{
|
||||
userId: user.Id,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ ImageType.Primary, ImageType.Thumb ],
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
response = (
|
||||
await getItemsApi(api).getItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
recursive: true,
|
||||
fields: [ItemFields.PrimaryImageAspectRatio],
|
||||
filters: [ItemFilter.IsPlayed],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop,
|
||||
ImageType.Thumb
|
||||
],
|
||||
limit: 25,
|
||||
enableTotalRecordCount: false,
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetItemsBySectionType = (
|
||||
sections: Sections,
|
||||
parentId: ParentId
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['ItemsBySuggestionsType', sections.view],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetItemsBySuggestionsType(
|
||||
currentApi,
|
||||
sections,
|
||||
parentId,
|
||||
{ signal }
|
||||
),
|
||||
enabled: !!sections.view
|
||||
isMovieRecommendationEnabled ? fetchGetMovieRecommendations(currentApi, parentId, { signal }) : []
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -314,17 +187,18 @@ const fetchGetStudios = async (
|
|||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
return response.data.Items;
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => {
|
||||
const currentApi = useApi();
|
||||
const isLivetv = parentId === 'livetv';
|
||||
return useQuery({
|
||||
queryKey: ['Studios', parentId, itemType],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetStudios(currentApi, parentId, itemType, { signal }),
|
||||
enabled: !!parentId
|
||||
enabled: !!parentId && !isLivetv
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -355,13 +229,14 @@ export const useGetQueryFiltersLegacy = (
|
|||
itemType: BaseItemKind[]
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
const isLivetv = parentId === 'livetv';
|
||||
return useQuery({
|
||||
queryKey: ['QueryFiltersLegacy', parentId, itemType],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetQueryFiltersLegacy(currentApi, parentId, itemType, {
|
||||
signal
|
||||
}),
|
||||
enabled: !!parentId
|
||||
enabled: !!parentId && !isLivetv
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -434,6 +309,34 @@ const fetchGetItemsViewByType = async (
|
|||
}
|
||||
);
|
||||
break;
|
||||
case LibraryTab.Channels: {
|
||||
response = await getLiveTvApi(api).getLiveTvChannels(
|
||||
{
|
||||
userId: user.Id,
|
||||
fields: [ItemFields.PrimaryImageAspectRatio],
|
||||
startIndex: libraryViewSettings.StartIndex,
|
||||
isFavorite: libraryViewSettings.Filters?.Status?.includes(ItemFilter.IsFavorite) ?
|
||||
true :
|
||||
undefined,
|
||||
enableImageTypes: [ImageType.Primary]
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case LibraryTab.SeriesTimers:
|
||||
response = await getLiveTvApi(api).getSeriesTimers(
|
||||
{
|
||||
sortBy: 'SortName',
|
||||
sortOrder: SortOrder.Ascending
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
break;
|
||||
default: {
|
||||
response = await getItemsApi(api).getItems(
|
||||
{
|
||||
|
@ -505,8 +408,10 @@ export const useGetItemsViewByType = (
|
|||
LibraryTab.Songs,
|
||||
LibraryTab.Books,
|
||||
LibraryTab.Photos,
|
||||
LibraryTab.Videos
|
||||
].includes(viewType) && !!parentId
|
||||
LibraryTab.Videos,
|
||||
LibraryTab.Channels,
|
||||
LibraryTab.SeriesTimers
|
||||
].includes(viewType)
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -690,3 +595,336 @@ export const useTogglePlayedMutation = () => {
|
|||
fetchUpdatePlayedState(currentApi, itemId, playedState )
|
||||
});
|
||||
};
|
||||
|
||||
export type GroupsTimers = {
|
||||
name: string;
|
||||
timerInfo: TimerInfoDto[];
|
||||
};
|
||||
|
||||
function groupsTimers(timers: TimerInfoDto[], indexByDate?: boolean) {
|
||||
const items = timers.map(function (t) {
|
||||
t.Type = 'Timer';
|
||||
return t;
|
||||
});
|
||||
const groups: GroupsTimers[] = [];
|
||||
let currentGroupName = '';
|
||||
let currentGroup: TimerInfoDto[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
let dateText = '';
|
||||
|
||||
if (indexByDate !== false && item.StartDate) {
|
||||
try {
|
||||
const premiereDate = datetime.parseISO8601Date(item.StartDate, true);
|
||||
dateText = datetime.toLocaleDateString(premiereDate, {
|
||||
weekday: 'long',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('error parsing premiereDate:' + item.StartDate + '; error: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
if (dateText != currentGroupName) {
|
||||
if (currentGroup.length) {
|
||||
groups.push({
|
||||
name: currentGroupName,
|
||||
timerInfo: currentGroup
|
||||
});
|
||||
}
|
||||
|
||||
currentGroupName = dateText;
|
||||
currentGroup = [item];
|
||||
} else {
|
||||
currentGroup.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentGroup.length) {
|
||||
groups.push({
|
||||
name: currentGroupName,
|
||||
timerInfo: currentGroup
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
const fetchGetTimers = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
indexByDate?: boolean,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const { api } = currentApi;
|
||||
if (api) {
|
||||
const response = await getLiveTvApi(api).getTimers(
|
||||
{
|
||||
isActive: false,
|
||||
isScheduled: true
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
|
||||
const timers = response.data.Items ?? [];
|
||||
|
||||
return groupsTimers(timers, indexByDate);
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetTimers = (isUpcomingRecordingsEnabled: boolean, indexByDate?: boolean) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['Timers', isUpcomingRecordingsEnabled, indexByDate],
|
||||
queryFn: ({ signal }) =>
|
||||
isUpcomingRecordingsEnabled ? fetchGetTimers(currentApi, indexByDate, { signal }) : []
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGetSectionItems = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
parentId: ParentId,
|
||||
section: Section,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const { api, user } = currentApi;
|
||||
if (api && user?.Id) {
|
||||
let response;
|
||||
switch (section.apiMethod) {
|
||||
case SectionApiMethod.RecommendedPrograms: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getRecommendedPrograms(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 12,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop],
|
||||
enableTotalRecordCount: false,
|
||||
fields: [
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.LiveTvPrograms: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getLiveTvPrograms(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 12,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop],
|
||||
enableTotalRecordCount: false,
|
||||
fields: [
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.PrimaryImageAspectRatio
|
||||
],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.Recordings: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getRecordings(
|
||||
{
|
||||
userId: user.Id,
|
||||
enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop],
|
||||
enableTotalRecordCount: false,
|
||||
fields: [
|
||||
ItemFields.CanDelete,
|
||||
ItemFields.PrimaryImageAspectRatio
|
||||
],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.RecordingFolders: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getRecordingFolders(
|
||||
{
|
||||
userId: user.Id
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.NextUp: {
|
||||
response = (
|
||||
await getTvShowsApi(api).getNextUp(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 25,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop,
|
||||
ImageType.Thumb
|
||||
],
|
||||
enableTotalRecordCount: false,
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.ResumeItems: {
|
||||
response = (
|
||||
await getItemsApi(api).getResumeItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Thumb],
|
||||
enableTotalRecordCount: false,
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.LatestMedia: {
|
||||
response = (
|
||||
await getUserLibraryApi(api).getLatestMedia(
|
||||
{
|
||||
userId: user.Id,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
response = (
|
||||
await getItemsApi(api).getItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
recursive: true,
|
||||
limit: 25,
|
||||
enableTotalRecordCount: false,
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
type SectionWithItems = {
|
||||
section: Section;
|
||||
items: BaseItemDto[];
|
||||
};
|
||||
|
||||
const getSectionsWithItems = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
parentId: ParentId,
|
||||
sections: Section[],
|
||||
sectionType?: SectionType[],
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
if (sectionType) {
|
||||
sections = sections.filter((section) => sectionType.includes(section.type));
|
||||
}
|
||||
|
||||
const updatedSectionWithItems: SectionWithItems[] = [];
|
||||
|
||||
for (const section of sections) {
|
||||
try {
|
||||
const items = await fetchGetSectionItems(
|
||||
currentApi, parentId, section, options
|
||||
);
|
||||
|
||||
if (items && items.length > 0) {
|
||||
updatedSectionWithItems.push({
|
||||
section,
|
||||
items
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error occurred for section ${section.type}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedSectionWithItems;
|
||||
};
|
||||
|
||||
export const useGetSuggestionSectionsWithItems = (
|
||||
parentId: ParentId,
|
||||
suggestionSectionType: SectionType[]
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
const sections = getSuggestionSections();
|
||||
return useQuery({
|
||||
queryKey: ['SuggestionSectionWithItems', suggestionSectionType],
|
||||
queryFn: ({ signal }) =>
|
||||
getSectionsWithItems(currentApi, parentId, sections, suggestionSectionType, { signal }),
|
||||
enabled: !!parentId
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProgramsSectionsWithItems = (
|
||||
parentId: ParentId,
|
||||
programSectionType: SectionType[]
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
const sections = getProgramSections();
|
||||
return useQuery({
|
||||
queryKey: ['ProgramSectionWithItems', programSectionType],
|
||||
queryFn: ({ signal }) =>
|
||||
getSectionsWithItems(currentApi, parentId, sections, programSectionType, { signal })
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue