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
b5141ab112
commit
fb6f0c5dcb
11 changed files with 517 additions and 4 deletions
|
|
@ -20,9 +20,12 @@ import { notify } from "../lib/notifications";
|
|||
import Tooltip from "./Tooltip";
|
||||
import Avatar from "./Avatar";
|
||||
import DataTable, { type Column } from "./DataTable";
|
||||
import ChannelDiscovery from "./ChannelDiscovery";
|
||||
import TagManager from "./TagManager";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
||||
type ChannelsView = "subscribed" | "discovery";
|
||||
|
||||
export type ChannelStatusFilter = "all" | "needs_full" | "fully_synced" | "hidden";
|
||||
|
||||
// Compact total-duration label for a whole channel (hours-scale, so H:MM:SS would be huge).
|
||||
|
|
@ -64,6 +67,17 @@ export default function Channels({
|
|||
const qc = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
|
||||
// Which tab is showing: the user's subscriptions, or channels discovered from their
|
||||
// playlists. Persisted so a reload keeps the active tab (see other siftlode.* keys).
|
||||
const [view, setView] = useState<ChannelsView>(() =>
|
||||
localStorage.getItem("siftlode.channelsView") === "discovery"
|
||||
? "discovery"
|
||||
: "subscribed"
|
||||
);
|
||||
useEffect(() => {
|
||||
localStorage.setItem("siftlode.channelsView", view);
|
||||
}, [view]);
|
||||
|
||||
// A YouTube-gated action (sync, backfill, unsubscribe) that 403s means the user hasn't
|
||||
// granted the needed scope — surface that with a "Connect" action instead of a vague fail.
|
||||
const notifyActionError = (err: unknown, fallbackKey: string) => {
|
||||
|
|
@ -348,8 +362,39 @@ export default function Channels({
|
|||
</div>
|
||||
);
|
||||
|
||||
const tabs = (
|
||||
<div className="px-4 pt-4 max-w-7xl mx-auto">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{(["subscribed", "discovery"] as ChannelsView[]).map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setView(v)}
|
||||
className={`text-sm px-3 py-1.5 rounded-full border transition ${
|
||||
view === v
|
||||
? "bg-accent text-accent-fg border-accent"
|
||||
: "bg-card border-border text-muted hover:border-accent"
|
||||
}`}
|
||||
>
|
||||
{t(`channels.tabs.${v}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (view === "discovery") {
|
||||
return (
|
||||
<>
|
||||
{tabs}
|
||||
<ChannelDiscovery canWrite={canWrite} onOpenWizard={onOpenWizard} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-7xl mx-auto">
|
||||
<>
|
||||
{tabs}
|
||||
<div className="px-4 pb-4 pt-3 max-w-7xl mx-auto">
|
||||
{/* Per-user sync status + catalog-wide actions on one row (search/tags filtering
|
||||
now lives in the table headers). */}
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap mb-4">
|
||||
|
|
@ -468,6 +513,7 @@ export default function Channels({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue