) : (
n.action &&
diff --git a/frontend/src/i18n/locales/de/channels.json b/frontend/src/i18n/locales/de/channels.json
index b559115..673ef86 100644
--- a/frontend/src/i18n/locales/de/channels.json
+++ b/frontend/src/i18n/locales/de/channels.json
@@ -5,6 +5,25 @@
"syncSubscriptionsHint": "Importiert deine Abo-Liste erneut von YouTube — fügt neu abonnierte Kanäle hinzu und entfernt abbestellte. Die Videos selbst werden weiterhin automatisch im Hintergrund synchronisiert; sie werden hierbei nicht neu geladen.",
"backfillEverything": "Alles nachladen",
"backfillEverythingHint": "Fordert das vollständige Nachladen des gesamten Katalogs für jeden abonnierten Kanal an. Ältere Videos und die Suche werden vollständig, soweit das gemeinsame Tageskontingent es zulässt — das kann eine Weile dauern.",
+ "tabs": {
+ "subscribed": "Abonnements",
+ "discovery": "Aus Playlists entdecken"
+ },
+ "discovery": {
+ "intro": "Kanäle, die in deinen Playlists vorkommen, die du aber nicht abonniert hast. Abonniere sie, um ihnen zu folgen — ihre neuen Uploads erscheinen dann in deinem Feed (das Abonnieren kostet etwas YouTube-Kontingent; vorhandene Videos werden nicht erneut geladen).",
+ "loading": "Kanäle werden gesucht…",
+ "empty": "Keine neuen Kanäle — jeden Kanal in deinen Playlists abonnierst du bereits.",
+ "subscribe": "Abonnieren",
+ "subscribeHint": "Diesen Kanal auf YouTube abonnieren (ändert dein echtes Konto; verbraucht etwas API-Kontingent). Seine Videos kommen bei der nächsten Hintergrund-Synchronisierung.",
+ "needWriteHint": "Aktiviere das Bearbeiten von Playlists in den Einstellungen, um auf YouTube zu abonnieren.",
+ "subscribedTitle": "Auf YouTube abonniert",
+ "subscribedBody": "Du folgst jetzt {{name}} — neue Uploads erscheinen in deinem Feed.",
+ "subscribeFailed": "Abonnieren fehlgeschlagen",
+ "cols": {
+ "inPlaylists": "In Playlists",
+ "inPlaylistsHint": "{{videos}} Video(s) von diesem Kanal in {{playlists}} deiner Playlist(s)."
+ }
+ },
"filters": {
"all": "Alle",
"needsFull": "Verlauf unvollständig",
diff --git a/frontend/src/i18n/locales/de/notifications.json b/frontend/src/i18n/locales/de/notifications.json
index f08acb7..ec7f745 100644
--- a/frontend/src/i18n/locales/de/notifications.json
+++ b/frontend/src/i18n/locales/de/notifications.json
@@ -8,6 +8,8 @@
"findInFeed": "Im Feed finden",
"unhide": "Einblenden",
"unwatch": "Nicht angesehen",
+ "openInManager": "Kanalverwaltung",
+ "openOnYouTube": "Auf YouTube öffnen",
"unhidden": "Eingeblendet „{{title}}”",
"unwatched": "Als nicht angesehen markiert „{{title}}”",
"time": {
diff --git a/frontend/src/i18n/locales/en/channels.json b/frontend/src/i18n/locales/en/channels.json
index 80d1b11..e2da771 100644
--- a/frontend/src/i18n/locales/en/channels.json
+++ b/frontend/src/i18n/locales/en/channels.json
@@ -5,6 +5,25 @@
"syncSubscriptionsHint": "Re-import your subscription list from YouTube — adds channels you've newly followed and drops ones you've unfollowed. The videos themselves keep syncing automatically in the background; this does not re-fetch them.",
"backfillEverything": "Backfill everything",
"backfillEverythingHint": "Request full back-catalog backfill for every channel you're subscribed to. Older videos and search become complete as the shared daily quota allows — this can take a while.",
+ "tabs": {
+ "subscribed": "Subscriptions",
+ "discovery": "Discover from playlists"
+ },
+ "discovery": {
+ "intro": "Channels that appear in your playlists but that you don't subscribe to. Subscribe to follow them — their new uploads will start showing in your feed (subscribing costs a little YouTube quota; existing videos aren't re-fetched).",
+ "loading": "Finding channels…",
+ "empty": "No new channels — every channel in your playlists is one you already follow.",
+ "subscribe": "Subscribe",
+ "subscribeHint": "Subscribe to this channel on YouTube (changes your real account; uses a little API quota). Its videos arrive on the next background sync.",
+ "needWriteHint": "Enable playlist editing in Settings to subscribe on YouTube.",
+ "subscribedTitle": "Subscribed on YouTube",
+ "subscribedBody": "You're now following {{name}} — its new uploads will start arriving in your feed.",
+ "subscribeFailed": "Subscribe failed",
+ "cols": {
+ "inPlaylists": "In playlists",
+ "inPlaylistsHint": "{{videos}} video(s) from this channel across {{playlists}} of your playlist(s)."
+ }
+ },
"filters": {
"all": "All",
"needsFull": "Needs full history",
diff --git a/frontend/src/i18n/locales/en/notifications.json b/frontend/src/i18n/locales/en/notifications.json
index 352bc22..08a23dd 100644
--- a/frontend/src/i18n/locales/en/notifications.json
+++ b/frontend/src/i18n/locales/en/notifications.json
@@ -8,6 +8,8 @@
"findInFeed": "Find in feed",
"unhide": "Unhide",
"unwatch": "Unwatch",
+ "openInManager": "Channel manager",
+ "openOnYouTube": "Open on YouTube",
"unhidden": "Unhidden “{{title}}”",
"unwatched": "Unwatched “{{title}}”",
"time": {
diff --git a/frontend/src/i18n/locales/hu/channels.json b/frontend/src/i18n/locales/hu/channels.json
index 3b43b30..6ab23bc 100644
--- a/frontend/src/i18n/locales/hu/channels.json
+++ b/frontend/src/i18n/locales/hu/channels.json
@@ -5,6 +5,25 @@
"syncSubscriptionsHint": "Újraimportálja a feliratkozási listádat a YouTube-ról — hozzáadja az újonnan követett csatornákat, és eltávolítja azokat, amelyekről leiratkoztál. Maguk a videók a háttérben automatikusan tovább szinkronizálódnak; ez nem tölti le őket újra.",
"backfillEverything": "Minden letöltése",
"backfillEverythingHint": "Teljes archívum letöltését kéri minden csatornához, amelyre feliratkoztál. A régebbi videók és a keresés a megosztott napi kvóta függvényében válik teljessé — ez eltarthat egy ideig.",
+ "tabs": {
+ "subscribed": "Feliratkozások",
+ "discovery": "Felfedezés a lejátszási listákból"
+ },
+ "discovery": {
+ "intro": "Csatornák, amelyek megjelennek a lejátszási listáidban, de még nem iratkoztál fel rájuk. Iratkozz fel, hogy kövesd őket — az új feltöltéseik megjelennek a hírfolyamodban (a feliratkozás kevés YouTube-kvótát használ; a meglévő videókat nem tölti le újra).",
+ "loading": "Csatornák keresése…",
+ "empty": "Nincs új csatorna — a lejátszási listáidban minden csatornát már követsz.",
+ "subscribe": "Feliratkozás",
+ "subscribeHint": "Feliratkozás erre a csatornára a YouTube-on (a valódi fiókodat módosítja; kevés API-kvótát használ). A videói a következő háttér-szinkronnál érkeznek.",
+ "needWriteHint": "Engedélyezd a lejátszási listák szerkesztését a Beállításokban a feliratkozáshoz.",
+ "subscribedTitle": "Feliratkozva a YouTube-on",
+ "subscribedBody": "Mostantól követed: {{name}} — az új feltöltései megjelennek a hírfolyamodban.",
+ "subscribeFailed": "A feliratkozás sikertelen",
+ "cols": {
+ "inPlaylists": "Listákban",
+ "inPlaylistsHint": "{{videos}} videó ettől a csatornától, {{playlists}} lejátszási listádban."
+ }
+ },
"filters": {
"all": "Mind",
"needsFull": "Hiányos előzmény",
diff --git a/frontend/src/i18n/locales/hu/notifications.json b/frontend/src/i18n/locales/hu/notifications.json
index 41c82c3..e305c4b 100644
--- a/frontend/src/i18n/locales/hu/notifications.json
+++ b/frontend/src/i18n/locales/hu/notifications.json
@@ -8,6 +8,8 @@
"findInFeed": "Keresés a hírfolyamban",
"unhide": "Megjelenítés",
"unwatch": "Megtekintés visszavonása",
+ "openInManager": "Csatornakezelő",
+ "openOnYouTube": "Megnyitás a YouTube-on",
"unhidden": "Megjelenítve: „{{title}}”",
"unwatched": "Megtekintés visszavonva: „{{title}}”",
"time": {
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 33aae14..448d593 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -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 =>
+ req("/api/channels/discovery"),
+ subscribeChannel: (id: string) =>
+ req(`/api/channels/${id}/subscribe`, { method: "POST" }),
syncSubscriptions: () => req("/api/sync/subscriptions", { method: "POST" }),
// --- playlists ---
diff --git a/frontend/src/lib/notifications.ts b/frontend/src/lib/notifications.ts
index 717533b..583637f 100644
--- a/frontend/src/lib/notifications.ts
+++ b/frontend/src/lib/notifications.ts
@@ -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();
}