Skip to content

Commit b24e5e9

Browse files
kevincadorvladjerca
authored andcommitted
feat(up-next): add Continue Watching smart sort
1 parent 794957a commit b24e5e9

16 files changed

Lines changed: 271 additions & 38 deletions

File tree

projects/client/i18n/meta/en.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4474,6 +4474,14 @@
44744474
"default": "Sort by default order",
44754475
"description": "Aria-label for the button that allows users to sort items by the default sorting method."
44764476
},
4477+
"button_text_sort_smart": {
4478+
"default": "Smart",
4479+
"description": "Text for the button that allows users to sort up next items by the smart sorting method."
4480+
},
4481+
"button_label_sort_smart": {
4482+
"default": "Sort by smart order",
4483+
"description": "Aria-label for the button that allows users to sort up next items by the smart sorting method."
4484+
},
44774485
"button_text_sort_added_date": {
44784486
"default": "Date Added",
44794487
"description": "Text for the button that allows users to sort items by the date they were added."
@@ -8003,6 +8011,14 @@
80038011
"default": "Use our next-gen engine to surface recommended movies and shows beyond the obvious.",
80048012
"description": "Description for the Smart Recommendations preview feature."
80058013
},
8014+
"preview_feature_title_up_next_smart_sort": {
8015+
"default": "Continue Watching Smart Sort",
8016+
"description": "Title for the Up Next Smart Sort preview feature."
8017+
},
8018+
"preview_feature_description_up_next_smart_sort": {
8019+
"default": "Test a smarter way to sort your Continue Watching.",
8020+
"description": "Description for the Up Next Smart Sort preview feature."
8021+
},
80068022
"preview_feature_title_rewatch": {
80078023
"default": "Rewatch",
80088024
"description": "Title for the Rewatch preview feature."

projects/client/src/lib/features/feature-flag/models/FeatureFlag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export enum FeatureFlag {
33
ScopedFavorites = 'scoped-favorites',
44
SmartRelated = 'smart-related',
55
SmartRecommendations = 'smart-recommendations',
6+
UpNextSmartSort = 'up-next-smart-sort',
67
Rewatching = 'rewatching',
78
Leaderboard = 'leaderboard',
89
}

projects/client/src/lib/features/feature-flag/models/featureFlagDefinitions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import EditModeIcon from '$lib/components/icons/EditModeIcon.svelte';
33
import FastRewindIcon from '$lib/components/icons/FastRewindIcon.svelte';
44
import FavoriteIcon from '$lib/components/icons/FavoriteIcon.svelte';
55
import PeopleIcon from '$lib/components/icons/PeopleIcon.svelte';
6+
import SmartListIcon from '$lib/components/icons/SmartListIcon.svelte';
67
import { m } from '$lib/features/i18n/messages.ts';
78
import { UrlBuilder } from '$lib/utils/url/UrlBuilder.ts';
89
import type { Component } from 'svelte';
@@ -56,6 +57,16 @@ export const featureFlagDefinitions: FeatureFlagDefinitions = {
5657
title: () => m.preview_feature_title_smart_recommendations(),
5758
description: () => m.preview_feature_description_smart_recommendations(),
5859
},
60+
[FeatureFlag.UpNextSmartSort]: {
61+
icon: SmartListIcon,
62+
title: () => m.preview_feature_title_up_next_smart_sort(),
63+
description: () => m.preview_feature_description_up_next_smart_sort(),
64+
featureLink: () =>
65+
openFeatureLink(
66+
UrlBuilder.progress('me', { sort_by: 'smart' }),
67+
m.preview_feature_title_up_next_smart_sort(),
68+
),
69+
},
5970
[FeatureFlag.Rewatching]: {
6071
icon: FastRewindIcon,
6172
title: () => m.preview_feature_title_rewatch(),

projects/client/src/lib/features/feature-flag/useFeatureFlag.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ describe('store: useFeatureFlag', () => {
3939
);
4040
expect(await waitForValue(isEnabled(FeatureFlag.SmartRelated), true))
4141
.toBe(true);
42+
expect(await waitForValue(isEnabled(FeatureFlag.UpNextSmartSort), true))
43+
.toBe(true);
4244
});
4345

4446
it('should default every flag to disabled for non-director accounts', async () => {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { UpNextSortBy } from '$lib/sections/lists/progress/UpNextSortBy.ts';
2+
import type { MovieProgressEntry } from '../../../models/MovieProgressEntry.ts';
3+
4+
export function getMovieProgressSortValue(
5+
entry: MovieProgressEntry,
6+
sortBy: UpNextSortBy | undefined,
7+
): number {
8+
switch (sortBy) {
9+
case 'released':
10+
return entry.effectiveReleaseDate.getTime();
11+
case 'remaining':
12+
return 0;
13+
default:
14+
return entry.lastWatchedAt?.getTime() ?? 0;
15+
}
16+
}

projects/client/src/lib/requests/queries/sync/_internal/interleaveMediaProgress.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,46 @@ describe('interleaveMediaProgress', () => {
239239
expect(result[3]!.key).toBe('episode-2');
240240
});
241241
});
242+
243+
describe('sorting by smart', () => {
244+
it('should interleave movies by progress date without reordering smart episodes', () => {
245+
const episodes = [
246+
createContinueEpisode(
247+
'episode-smart-first',
248+
new Date('2020-01-01'),
249+
new Date('2024-01-05'),
250+
),
251+
createContinueEpisode(
252+
'episode-smart-second',
253+
new Date('2020-01-01'),
254+
new Date('2024-01-01'),
255+
),
256+
];
257+
const movies = [
258+
createContinueMovie(
259+
'movie-first',
260+
new Date('2020-01-01'),
261+
new Date('2024-01-07'),
262+
),
263+
createContinueMovie(
264+
'movie-second',
265+
new Date('2020-01-01'),
266+
new Date('2024-01-03'),
267+
),
268+
];
269+
270+
const result = interleaveMediaProgress({
271+
episodes,
272+
movies,
273+
sortBy: 'smart',
274+
});
275+
276+
expect(result.map((e) => e.key)).toEqual([
277+
'movie-first',
278+
'episode-smart-first',
279+
'movie-second',
280+
'episode-smart-second',
281+
]);
282+
});
283+
});
242284
});

