feat(channels): a "Discover from your history" tab for the channels watched but not followed
The Channel manager lists the channels the Takeout history shows watched often but not followed, the most watched first: twenty at a time, the channel's About on hover, its name to its page, Follow, and Not interested with an Undo. A row's count opens the videos it counts, ready to play one or all. Before a history is uploaded it points to the upload; while the import still asks YouTube it says more channels may join.
This commit is contained in:
@@ -195,6 +195,33 @@ export interface TakeoutStatus {
|
||||
watch_later?: TakeoutCounts;
|
||||
}
|
||||
|
||||
// A channel the user's Takeout history shows them watching often but not following
|
||||
// (`GET /api/takeout/channels`): `watched` counts its videos of five minutes or more they watched.
|
||||
export interface WatchedChannel {
|
||||
id: string;
|
||||
title: string | null;
|
||||
handle: string | null;
|
||||
thumbnail_url: string | null;
|
||||
subscriber_count: number | null;
|
||||
video_count: number | null;
|
||||
description: string | null;
|
||||
watched: number;
|
||||
last_watched_at: string | null;
|
||||
}
|
||||
export interface WatchedChannels {
|
||||
/** Whether a watch history was uploaded at all ("nothing to suggest" vs "nothing to go on"). */
|
||||
imported: boolean;
|
||||
/** How many more channels may join once the import has asked YouTube for their videos' lengths. */
|
||||
pending: number;
|
||||
channels: WatchedChannel[];
|
||||
}
|
||||
/** A channel's counted videos, ready to play, last watched first; `gone`: those YouTube lost. */
|
||||
export interface WatchedChannelVideos {
|
||||
ok: true;
|
||||
items: (Video & { takeout_watched_at: string | null })[];
|
||||
gone: number;
|
||||
}
|
||||
|
||||
export const feedApi = {
|
||||
feed: (f: FeedFilters, cursor: string | null, limit: number): Promise<FeedResponse> =>
|
||||
req(`/api/feed?${feedQuery(f, cursor, limit)}`),
|
||||
@@ -260,4 +287,11 @@ export const feedApi = {
|
||||
// One file of a Google Takeout export, its bytes as they are; the server tells the two apart.
|
||||
takeoutUpload: (file: File): Promise<TakeoutUpload> => uploadReq("/api/takeout", file),
|
||||
takeoutStatus: (): Promise<TakeoutStatus> => req("/api/takeout"),
|
||||
watchedChannels: (): Promise<WatchedChannels> => req("/api/takeout/channels"),
|
||||
watchedChannelVideos: (id: string): Promise<WatchedChannelVideos> =>
|
||||
req(`/api/takeout/channels/${encodeURIComponent(id)}/videos`, { method: "POST" }),
|
||||
dismissWatchedChannel: (id: string) =>
|
||||
req(`/api/takeout/channels/${encodeURIComponent(id)}/dismissal`, { method: "PUT" }),
|
||||
restoreWatchedChannel: (id: string) =>
|
||||
req(`/api/takeout/channels/${encodeURIComponent(id)}/dismissal`, { method: "DELETE" }),
|
||||
};
|
||||
|
||||
@@ -37,6 +37,8 @@ export const qk = {
|
||||
channels: () => ["channels"] as const,
|
||||
channel: (id: string) => ["channel", id] as const,
|
||||
discoveredChannels: () => ["discovered-channels"] as const,
|
||||
watchedChannels: () => ["watched-channels"] as const,
|
||||
watchedChannelVideos: (id: string) => ["watched-channels", "videos", id] as const,
|
||||
blockedChannels: () => ["blocked-channels"] as const,
|
||||
|
||||
// --- playlists ---
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("a subscription change", () => {
|
||||
qk.feed(),
|
||||
qk.feedSummary(),
|
||||
qk.discoveredChannels(),
|
||||
qk.watchedChannels(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "./queryKeys";
|
||||
|
||||
/** Everything a subscribe or an unsubscribe changes: the channel itself, the manager's list and its
|
||||
* counts, the feed and its facets (the channel's videos come or go), and discovery (a subscribed
|
||||
* channel leaves it). ONE set for every site that makes the change: each used to keep
|
||||
* counts, the feed and its facets (the channel's videos come or go), and the two discovery lists (a
|
||||
* subscribed channel leaves them). ONE set for every site that makes the change: each used to keep
|
||||
* its own list, and each list missed something. */
|
||||
export function invalidateSubscription(qc: QueryClient, channelId: string): void {
|
||||
for (const queryKey of [
|
||||
@@ -14,6 +14,7 @@ export function invalidateSubscription(qc: QueryClient, channelId: string): void
|
||||
qk.feed(),
|
||||
qk.feedSummary(),
|
||||
qk.discoveredChannels(),
|
||||
qk.watchedChannels(),
|
||||
])
|
||||
void qc.invalidateQueries({ queryKey });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user