feat(channels): dedicated channel page + ephemeral explore UI
Frontend for the channel-explore feature: - ChannelPage: banner/avatar/stats header, Subscribe/unsubscribe, an "exploring" badge while browsing an un-subscribed channel, Videos/About tabs. Reuses Feed scoped to the channel (scope=all + source=all so the per-user ephemeral videos show). Auto-ingests recent uploads on first visit (background, with a loading note) + "Load more from YouTube" to page deeper; skipped for demo / already-subscribed channels. - App: openChannel/closeChannel as a Back-aware sub-view (history.state._chan, mirrors the YT-search _yt pattern); ChannelPage takes over the content column, nav rail stays. - ChannelLink/cards/player: the channel name now opens our channel page (onChannelFilter → onOpenChannel); the in-card "only this channel" filter button is dropped (the page subsumes it). PlayerModal channel-name wiring follows in the next commit. - api: channelDetail + exploreChannel; ChannelDetail/ExploreResult types. - i18n EN/HU/DE: channel namespace, explore_cleanup scheduler labels, explore config group, channels_explore quota label.
This commit is contained in:
parent
bc4c362423
commit
cc1e670202
18 changed files with 744 additions and 120 deletions
20
frontend/src/i18n/locales/en/channel.json
Normal file
20
frontend/src/i18n/locales/en/channel.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"back": "Back",
|
||||
"exploringBadge": "Exploring",
|
||||
"subscribe": "Subscribe",
|
||||
"subscribedState": "Subscribed",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"unsubTitle": "Unsubscribe?",
|
||||
"unsubBody": "Stop following {{name}} on YouTube?",
|
||||
"subscribed": "Subscribed to {{name}}",
|
||||
"subscribers": "{{formatted}} subscribers",
|
||||
"videoCount_one": "{{count}} video",
|
||||
"videoCount_other": "{{count}} videos",
|
||||
"totalViews": "{{formatted}} views",
|
||||
"joined": "Joined {{date}}",
|
||||
"loadingVideos": "Loading this channel's videos…",
|
||||
"tabVideos": "Videos",
|
||||
"tabAbout": "About",
|
||||
"loadMore": "Load more from YouTube",
|
||||
"noDescription": "This channel has no description."
|
||||
}
|
||||
|
|
@ -9,29 +9,94 @@
|
|||
"quota": "Quota",
|
||||
"backfill": "Backfill",
|
||||
"shorts": "Shorts probe",
|
||||
"batch": "Batch sizes"
|
||||
"batch": "Batch sizes",
|
||||
"explore": "Channel explore"
|
||||
},
|
||||
"fields": {
|
||||
"smtp_host": { "label": "SMTP host", "hint": "e.g. smtp.gmail.com" },
|
||||
"smtp_port": { "label": "SMTP port", "hint": "Usually 587 (STARTTLS)." },
|
||||
"smtp_user": { "label": "SMTP username", "hint": "The sending account / login." },
|
||||
"smtp_from": { "label": "From address", "hint": "e.g. Siftlode <addr@gmail.com>. Defaults to the username." },
|
||||
"smtp_password": { "label": "SMTP password", "hint": "App password. Stored encrypted; write-only — it's never shown back." },
|
||||
"quota_daily_budget": { "label": "Daily quota budget", "hint": "Total YouTube Data API units to spend per day (free tier is 10,000). Default 9000 leaves headroom." },
|
||||
"backfill_quota_reserve": { "label": "Backfill quota reserve", "hint": "Units kept in reserve so scheduled backfill never starves interactive syncs / enrichment." },
|
||||
"search_daily_limit_per_user": { "label": "Live search — daily limit per user", "hint": "Max live YouTube searches per user per day. With the API source each search costs 100 units (so the shared budget affords only ~80-90/day total); with the scrape source it's a pure anti-abuse limit. Set 0 to disable live search." },
|
||||
"search_source": { "label": "Live search source", "hint": "Where live YouTube search results come from. \"scrape\" uses YouTube's internal endpoint and spends no API quota (recommended). \"api\" uses the official search.list (100 units/page). Switch to \"api\" if scraping ever breaks or is blocked." },
|
||||
"backfill_recent_max_videos": { "label": "Recent backfill — max videos", "hint": "Newest videos fetched per channel on the first pass." },
|
||||
"backfill_recent_max_days": { "label": "Recent backfill — max age (days)", "hint": "How far back the first pass reaches per channel." },
|
||||
"shorts_probe_max_seconds": { "label": "Shorts probe — max duration (s)", "hint": "Only videos at or below this length are probed as possible Shorts." },
|
||||
"shorts_probe_batch": { "label": "Shorts probe — batch size", "hint": "How many videos to classify per run." },
|
||||
"enrich_batch_size": { "label": "Enrichment batch size", "hint": "videos.list ids per call (YouTube caps this at 50)." },
|
||||
"autotag_title_sample": { "label": "Auto-tag title sample", "hint": "Recent video titles sampled per channel for language detection." },
|
||||
"youtube_api_key": { "label": "YouTube API key", "hint": "Optional. Used for public reads (channels/videos) so shared backfill doesn't depend on a user's OAuth. Stored encrypted; write-only." },
|
||||
"youtube_api_proxy": { "label": "YouTube API egress proxy", "hint": "Optional. HTTP(S) proxy URL for outbound YouTube API calls — route them through a fixed-IP host so an IP-restricted key keeps working from a dynamic-IP server. Leave empty for direct." },
|
||||
"google_client_id": { "label": "Google client ID", "hint": "OAuth 2.0 client ID for Sign in with Google. Stored encrypted; write-only. Leave both Google fields empty to disable Google sign-in (email+password still works)." },
|
||||
"google_client_secret": { "label": "Google client secret", "hint": "OAuth 2.0 client secret. Stored encrypted; write-only." },
|
||||
"allow_registration": { "label": "Allow registration", "hint": "When on, anyone can submit an email+password registration. They still need to verify their email and be approved by an admin before they can sign in." }
|
||||
"smtp_host": {
|
||||
"label": "SMTP host",
|
||||
"hint": "e.g. smtp.gmail.com"
|
||||
},
|
||||
"smtp_port": {
|
||||
"label": "SMTP port",
|
||||
"hint": "Usually 587 (STARTTLS)."
|
||||
},
|
||||
"smtp_user": {
|
||||
"label": "SMTP username",
|
||||
"hint": "The sending account / login."
|
||||
},
|
||||
"smtp_from": {
|
||||
"label": "From address",
|
||||
"hint": "e.g. Siftlode <addr@gmail.com>. Defaults to the username."
|
||||
},
|
||||
"smtp_password": {
|
||||
"label": "SMTP password",
|
||||
"hint": "App password. Stored encrypted; write-only — it's never shown back."
|
||||
},
|
||||
"quota_daily_budget": {
|
||||
"label": "Daily quota budget",
|
||||
"hint": "Total YouTube Data API units to spend per day (free tier is 10,000). Default 9000 leaves headroom."
|
||||
},
|
||||
"backfill_quota_reserve": {
|
||||
"label": "Backfill quota reserve",
|
||||
"hint": "Units kept in reserve so scheduled backfill never starves interactive syncs / enrichment."
|
||||
},
|
||||
"search_daily_limit_per_user": {
|
||||
"label": "Live search — daily limit per user",
|
||||
"hint": "Max live YouTube searches per user per day. With the API source each search costs 100 units (so the shared budget affords only ~80-90/day total); with the scrape source it's a pure anti-abuse limit. Set 0 to disable live search."
|
||||
},
|
||||
"search_source": {
|
||||
"label": "Live search source",
|
||||
"hint": "Where live YouTube search results come from. \"scrape\" uses YouTube's internal endpoint and spends no API quota (recommended). \"api\" uses the official search.list (100 units/page). Switch to \"api\" if scraping ever breaks or is blocked."
|
||||
},
|
||||
"backfill_recent_max_videos": {
|
||||
"label": "Recent backfill — max videos",
|
||||
"hint": "Newest videos fetched per channel on the first pass."
|
||||
},
|
||||
"backfill_recent_max_days": {
|
||||
"label": "Recent backfill — max age (days)",
|
||||
"hint": "How far back the first pass reaches per channel."
|
||||
},
|
||||
"shorts_probe_max_seconds": {
|
||||
"label": "Shorts probe — max duration (s)",
|
||||
"hint": "Only videos at or below this length are probed as possible Shorts."
|
||||
},
|
||||
"shorts_probe_batch": {
|
||||
"label": "Shorts probe — batch size",
|
||||
"hint": "How many videos to classify per run."
|
||||
},
|
||||
"enrich_batch_size": {
|
||||
"label": "Enrichment batch size",
|
||||
"hint": "videos.list ids per call (YouTube caps this at 50)."
|
||||
},
|
||||
"autotag_title_sample": {
|
||||
"label": "Auto-tag title sample",
|
||||
"hint": "Recent video titles sampled per channel for language detection."
|
||||
},
|
||||
"youtube_api_key": {
|
||||
"label": "YouTube API key",
|
||||
"hint": "Optional. Used for public reads (channels/videos) so shared backfill doesn't depend on a user's OAuth. Stored encrypted; write-only."
|
||||
},
|
||||
"youtube_api_proxy": {
|
||||
"label": "YouTube API egress proxy",
|
||||
"hint": "Optional. HTTP(S) proxy URL for outbound YouTube API calls — route them through a fixed-IP host so an IP-restricted key keeps working from a dynamic-IP server. Leave empty for direct."
|
||||
},
|
||||
"google_client_id": {
|
||||
"label": "Google client ID",
|
||||
"hint": "OAuth 2.0 client ID for Sign in with Google. Stored encrypted; write-only. Leave both Google fields empty to disable Google sign-in (email+password still works)."
|
||||
},
|
||||
"google_client_secret": {
|
||||
"label": "Google client secret",
|
||||
"hint": "OAuth 2.0 client secret. Stored encrypted; write-only."
|
||||
},
|
||||
"allow_registration": {
|
||||
"label": "Allow registration",
|
||||
"hint": "When on, anyone can submit an email+password registration. They still need to verify their email and be approved by an admin before they can sign in."
|
||||
},
|
||||
"explore_grace_days": {
|
||||
"label": "Explore — grace period (days)",
|
||||
"hint": "How long an explored-but-unsubscribed channel (and its ephemeral videos) is kept before the cleanup job removes it. Subscribing keeps it permanently."
|
||||
}
|
||||
},
|
||||
"save": "Save",
|
||||
"saving": "Saving…",
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@
|
|||
"channels_subscribe": "Subscribe",
|
||||
"channels_unsubscribe": "Unsubscribe",
|
||||
"maintenance_revalidate": "Maintenance re-validation",
|
||||
"other": "Other"
|
||||
"other": "Other",
|
||||
"channels_explore": "Channel explore"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@
|
|||
"shorts": "Probes youtube.com/shorts to mark which videos are Shorts. If it stops, Shorts aren't separated from normal videos in the feed.",
|
||||
"subscriptions": "Re-imports every user's YouTube subscriptions. If it stops, channels subscribed to or dropped on YouTube aren't reflected here.",
|
||||
"playlist_sync": "Mirrors each user's YouTube playlists into the app. If it stops, changes to your YouTube playlists don't show up locally.",
|
||||
"maintenance": "Finds videos that can no longer be played (deleted, made private, abandoned premieres), hides them from the feed and removes them after a grace period; re-checks the least-recently-checked videos. If it stops, dead videos linger in the catalogue."
|
||||
"maintenance": "Finds videos that can no longer be played (deleted, made private, abandoned premieres), hides them from the feed and removes them after a grace period; re-checks the least-recently-checked videos. If it stops, dead videos linger in the catalogue.",
|
||||
"explore_cleanup": "Removes channels you explored but never subscribed to (and their videos) after a grace period, so curiosity browsing doesn't pile up in the catalogue. If it stops, abandoned explored channels linger."
|
||||
},
|
||||
"jobs": {
|
||||
"rss_poll": "RSS poll (new uploads)",
|
||||
|
|
@ -77,7 +78,8 @@
|
|||
"subscriptions": "Subscription resync",
|
||||
"playlist_sync": "YouTube playlist sync",
|
||||
"maintenance": "Maintenance + validation",
|
||||
"demo_reset": "Demo account reset"
|
||||
"demo_reset": "Demo account reset",
|
||||
"explore_cleanup": "Explored-channel cleanup"
|
||||
},
|
||||
"queue": {
|
||||
"recentPending": "Channels to sync",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue