feat(channels): discover & subscribe to channels from playlists
Add a "Discover from playlists" tab to the Channel manager that lists
channels appearing in the user's playlists they don't subscribe to, with
a one-click Subscribe.
- GET /api/channels/discovery: local join (playlist_items -> videos ->
channels) minus the user's subscriptions and their own channel. Enriches
stub channels' metadata up front (title/thumbnail/subscriber count via the
API key) so the user can judge a channel before subscribing; videos are
not pulled (the scheduler picks those up).
- POST /api/channels/{id}/subscribe: write-scope gated, subscriptions.insert
+ local Subscription with the returned resource id.
- YouTubeClient.insert_subscription / get_my_channel_id.
- users.yt_channel_id (migration 0019) caches the user's own channel id so
discovery can exclude it.
- Frontend: ChannelDiscovery DataTable, Channels tab toggle (persisted),
api methods, trilingual strings. Subscribe ships a typed notification
payload (ChannelSubscribedMeta) for the inbox to act on.
This commit is contained in:
parent
5ae12a1752
commit
a8ba66007f
11 changed files with 517 additions and 4 deletions
|
|
@ -362,6 +362,20 @@ export interface ManagedChannel {
|
|||
backfill_done: boolean;
|
||||
}
|
||||
|
||||
// A channel that shows up in the user's playlists but that they don't subscribe to —
|
||||
// surfaced in the Channel manager's Discovery tab so they can subscribe in one click.
|
||||
export interface DiscoveredChannel {
|
||||
id: string;
|
||||
title: string | null;
|
||||
handle: string | null;
|
||||
thumbnail_url: string | null;
|
||||
subscriber_count: number | null;
|
||||
video_count: number | null;
|
||||
playlist_video_count: number; // how many of the user's playlist videos are from here
|
||||
playlist_count: number; // across how many of their playlists
|
||||
details_synced: boolean;
|
||||
}
|
||||
|
||||
export interface SchedulerJob {
|
||||
id: string;
|
||||
interval_minutes: number;
|
||||
|
|
@ -485,6 +499,10 @@ export const api = {
|
|||
req(`/api/channels/${id}/tags/${tagId}`, { method: "DELETE" }),
|
||||
unsubscribeChannel: (id: string) =>
|
||||
req(`/api/channels/${id}/subscription`, { method: "DELETE" }),
|
||||
discoveredChannels: (): Promise<DiscoveredChannel[]> =>
|
||||
req("/api/channels/discovery"),
|
||||
subscribeChannel: (id: string) =>
|
||||
req(`/api/channels/${id}/subscribe`, { method: "POST" }),
|
||||
syncSubscriptions: () => req("/api/sync/subscriptions", { method: "POST" }),
|
||||
|
||||
// --- playlists ---
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ export type VideoWatchedMeta = {
|
|||
videoId: string;
|
||||
title: string;
|
||||
};
|
||||
export type NotifMeta = VideoHiddenMeta | VideoWatchedMeta;
|
||||
export type ChannelSubscribedMeta = {
|
||||
kind: "channel-subscribed";
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
};
|
||||
export type NotifMeta = VideoHiddenMeta | VideoWatchedMeta | ChannelSubscribedMeta;
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
|
|
@ -239,7 +244,13 @@ export function remove(id: number): void {
|
|||
* now-stale "Hidden/Watched X" notice disappears instead of lingering with a dead action. */
|
||||
export function resolveVideo(videoId: string): void {
|
||||
const before = items.length;
|
||||
items = items.filter((n) => n.meta?.videoId !== videoId);
|
||||
items = items.filter(
|
||||
(n) =>
|
||||
!(
|
||||
(n.meta?.kind === "video-hidden" || n.meta?.kind === "video-watched") &&
|
||||
n.meta.videoId === videoId
|
||||
)
|
||||
);
|
||||
if (items.length !== before) emit();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue