-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathWatchHistoryService.ts
More file actions
181 lines (148 loc) · 6.87 KB
/
WatchHistoryService.ts
File metadata and controls
181 lines (148 loc) · 6.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import { inject, injectable } from 'inversify';
import { number, object, string } from 'yup';
import type { PlaylistItem } from '../../types/playlist';
import type { SerializedWatchHistoryItem, WatchHistoryItem } from '../../types/watchHistory';
import type { Customer } from '../../types/account';
import { getNamedModule } from '../modules/container';
import { INTEGRATION_TYPE } from '../modules/types';
import { logDev } from '../utils/common';
import { MAX_WATCHLIST_ITEMS_COUNT } from '../constants';
import ApiService from './ApiService';
import StorageService from './StorageService';
import AccountService from './integrations/AccountService';
const schema = object().shape({
mediaid: string(),
progress: number(),
});
@injectable()
export default class WatchHistoryService {
private PERSIST_KEY_WATCH_HISTORY = 'history';
private readonly apiService;
private readonly storageService;
private readonly accountService;
constructor(@inject(INTEGRATION_TYPE) integrationType: string, apiService: ApiService, storageService: StorageService) {
this.apiService = apiService;
this.storageService = storageService;
this.accountService = getNamedModule(AccountService, integrationType);
}
// Retrieve watch history media items info using a provided watch list
private getWatchHistoryItems = async (continueWatchingList: string, ids: string[]): Promise<Record<string, PlaylistItem>> => {
const watchHistoryItems = await this.apiService.getMediaByWatchlist(continueWatchingList, ids);
const watchHistoryItemsDict = Object.fromEntries((watchHistoryItems || []).map((item) => [item.mediaid, item]));
return watchHistoryItemsDict;
};
// We store separate episodes in the watch history and to show series card in the Continue Watching shelf we need to get their parent media items
private getWatchHistorySeriesItems = async (continueWatchingList: string, ids: string[]): Promise<Record<string, PlaylistItem | undefined>> => {
const mediaWithSeries = await this.apiService.getSeriesByMediaIds(ids);
const seriesIds = Object.keys(mediaWithSeries || {})
.map((key) => mediaWithSeries?.[key]?.[0]?.series_id)
.filter(Boolean) as string[];
const uniqueSerieIds = [...new Set(seriesIds)];
const seriesItems = await this.apiService.getMediaByWatchlist(continueWatchingList, uniqueSerieIds);
const seriesItemsDict = Object.keys(mediaWithSeries || {}).reduce((acc, key) => {
const seriesItemId = mediaWithSeries?.[key]?.[0]?.series_id;
if (seriesItemId) {
acc[key] = seriesItems?.find((el) => el.mediaid === seriesItemId);
}
return acc;
}, {} as Record<string, PlaylistItem | undefined>);
return seriesItemsDict;
};
private validateWatchHistory(history: unknown) {
if (Array.isArray(history)) {
const validatedHistory = history.filter((item) => {
try {
return schema.validateSync(item);
} catch (error: unknown) {
logDev('Failed to validated watch history item', error);
return false;
}
});
return validatedHistory as SerializedWatchHistoryItem[];
}
return [];
}
private async getWatchHistoryFromAccount(user: Customer) {
const history = await this.accountService.getWatchHistory({ user });
return this.validateWatchHistory(history);
}
private async getWatchHistoryFromStorage() {
const history = await this.storageService.getItem(this.PERSIST_KEY_WATCH_HISTORY, true);
return this.validateWatchHistory(history);
}
getWatchHistory = async (user: Customer | null, continueWatchingList: string) => {
const savedItems = user ? await this.getWatchHistoryFromAccount(user) : await this.getWatchHistoryFromStorage();
// When item is an episode of the new flow -> show the card as a series one, but keep episode to redirect in a right way
const ids = savedItems.map(({ mediaid }) => mediaid);
if (!ids.length) {
return [];
}
try {
const watchHistoryItems = await this.getWatchHistoryItems(continueWatchingList, ids);
const seriesItems = await this.getWatchHistorySeriesItems(continueWatchingList, ids);
return savedItems
.map((item) => {
const parentSeries = seriesItems?.[item.mediaid];
const historyItem = watchHistoryItems[item.mediaid];
if (historyItem) {
return this.createWatchHistoryItem(parentSeries || historyItem, item.mediaid, parentSeries?.mediaid, item.progress);
}
})
.filter((item): item is WatchHistoryItem => Boolean(item));
} catch (error: unknown) {
logDev('Failed to get watch history items', error);
}
return [];
};
serializeWatchHistory = (watchHistory: WatchHistoryItem[]): SerializedWatchHistoryItem[] =>
watchHistory.map(({ mediaid, progress }) => ({
mediaid,
progress,
}));
persistWatchHistory = async (watchHistory: WatchHistoryItem[], user: Customer | null) => {
if (user) {
await this.accountService?.updateWatchHistory({
history: this.serializeWatchHistory(watchHistory),
user,
});
} else {
await this.storageService.setItem(this.PERSIST_KEY_WATCH_HISTORY, JSON.stringify(this.serializeWatchHistory(watchHistory)));
}
};
/** Use mediaid of originally watched movie / episode.
* A playlistItem can be either a series item (to show series card) or media item
* */
createWatchHistoryItem = (item: PlaylistItem, mediaid: string, seriesId: string | undefined, videoProgress: number): WatchHistoryItem => {
return {
mediaid,
seriesId,
title: item.title,
tags: item.tags,
duration: item.duration,
progress: videoProgress,
playlistItem: item,
} as WatchHistoryItem;
};
getMaxWatchHistoryCount = () => {
return this.accountService?.features?.watchListSizeLimit || MAX_WATCHLIST_ITEMS_COUNT;
};
/**
* If we already have an element with continue watching state, we:
* 1. Update the progress
* 2. Move the element to the continue watching list start
* Otherwise:
* 1. Move the element to the continue watching list start
* 2. If there are many elements in continue watching state we remove the oldest one
*/
saveItem = async (item: PlaylistItem, seriesItem: PlaylistItem | undefined, videoProgress: number | null, watchHistory: WatchHistoryItem[]) => {
if (!videoProgress) return;
const watchHistoryItem = this.createWatchHistoryItem(seriesItem || item, item.mediaid, seriesItem?.mediaid, videoProgress);
// filter out the existing watch history item, so we can add it to the beginning of the list
const updatedHistory = watchHistory.filter(({ mediaid, seriesId }) => {
return mediaid !== watchHistoryItem.mediaid && (!seriesId || seriesId !== watchHistoryItem.seriesId);
});
updatedHistory.unshift(watchHistoryItem);
updatedHistory.splice(this.getMaxWatchHistoryCount());
return updatedHistory;
};
}