Skip to content

feat: multi-channel support via named profiles - #10

Merged
jdwit merged 4 commits into
mainfrom
feat/multi-channel-profiles
May 26, 2026
Merged

jdwit merged 4 commits into
mainfrom
feat/multi-channel-profiles

Conversation

@jdwit

@jdwit jdwit commented May 24, 2026

Copy link
Copy Markdown
Owner

Proposal

Add support for managing multiple YouTube channels from one ytstudio install, using named credential profiles. Inspired by the approach in the ParkerClelland fork, reworked to fit the current codebase (post headless-login) with a smaller surface and test coverage.

Opening this as a proposal for review; not for merge until you have looked it over.

What it does

Each login is stored under its own named profile. Every existing command (videos, analytics, comments, ...) keeps working unchanged and operates on the active channel.

ytstudio channel add work       # authenticate a new channel and make it active
ytstudio channel add personal
ytstudio channel list           # active channel marked with *
ytstudio channel use work       # switch active channel
ytstudio channel status work    # auth status (defaults to active)
ytstudio channel remove personal
YTSTUDIO_PROFILE=work ytstudio videos list   # per-command override for scripting

Design

  • Storage: ~/.config/ytstudio-cli/profiles/<name>/credentials.json (+ meta.json caching the channel title/id for display). Active profile tracked in state.json. The shared OAuth client secrets stay at the top level, so ytstudio init is still a one-time step.
  • Seam: config.load/save/clear_credentials and api.get_credentials/get_status take an optional profile that defaults to the active one. Because every command already routes through get_authenticated_service(), nothing in the command layer needed per-command changes.
  • Migration: existing single-channel installs (credentials.json at the top level) are moved into the default profile automatically on first run. One-shot, no-op once profiles exist.
  • Security: credential and client-secret files are written with owner-only (0600) permissions. Profile names are validated (^[A-Za-z0-9][A-Za-z0-9_-]*$) to keep them safe as directory names.

Naming

Storage primitive is a "profile"; the user-facing command is channel since that is the tool's domain noun. Happy to rename to ytstudio profile ... if you prefer one consistent term.

Tests

channel command group fully covered (tests/test_channel.py), plus config profile/migration/permissions tests. Full suite: 120 passed, ruff clean. Also smoke-tested end to end against a throwaway HOME (migration, list/use/remove, invalid/duplicate name handling).

Out of scope (deliberately)

Livestreams command from the same fork is a separate feature; this PR is only the multi-channel groundwork.

jdwit added 2 commits May 24, 2026 15:22
Store each YouTube login under its own named profile so the CLI can manage
multiple channels. All existing commands operate on the active channel.

- config: profiles/<name>/credentials.json, state.json active_profile,
  YTSTUDIO_PROFILE env override, profile name validation, owner-only (600)
  permissions on credential and client-secret files
- channel command group: add, list, use, status, remove
- api: authenticate/get_credentials/get_status are profile-aware; channel
  title/id cached as profile meta at login for display in `channel list`
- one-shot migration of pre-profiles credentials.json into the default profile
Address review findings:
- validate profile names at the path boundary (profile_dir raises, profile_exists
  guards); reject an invalid YTSTUDIO_PROFILE instead of letting it become a path
- is_valid_profile_name uses fullmatch so a trailing newline no longer passes
- get_credentials resolves the profile once so a token refresh saves to the same profile
- load_credentials returns None on corrupt JSON instead of crashing
- channel-info fetch after login is best-effort; a quota error no longer fails login
- channel status guards against unknown names like use/remove
- config dirs created 0700 and secrets written without a world-readable window
- channel list shows the cached handle (custom_url)
@jdwit

jdwit commented May 24, 2026

Copy link
Copy Markdown
Owner Author

Review pass (two independent reviewers) + fixes applied

Codex CLI is broken locally (native binary ENOENT), so I ran two independent subagent reviews (correctness/security and design/UX). Both converged on the same top issues. Fixed in afcd8d7:

  • Path-traversal via profile names / YTSTUDIO_PROFILE (top finding from both): names are now validated at the path boundary (profile_dir raises, profile_exists guards, invalid YTSTUDIO_PROFILE is rejected with a warning). Previously YTSTUDIO_PROFILE=../../x or an absolute path could route a credential write outside profiles/.
  • get_credentials resolved the active profile twice across a token refresh; now resolved once so the refreshed token saves to the same profile.
  • is_valid_profile_name allowed a trailing newline ($ quirk); now uses fullmatch.
  • load_credentials crashed on corrupt JSON; now returns None (treated as not-authenticated), consistent with state/meta loading.
  • Post-login channel fetch could fail an otherwise-successful login on a quota error; now best-effort.
  • channel status now guards unknown names like use/remove.
  • Permissions: config/profile dirs created 0700, secrets written with no world-readable window.
  • channel list now shows the cached handle (custom_url) so look-alike titles are distinguishable.

Tests: 129 passed, ruff clean. Added tests for env-var fallback, traversal rejection, corrupt-JSON handling, 0700 dirs, and the status guard.

One open decision (not fixed - your call)

Both reviewers flagged the profile vs channel vocabulary split. The storage primitive is "profile" (profiles/, YTSTUDIO_PROFILE, DEFAULT_PROFILE) but the command is channel. One reviewer notes channel collides with YouTube's own noun (a single login can own multiple YouTube channels), so the user-facing term is arguably a login/account, not a channel.

Options:

  1. Keep channel command, rename env var to YTSTUDIO_CHANNEL for consistency.
  2. Rename the command to ytstudio profile ... and keep YTSTUDIO_PROFILE.
  3. Leave as-is (profile internal, channel user-facing) and document the relationship.

Happy to apply whichever you prefer before merge.

Possible follow-ups (out of scope here)

  • channel rename; showing the active channel in videos/analytics/comments output; wiring or removing the now profile-unaware logout().

jdwit added 2 commits May 25, 2026 06:18
- get_authenticated_service / get_data_service / get_analytics_service now
  accept and forward an explicit profile, completing the optional-profile
  seam that already existed in get_credentials and get_status.
- authenticate() resolves the target profile up-front so a profile switch
  during a long OAuth flow cannot redirect freshly minted credentials to
  a different channel.
- state.json writes go through an atomic temp+os.replace path; mutations
  in set_active_profile, remove_profile, and migrate_legacy_credentials
  are serialized under an fcntl-based config lock so a partial write or a
  concurrent first-run cannot leave state corrupt or lose credentials.
- list_profiles() filters stray directory names through is_valid_profile_name,
  so manual edits under profiles/ no longer surface as profiles or block
  legacy migration.

Tests cover profile propagation through get_authenticated_service, the
once-resolved profile in authenticate(), atomic state.json writes, and
that migration ignores stray invalid directories.
@jdwit
jdwit merged commit af0fbfb into main May 26, 2026
5 checks passed
@jdwit
jdwit deleted the feat/multi-channel-profiles branch May 26, 2026 20:14
@jdwit jdwit mentioned this pull request May 26, 2026
3 tasks done
jdwit added a commit that referenced this pull request May 26, 2026
Highlights since v0.1.1:

- Multi-channel profile support: 'ytstudio profile add/use/list/remove'
  with per-profile credential storage and automatic migration from
  single-channel installs (#10).
- New 'ytstudio livestreams' command: list/show/schedule/start/stop/update
  YouTube live broadcasts, with stream-key handling and demo-mode support
  (#12).
- New 'yts' short CLI alias (#11).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant