Skip to content

Commit

Permalink
feat(series): add new series flow support
Browse files Browse the repository at this point in the history
- series and media items have same id
- bulk endpount is used to get episodes
  • Loading branch information
AntonLantukh committed Apr 13, 2023
1 parent 4b6af51 commit cffb723
Show file tree
Hide file tree
Showing 28 changed files with 332 additions and 257 deletions.
112 changes: 77 additions & 35 deletions docs/features/series.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
# Series

Series enables customers to bundle episodic content such as TV shows and learning courses or non-episodic content like sports leagues events. By organizing content into a series, viewers are guided through the content. Series have a predefined sequence of episodes and can be split in seasons.
Series enables customers to bundle episodic content such as TV shows and learning courses or non-episodic content like sports leagues events. By organizing content into a series, viewers are guided through the content. Series have a predefined sequence of episodes and can be split in seasons.

<img title="" src="../_images/series.jpg" alt="Series" width="580">

Series are tagged with `Series` in [shelves and libraries](shelves-and-libraries.md):

<img title="" src="../_images/series-in-library.jpg" alt="Series in library" width="581">

Series are defined through 'series playlist'. This is handled in the first piece of this article.
Series are defined through two approaches: 'Series playlist' (deprecated) and 'Series media item' (new native approach).

In the near future JW player will native series construct. This is handled in the second part of this article.
## (DEPRECATED) Series through playlist

## Series through playlist

Series are not a native construct in the JW Dashboard at this moment. So customers create a 'series playlist' and set the sequences and episodes using custom parameters.
Customers can create a 'Series playlist' and set the sequences and episodes using custom parameters.

### Creating series playlists in the dashboard