projects/client/src/lib/requests/queries/sync/_internal/interleaveMediaProgress.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { UpNextSortBy } from '$lib/sections/lists/progress/UpNextSortBy.ts'
22
import type { SortDirection } from '$lib/sections/lists/user/models/SortDirection.ts';
33
import type { MovieProgressEntry } from '../../../models/MovieProgressEntry.ts';
44
import type { UpNextEntry } from '../../../models/UpNextEntry.ts';
5+
import { getMovieProgressSortValue } from './getMovieProgressSortValue.ts';
56
import { sortMovieProgress } from './sortMovieProgress.ts';
67

78
type SortMediaProgressProps = {
@@ -11,22 +12,31 @@ type SortMediaProgressProps = {
1112
sortHow?: SortDirection;
1213
};
1314

14-
function getComparableValues(
15-
movie: MovieProgressEntry,
15+
function getEpisodeSortValue(
1616
episode: UpNextEntry,
1717
sortBy: UpNextSortBy,
18-
): [number, number] {
18+
): number {
1919
switch (sortBy) {
2020
case 'released':
21-
return [
22-
movie.effectiveReleaseDate.getTime(),
23-
episode.effectiveReleaseDate.getTime(),
24-
];
21+
return episode.effectiveReleaseDate.getTime();
2522
case 'remaining':
26-
return [0, episode.remaining];
23+
return episode.remaining;
24+
case 'smart':
25+
return episode.lastWatchedAt?.getTime() ?? 0;
2726
}
2827
}
2928

29+
function getComparableValues(
30+
movie: MovieProgressEntry,
31+
episode: UpNextEntry,
32+
sortBy: UpNextSortBy,
33+
): [number, number] {
34+
return [
35+
getMovieProgressSortValue(movie, sortBy),
36+
getEpisodeSortValue(episode, sortBy),
37+
];
38+
}
39+
3040
function shouldInsertBefore(
3141
movie: MovieProgressEntry,
3242
episode: UpNextEntry,

projects/client/src/lib/requests/queries/sync/_internal/sortMovieProgress.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,18 @@ describe('sortMovieProgress', () => {
9898
expect(result).toHaveLength(2);
9999
});
100100
});
101+
102+
describe('sortBy smart', () => {
103+
it('should keep movies on the default progress sort', () => {
104+
const entries = [
105+
createMovie('a', { lastWatchedAt: new Date('2024-01-01') }),
106+
createMovie('b', { lastWatchedAt: new Date('2024-01-03') }),
107+
createMovie('c', { lastWatchedAt: new Date('2024-01-02') }),
108+
];
109+
110+
const result = sortMovieProgress({ entries, sortBy: 'smart' });
111+
112+
expect(result.map((e) => e.key)).toEqual(['b', 'c', 'a']);
113+
});
114+
});
101115
});
Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,22 @@
11
import type { UpNextSortBy } from '$lib/sections/lists/progress/UpNextSortBy.ts';
22
import type { SortDirection } from '$lib/sections/lists/user/models/SortDirection.ts';
33
import type { MovieProgressEntry } from '../../../models/MovieProgressEntry.ts';
4+
import { getMovieProgressSortValue } from './getMovieProgressSortValue.ts';
45

