Skip to content

Commit

Permalink
feat(series): review fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonLantukh committed May 17, 2023
1 parent 8975649 commit 5982c9a
Show file tree
Hide file tree
Showing 25 changed files with 388 additions and 342 deletions.
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ The easiest way to maintain configuration files is to use the 'Apps' section in
Which app config file the application uses is determined by the [ini file](initialization-file.md).

You can specify the default that the application starts with and also which config, if any, it will allow to be set using the [`app-config=<config source>` query param](#switching-between-app-configs).
The location is usually specified by the 8-character ID (i.e. `gnnuzabk`) of the App Config from your JWP account, in which case the file will be loaded from the JW Player App Config delivery endpoint (i.e. `https://cdn.jwplayer.com/apps/configs/gnnuzabk.json`).
The location is usually specified by the 8-character ID (i.e. `ehon8mco`) of the App Config from your JWP account, in which case the file will be loaded from the JW Player App Config delivery endpoint (i.e. `https://cdn.jwplayer.com/apps/configs/ehon8mco.json`).
You may also specify a relative or absolute URL.

### Switching between app configs

As mentioned above, if you have 1 or more additional allowed sources (see additionalAllowedConfigSources in [`initialization-file`](initialization-file.md)), you can switch between them by adding `app-config=<config source>` as a query parameter in the web app URL in your browser (i.e. `https://<your domain>/?app-config=gnnuzabk`).
As mentioned above, if you have 1 or more additional allowed sources (see additionalAllowedConfigSources in [`initialization-file`](initialization-file.md)), you can switch between them by adding `app-config=<config source>` as a query parameter in the web app URL in your browser (i.e. `https://<your domain>/?app-config=ehon8mco`).

The parameter is automatically evaluated, loaded, and stored in browser session storage and should remain part of the url as the user navigates around the site.

Expand Down
5 changes: 0 additions & 5 deletions docs/features/user-watchlists.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,8 @@ To ensure a **cross-device experience**, we standardize on the following datafor

## Watchlist playlist

<<<<<<< HEAD
The media metadata for the stored media ids an be retrieved through a [watchlist playlist](https://developer.jwplayer.com/jwplayer/docs/creating-and-using-a-watchlist-playlist):
=======
The media metadata for the stored media ids can be retrieved through a [watchlist playlist](https://developer.jwplayer.com/jwplayer/docs/creating-and-using-a-watchlist-playlist):

> > > > > > > develop
```
curl 'https://cdn.jwplayer.com/apps/watchlists/<watchlist-id>?media_ids=<media-ids-comma-seperated>'
```
Expand Down
2 changes: 1 addition & 1 deletion ini/templates/.webapp.dev.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
; This is a basic Blender demo
defaultConfigSource = gnnuzabk
defaultConfigSource = ehon8mco
; When developing, switching between configs is useful for test and debug
UNSAFE_allowAnyConfigSource = true
2 changes: 1 addition & 1 deletion ini/templates/.webapp.prod.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; This is the app config that your application will load at startup
defaultConfigSource = ; Enter your 8 digit config ID (i.e. gnnuzabk) here (case sensitive)
defaultConfigSource = ; Enter your 8 digit config ID (i.e. ehon8mco) here (case sensitive)

; This key ensures proper integration with the JW Platform.
; Please make sure to set it here unless you are setting it at build time with the APP_PLAYER_LICENSE_KEY env variable.
Expand Down
4 changes: 2 additions & 2 deletions ini/templates/.webapp.test.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defaultConfigSource = gnnuzabk
defaultConfigSource = ehon8mco
additionalAllowedConfigSources[] = kujzeu1b
additionalAllowedConfigSources[] = ata6ucb8
additionalAllowedConfigSources[] = 7xlh4b33
additionalAllowedConfigSources[] = nvqkufhy
additionalAllowedConfigSources[] = 7weyqrua
additionalAllowedConfigSources[] = ozylzc5m
Expand Down
2 changes: 1 addition & 1 deletion src/components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const Filter: FC<Props> = ({ name, value, defaultLabel, options, setValue, value
{options.map((option) => (
<Button label={`${valuePrefix}${option}`} onClick={() => setValue(option)} key={option} active={value === option} role="option" />
))}
<Button label={defaultLabel} onClick={() => setValue('')} active={value === ''} key={defaultLabel} role="option" />
<Button label={defaultLabel} onClick={() => setValue('all')} active={value === 'all'} key={defaultLabel} role="option" />
</div>
) : (
<div className={styles.filterDropDown}>
Expand Down
39 changes: 30 additions & 9 deletions src/containers/SeriesRedirect/SeriesRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@ import { useTranslation } from 'react-i18next';
import { useSeriesData } from '#src/hooks/series/useSeriesData';
import useQueryParam from '#src/hooks/useQueryParam';
import { episodeURL } from '#src/utils/formatting';
import { getEpisodeToRedirect } from '#src/utils/series';
import Loading from '#src/pages/Loading/Loading';
import ErrorPage from '#components/ErrorPage/ErrorPage';
import { useSeriesEpisodes } from '#src/hooks/series/useEpisodes';
import { useEpisode } from '#src/hooks/series/useEpisode';
import { useEpisodes } from '#src/hooks/series/useEpisodes';
import useMedia from '#src/hooks/useMedia';
import type { Playlist, PlaylistItem } from '#types/playlist';
import type { EpisodesWithPagination } from '#types/series';

/** Get episode to redirect to MediaSeriesEpisodePage */
const getEpisodeToRedirect = (
episodeId: string | undefined,
seriesPlaylist: Playlist,
episodeData: PlaylistItem | undefined,
episodesData: EpisodesWithPagination[] | undefined,
isNewSeriesFlow: boolean,
) => {
if (isNewSeriesFlow) {
// For the new flow we return either a selected episode (Continue Watching) or just first available one
return episodeData || episodesData?.[0]?.episodes?.[0];
}

// For the old approach we do the same thing, the only thing here that our playlist already have all the episodes inside
if (!episodeId) {
return seriesPlaylist.playlist[0];
}

return seriesPlaylist.playlist.find(({ mediaid }) => mediaid === episodeId);
};

type Props = {
seriesId: string;
Expand All @@ -29,12 +51,11 @@ const SeriesRedirect = ({ seriesId, episodeId, mediaId }: Props) => {
const { t } = useTranslation('video');

const { isLoading, isPlaylistError, data } = useSeriesData(seriesId, mediaId);
const { newSeries, playlist } = data || {};
const { series } = newSeries || {};
const { series, playlist: seriesPlaylist } = data || {};

const { data: episodeData, isLoading: isEpisodeLoading } = useEpisode(episodeId, series);
const { data: episode, isLoading: isEpisodeLoading } = useMedia(episodeId || '');
// Only request list of episodes if we have no episode provided for the new flow
const { data: episodesData, isLoading: isEpisodesLoading } = useSeriesEpisodes(mediaId, !!series && !episodeId);
const { data: episodesData, isLoading: isEpisodesLoading } = useEpisodes(mediaId, '0', { enabled: !!series && !episodeId });

const play = useQueryParam('play') === '1';
const feedId = useQueryParam('r');
Expand All @@ -43,9 +64,9 @@ const SeriesRedirect = ({ seriesId, episodeId, mediaId }: Props) => {
return <Loading />;
}

const toEpisode = getEpisodeToRedirect(episodeId, playlist, episodeData, episodesData?.pages, !!series);
const toEpisode = getEpisodeToRedirect(episodeId, seriesPlaylist, episode, episodesData, !!series);

if (isPlaylistError || !playlist || !toEpisode) {
if (isPlaylistError || !seriesPlaylist || !toEpisode) {
return <ErrorPage title={t('series_error')} />;
}

Expand Down
37 changes: 0 additions & 37 deletions src/hooks/series/useEpisode.ts

This file was deleted.

39 changes: 39 additions & 0 deletions src/hooks/series/useEpisodeMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { UseQueryResult, useQuery } from 'react-query';

import { getSeriesByMediaIds } from '#src/services/api.service';
import type { ApiError } from '#src/utils/api';
import type { Series, EpisodeMetadata } from '#types/series';
import type { PlaylistItem } from '#types/playlist';

// Get `episodeNumber` and `seasonNumber` for the new series flow
export const useEpisodeMetadata = (
episode: PlaylistItem,
series: Series | undefined,
options: { enabled: boolean },
): { isLoading: boolean; data: EpisodeMetadata | undefined } => {
const oldFlowMetadata = { episodeNumber: episode?.episodeNumber || '0', seasonNumber: episode?.seasonNumber || '0' };

const { isLoading, data }: UseQueryResult<EpisodeMetadata | undefined, ApiError | null> = useQuery(
['episodeId', episode.mediaid],
async () => {
if (!episode.mediaid) {
throw Error('No episode id provided');
}

const seriesDictionary = await getSeriesByMediaIds([episode.mediaid]);
// Get an item details of the associated series (we need its episode and season)
const { season_number, episode_number } = (seriesDictionary?.[episode.mediaid] || []).find((el) => el.series_id === series?.series_id) || {};
// Add seriesId to work with watch history
return { episodeNumber: String(episode_number || 0), seasonNumber: String(season_number || 0), seriesId: series?.series_id };
},
{
// Only enable this query when having new series flow
enabled: options.enabled,
},
);

return {
isLoading,
data: isLoading || !options.enabled ? data : series ? data : oldFlowMetadata,
};
};
113 changes: 41 additions & 72 deletions src/hooks/series/useEpisodes.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,64 @@
import { UseInfiniteQueryResult, useInfiniteQuery } from 'react-query';
import { useInfiniteQuery } from 'react-query';

import { getEpisodes, getSeasonWithEpisodes } from '#src/services/api.service';
import type { EpisodesWithPagination } from '#types/series';
import type { ApiError } from '#src/utils/api';
import type { Pagination } from '#types/pagination';

// 1 hour
const CACHE_TIME = 60 * 1000 * 60;

// Get episodes from a selected series using pagination
export const useSeriesEpisodes = (seriesId: string | undefined, isNewSeriesFlow: boolean): UseInfiniteQueryResult<EpisodesWithPagination, ApiError | null> => {
return useInfiniteQuery(
[seriesId, 'episodes'],
async ({ pageParam = 0 }) => {
const episodes = await getEpisodes(seriesId || '', pageParam);

return episodes;
},
{
getNextPageParam: (lastPage) => {
const { page, page_limit, total } = lastPage.pagination;
const getNextPageParam = (pagination: Pagination) => {
const { page, page_limit, total } = pagination;

// In case there are no more episodes in a season to fetch
if (page_limit * page >= total) {
return undefined;
}
// In case there are no more episodes in a season to fetch
if (page_limit * page >= total) {
return undefined;
}

return page;
},
enabled: isNewSeriesFlow,
// 1 hour
staleTime: CACHE_TIME,
cacheTime: CACHE_TIME,
},
);
return page;
};

// Get episodes from a selected season using pagination
const useSeasonEpisodes = (
isNewSeriesFlow: boolean,
export const useEpisodes = (
seriesId: string | undefined,
seasonNumber: number,
): UseInfiniteQueryResult<EpisodesWithPagination, ApiError | null> => {
return useInfiniteQuery(
[seriesId, 'season', seasonNumber],
seasonNumber: string | undefined,
options: { enabled: boolean },
): {
data: EpisodesWithPagination[];
hasNextPage: boolean;
fetchNextPage: (params?: { pageParam?: number }) => void;
isLoading: boolean;
} => {
const {
data,
fetchNextPage,
isLoading,
hasNextPage = false,
} = useInfiniteQuery(
[seriesId, seasonNumber],
async ({ pageParam = 0 }) => {
const season = await getSeasonWithEpisodes(seriesId || '', seasonNumber, pageParam);
if (Number(seasonNumber)) {
// Get episodes from a selected season using pagination
const season = await getSeasonWithEpisodes(seriesId || '', Number(seasonNumber), pageParam);

return { pagination: season.pagination, episodes: season.episodes };
return { pagination: season.pagination, episodes: season.episodes };
} else {
// Get episodes from a selected series using pagination
const data = await getEpisodes(seriesId || '', pageParam);
return data;
}
},
{
getNextPageParam: (lastPage) => {
const { page, page_limit, total } = lastPage.pagination;

// In case there are no more episodes in a season to fetch
if (page_limit * page >= total) {
return undefined;
}

return page;
},
enabled: isNewSeriesFlow,
// 1 hour
getNextPageParam: (lastPage) => getNextPageParam(lastPage?.pagination),
enabled: options.enabled,
staleTime: CACHE_TIME,
cacheTime: CACHE_TIME,
},
);
};

export const useEpisodes = (
seriesId: string | undefined,
isNewSeriesFlow: boolean,
filter: string | undefined,
): { data: EpisodesWithPagination[]; hasNextPage: boolean; fetchNextPage: () => void } => {
const {
data: episodesData,
fetchNextPage: fetchNextSeriesEpisodes,
hasNextPage: hasNextEpisodesPage = false,
} = useSeriesEpisodes(seriesId, isNewSeriesFlow && !filter);

const {
data: seasonEpisodesData,
fetchNextPage: fetchNextSeasonEpisodes,
hasNextPage: hasNextSeasonEpisodesPage = false,
} = useSeasonEpisodes(isNewSeriesFlow && !!filter, seriesId, Number(filter));

if (!filter) {
return { data: episodesData?.pages || [], fetchNextPage: fetchNextSeriesEpisodes, hasNextPage: hasNextEpisodesPage };
}

return {
data: seasonEpisodesData?.pages || [],
fetchNextPage: fetchNextSeasonEpisodes,
hasNextPage: hasNextSeasonEpisodesPage,
data: data?.pages || [],
isLoading,
fetchNextPage,
hasNextPage,
};
};
28 changes: 28 additions & 0 deletions src/hooks/series/useNextEpisode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';

import type { Playlist, PlaylistItem } from '#types/playlist';
import type { EpisodeMetadata, Series } from '#types/series';
import { getNextItem } from '#src/utils/series';

export const useNextEpisode = ({
episode,
seriesPlaylist,
series,
episodeMetadata,
}: {
episode: PlaylistItem | undefined;
seriesPlaylist: Playlist;
series: Series | undefined;
episodeMetadata: EpisodeMetadata | undefined;
}) => {
const [nextItem, setNextItem] = useState<PlaylistItem | undefined>(undefined);
useEffect(() => {
async function fetchData() {
const item = await getNextItem(episode, seriesPlaylist, series, episodeMetadata);
setNextItem(item);
}
fetchData();
}, [episode, seriesPlaylist, series, episodeMetadata]);

return nextItem;
};
Loading

0 comments on commit 5982c9a

Please sign in to comment.