The [JW manual](https://support.jwplayer.com/articles/build-an-ott-apps-series-playlist) describes the following process to create a serie playlist.

### Series in libraries and shelves

[Shelves and libraries](shelves-and-libraries.md) load their data using the [GET playlist endpoint](https://developer.jwplayer.com/jwplayer/reference/get_v2-playlists-playlist-id). Some items in this playlis refer to series. These are identified using the `seriesId` , which links to the playlist that contains the episodes.
[Shelves and libraries](shelves-and-libraries.md) load their data using the [GET playlist endpoint](https://developer.jwplayer.com/jwplayer/reference/get_v2-playlists-playlist-id). Some items in this playlist refer to series. These are identified using the `seriesPlayListId` OR `seriesPlaylistId` OR `seriesId` custom param, which links to the playlist that contains the episodes.

```
GET playlist/o45EkQBf
Expand Down Expand Up @@ -50,7 +48,7 @@ GET playlist/o45EkQBf

### Series detail window

The series detail window loads the series playlist using the [GET playlist endpoint](https://developer.jwplayer.com/jwplayer/reference/get_v2-playlists-playlist-id). The episodelabel(e.g. `S1:E1`) is coming from `seasonNumber` and `episodeNumber`
The series detail window loads the series playlist using the [GET playlist endpoint](https://developer.jwplayer.com/jwplayer/reference/get_v2-playlists-playlist-id). The episode label (e.g. `S1:E1`) is coming from `seasonNumber` and `episodeNumber` custom params.

```
GET playlist/xdAqW8ya
Expand All @@ -59,7 +57,7 @@ GET playlist/xdAqW8ya
"description":"Aimed at beginners, this workshop helps you connect basic Blender functionality into a complete workflow and mindset for building characters.",
"kind":"DYNAMIC",
"feedid":"xdAqW8ya",
"playlist":[
"playlist": [
{
"title":"Blocking",
"mediaid":"zKT3MFut",
Expand Down Expand Up @@ -91,26 +89,25 @@ GET playlist/xdAqW8ya
}
```

## Native series
## (NEW) Native series

JW Player has native series management from the JW Dashboard:

- simplifies the series creation workflow
- automatically calculate the number of episodes and duration of series
- contains trailers and bonus content

This section describes how this will work.
This section describes how this will work.

### Creating native series in the dashboard

1. Customers define series
2. Customers create media items each of which represents a series
3. Customers add a `seriesId` custom param to created media items
4. Customers publish their series by putting media items into a playlist
1. Customers create media items from the Media Library page with "Series" content type.
2. Customers add episodes to the media item. Media item / series item is the same entity now. They both have the same id.
3. Customers publish their series by putting media items into a playlist

### Native series in shelves and libraries

[Shelves and libraries](shelves-and-libraries.md) load their data using the [GET playlist endpoint](https://developer.jwplayer.com/jwplayer/reference/get_v2-playlists-playlist-id). Some items in this playlis refer to series. These can be recognized with the `seriesId` custom param.
[Shelves and libraries](shelves-and-libraries.md) load their data using the [GET playlist endpoint](https://developer.jwplayer.com/jwplayer/reference/get_v2-playlists-playlist-id). Some items in this playlist refer to series. These can be recognized with the `contentType` custom param set to `series`.

```
GET playlist\<playlistid>
Expand All @@ -119,23 +116,23 @@ GET playlist\<playlistid>
"medaid":"dwEE1oBP",
"title":"Video Title",
"description":"Lorem ipsum",
"seriesId": "aSZZ1oBP",
"contentType": "series",
"images":[],
"sources":[],
"tracks":[]
}
]
```

We do not show episode number and season number for separate series episodes. If you click on such an item, you will see a simple movie page.
We can also show episode number and season number for separate series episodes in case they have `seasonNumber` and `episodeNumber` custom params.

### Native series detail window

The series detail window loads the series playlist using a GET Series endpoint:

```
GET /apps/series/{series_id}
{
{
"title": "A Series of Unfortunate Events",
"description": "The series follow’
"series_id": "12345678",
Expand Down Expand Up @@ -166,31 +163,76 @@ The series detail window loads the series playlist using a GET Series endpoint:
]}]
```

Notice that the episodes don't include metadata (title, description, image, etc. ). That needs be retrieved seperately. This can be done one-by-one using [GET Media](https://developer.jwplayer.com/jwplayer/reference/get_v2-media-media-id), but to do this more efficiently [watchlist playlist](https://developer.jwplayer.com/jwplayer/reference/get_apps-watchlists-playlist-id) can be used:
Notice that the episodes don't include metadata (title, description, image, etc. ). That needs be retrieved seperately. This can be done one-by-one using [GET Media](https://developer.jwplayer.com/jwplayer/reference/get_v2-media-media-id), but to do this more efficiently episodes endpoint can be used (`/apps/series/${seriesId}/episodes`).

```
GET playlist?mediaids=zxcvbnma,lkjhgfds
[{
"title": "Big Buck Bunny",
"description": "Lorem ipsum",
"playlist": [
GET /apps/series/${seriesId}/episodes
{
media: {
'0t21PUiy': {
description:
"Let's get started with the Art of Blocking! The purpose of blocking is to play around with ideas and quickly iterate over several versions of a model, in order to find the right direction. During blocking it's essential to keep things simple, both in terms of shapes and materials - and do not be afraid to throw things away!",
duration: 3408,
image: 'http://cdn.jwplayer.com/v2/media/0t21PUiy/poster.jpg?width=720',
images: [
{
"title": "Big Buck Bunny",
"mediaid": "dwEE1oBP",
"images": [],
"duration": 596,
"description": Lorem ipsum,
"tags": "movie,Comedy"
]
}
src: 'http://cdn.jwplayer.com/v2/media/0t21PUiy/poster.jpg?width=320',
type: 'image/jpeg',
width: 320,
},
{
src: 'http://cdn.jwplayer.com/v2/media/0t21PUiy/poster.jpg?width=480',
type: 'image/jpeg',
width: 480,
},
{
src: 'http://cdn.jwplayer.com/v2/media/0t21PUiy/poster.jpg?width=640',
type: 'image/jpeg',
width: 640,
},
{
src: 'http://cdn.jwplayer.com/v2/media/0t21PUiy/poster.jpg?width=720',
type: 'image/jpeg',
width: 720,
},
{
src: 'http://cdn.jwplayer.com/v2/media/0t21PUiy/poster.jpg?width=1280',
type: 'image/jpeg',
width: 1280,
},
{
src: 'http://cdn.jwplayer.com/v2/media/0t21PUiy/poster.jpg?width=1920',
type: 'image/jpeg',
width: 1920,
},
],
link: 'http://cdn.jwplayer.com/previews/0t21PUiy',
mediaid: '0t21PUiy',
pubdate: 1555599600,
tags: 'lesson',
title: 'Blocking',
original_mediaid: 'I3k8wgIs',
genre: 'Advanced',
rating: 'CC-BY',
backgroundImage: 'background',
contentType: 'Episode',
},
},
siteId: 'siteId',
page: 1,
page_length: 50,
total: 5,
};
```

This playlist type is developed for [user watchlists](user-watchlist.md) but will also work here. Make sure the watchlist is created.

## Native series and search

Customer are advised to exclude series episodes from the search playlists by using tags. Likewise customers should ensure the series title and description are part of the first episode.

## Coming soon

We will add full Favorites and Continue Watching support

```
```
6 changes: 3 additions & 3 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type CardProps = {
title: string;
duration: number;
image?: ImageData;
seriesId?: string;
isSeries?: boolean;
seasonNumber?: string;
episodeNumber?: string;
progress?: number;
Expand All @@ -38,11 +38,11 @@ function Card({
title,
duration,
image,
seriesId,
seasonNumber,
episodeNumber,
progress,
posterAspect = '16:9',
isSeries = false,
featured = false,
disabled = false,
loading = false,
Expand All @@ -66,7 +66,7 @@ function Card({
const renderTag = () => {
if (loading || disabled || !title) return null;

if (seriesId) {
if (isSeries) {
return <div className={styles.tag}>Series</div>;
} else if (episodeNumber) {
return <div className={styles.tag}>{formatSeriesMetaString(seasonNumber, episodeNumber)}</div>;
Expand Down
7 changes: 5 additions & 2 deletions src/components/CardGrid/CardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { AccessModel } from '#types/Config';
import type { Playlist, PlaylistItem } from '#types/playlist';
import { parseAspectRatio, parseTilesDelta } from '#src/utils/collection';
import InfiniteScrollLoader from '#components/InfiniteScrollLoader/InfiniteScrollLoader';
import { isSeries } from '#src/utils/media';

const INITIAL_ROW_COUNT = 6;
const LOAD_ROWS_COUNT = 4;
Expand Down Expand Up @@ -61,7 +62,9 @@ function CardGrid({
}, [playlist.feedid]);

const renderTile = (playlistItem: PlaylistItem) => {
const { mediaid, title, duration, seriesId, episodeNumber, seasonNumber, shelfImage } = playlistItem;
const { mediaid, title, duration, episodeNumber, seasonNumber, shelfImage } = playlistItem;

const isSeriesItem = isSeries(playlistItem);

return (
<div className={styles.cell} key={mediaid} role="row">
Expand All @@ -71,7 +74,7 @@ function CardGrid({
duration={duration}
image={shelfImage}
progress={watchHistory ? watchHistory[mediaid] : undefined}
seriesId={seriesId}
isSeries={isSeriesItem}
episodeNumber={episodeNumber}
seasonNumber={seasonNumber}
onClick={() => onCardClick(playlistItem, playlistItem.feedid)}
Expand Down
43 changes: 24 additions & 19 deletions src/components/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { isLocked } from '#src/utils/entitlements';
import TileDock from '#components/TileDock/TileDock';
import Card, { type PosterAspectRatio } from '#components/Card/Card';
import type { Playlist, PlaylistItem } from '#types/playlist';
import { isSeries } from '#src/utils/media';

export const tileBreakpoints: Breakpoints = {
[Breakpoint.xs]: 1,
Expand Down Expand Up @@ -70,25 +71,29 @@ const Shelf = ({
const tilesToShow: number = (featured ? featuredTileBreakpoints[breakpoint] : tileBreakpoints[breakpoint]) + visibleTilesDelta;

const renderTile = useCallback(
(item: PlaylistItem, isInView: boolean) => (
<Card
key={item.mediaid}
title={item.title}
duration={item.duration}
progress={watchHistory ? watchHistory[item.mediaid] : undefined}
image={item.shelfImage}
seriesId={item.seriesId}
seasonNumber={item.seasonNumber}
episodeNumber={item.episodeNumber}
onClick={isInView ? () => onCardClick(item, playlist.feedid, type) : undefined}
onHover={typeof onCardHover === 'function' ? () => onCardHover(item) : undefined}
featured={featured}
disabled={!isInView}
loading={loading}
isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, item)}
posterAspect={posterAspect}
/>
),
(item: PlaylistItem, isInView: boolean) => {
const isSeriesItem = isSeries(item);

return (
<Card
key={item.mediaid}
title={item.title}
duration={item.duration}
progress={watchHistory ? watchHistory[item.mediaid] : undefined}
image={item.shelfImage}
isSeries={isSeriesItem}
seasonNumber={item.seasonNumber}
episodeNumber={item.episodeNumber}
onClick={isInView ? () => onCardClick(item, playlist.feedid, type) : undefined}
onHover={typeof onCardHover === 'function' ? () => onCardHover(item) : undefined}
featured={featured}
disabled={!isInView}
loading={loading}
isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, item)}
posterAspect={posterAspect}
/>
);
},
[watchHistory, onCardHover, featured, loading, accessModel, isLoggedIn, hasSubscription, posterAspect, onCardClick, playlist.feedid, type],
);

Expand Down
7 changes: 5 additions & 2 deletions src/components/VideoList/VideoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import styles from './VideoList.module.scss';
import VideoListItem from '#components/VideoListItem/VideoListItem';
import { isLocked } from '#src/utils/entitlements';
import { testId } from '#src/utils/common';
import { isSeries } from '#src/utils/media';
import type { AccessModel } from '#types/Config';
import type { Playlist, PlaylistItem } from '#types/playlist';

Expand Down Expand Up @@ -43,7 +44,9 @@ function VideoList({
{!!header && header}
{playlist &&
playlist.playlist.map((playlistItem: PlaylistItem) => {
const { mediaid, title, duration, seriesId, episodeNumber, seasonNumber, shelfImage } = playlistItem;
const { mediaid, title, duration, episodeNumber, seasonNumber, shelfImage } = playlistItem;

const isSeriesItem = isSeries(playlistItem);

return (
<VideoListItem
Expand All @@ -52,7 +55,7 @@ function VideoList({
duration={duration}
image={shelfImage}
progress={watchHistory ? watchHistory[mediaid] : undefined}
seriesId={seriesId}
isSeries={isSeriesItem}
episodeNumber={episodeNumber}
seasonNumber={seasonNumber}
onClick={() => onListItemClick && onListItemClick(playlistItem, playlistItem.feedid)}
Expand Down
10 changes: 5 additions & 5 deletions src/components/VideoListItem/VideoListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type VideoListItemProps = {
title: string;
duration: number;
image?: ImageData;
seriesId?: string;
isSeries?: boolean;
seasonNumber?: string;
episodeNumber?: string;
progress?: number;
Expand All @@ -31,15 +31,15 @@ function VideoListItem({
onHover,
title,
duration,
seriesId,
seasonNumber,
episodeNumber,
progress,
activeLabel,
image,
isSeries = false,
loading = false,
isActive = false,
activeLabel,
isLocked = true,
image,
}: VideoListItemProps): JSX.Element {
const { t } = useTranslation('common');
const [imageLoaded, setImageLoaded] = useState(false);
Expand All @@ -50,7 +50,7 @@ function VideoListItem({
const renderTagLabel = () => {
if (loading || !title) return null;

if (seriesId) {
if (isSeries) {
return t('series');
} else if (seasonNumber && episodeNumber) {
return formatSeriesMetaString(seasonNumber, episodeNumber);
Expand Down
Loading

0 comments on commit cffb723

Please sign in to comment.