56
type SortMovieProgressProps = {
67
entries: ReadonlyArray<MovieProgressEntry>;
78
sortBy?: UpNextSortBy;
89
sortHow?: SortDirection;
910
};
1011

11-
function getSortValue(
12-
entry: MovieProgressEntry,
13-
sortBy: UpNextSortBy | undefined,
14-
): number {
15-
switch (sortBy) {
16-
case 'released':
17-
return entry.effectiveReleaseDate.getTime();
18-
case 'remaining':
19-
return 0;
20-
default:
21-
return entry.lastWatchedAt?.getTime() ?? 0;
22-
}
23-
}
24-
2512
export function sortMovieProgress(
2613
{ entries, sortBy, sortHow = 'desc' }: SortMovieProgressProps,
2714
): MovieProgressEntry[] {
2815
const direction = sortHow === 'asc' ? 1 : -1;
2916

3017
return entries.toSorted(
31-
(a, b) => (getSortValue(a, sortBy) - getSortValue(b, sortBy)) * direction,
18+
(a, b) =>
19+
(getMovieProgressSortValue(a, sortBy) -
20+
getMovieProgressSortValue(b, sortBy)) * direction,
3221
);
3322
}

projects/client/src/lib/requests/queries/sync/upNextNitroQuery.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { UpNextMappedMock } from '$mocks/data/sync/mapped/UpNextMappedMock.ts';
2+
import { UpNextResponseMock } from '$mocks/data/sync/response/UpNextResponseMock.ts';
3+
import { server } from '$mocks/server.ts';
24
import { createTestBedInfiniteQuery } from '$test/beds/query/createTestBedInfiniteQuery.ts';
35
import { runQuery } from '$test/beds/query/runQuery.ts';
46
import { mapToEntries } from '$test/utils/mapToEntries.ts';
7+
import { http, HttpResponse } from 'msw';
58
import { describe, expect, it } from 'vitest';
69
import { upNextNitroQuery } from './upNextNitroQuery.ts';
710

@@ -17,4 +20,29 @@ describe('upNextNitroQuery', () => {
1720

1821
expect(result).to.deep.equal(UpNextMappedMock);
1922
});
23+
24+
it('should request smart sort for up next', async () => {
25+
let requestedUrl: URL | undefined;
26+
27+
server.use(
28+
http.get(
29+
'http://localhost/sync/progress/up_next_nitro',
30+
({ request }) => {
31+
requestedUrl = new URL(request.url);
32+
return HttpResponse.json(UpNextResponseMock);
33+
},
34+
),
35+
);
36+
37+
await runQuery({
38+
factory: () =>
39+
createTestBedInfiniteQuery(
40+
upNextNitroQuery({ limit: 10, sortBy: 'smart', sortHow: 'desc' }),
41+
),
42+
mapper: mapToEntries,
43+
});
44+
45+
expect(requestedUrl?.searchParams.get('sort_by')).toBe('smart');
46+
expect(requestedUrl?.searchParams.get('sort_how')).toBe('desc');
47+
});
2048
});

0 commit comments

Comments
 (0)