# Obs Course Media

> Download course media from a Google Sheets course-flow table and generate OBS scene collection JSON. Use when the user asks to prepare course materials, videos, audio, or images from a flow sheet, especially Google Sheets/Drive workflows that need local media folders and OBS-importable .json scenes.

- Skill: `haodao-culture/obs-course-media` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add haodao-culture/obs-course-media`
- Raw SKILL.md: https://api.skillmd.com/api/skills/haodao-culture/obs-course-media/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: haodao-culture (https://skillmd.com/u/haodao-culture)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/haodao-culture/obs-course-media

---


# OBS Course Media

Use this skill to turn a course flow sheet into local media assets plus OBS scene collection JSON files.
It supports Google Drive and YouTube media, Google Sheets/xlsx flow tables with hyperlinks,
and multi-day courses that need both per-day and all-in-one OBS imports.

## Workflow

1. Identify the Google Sheet URL, target worksheet name, media category, and output folder.
   - Target worksheet names may differ from the user's shorthand. Export or inspect workbook sheet names when needed.
   - Default media category is `影片`.
   - If the user gives a Google Sheet URL, test xlsx export access first. If export fails, ask the user to set sharing to link-viewable or provide an exported `.xlsx`.
2. Run `scripts/course_media_obs.py` to export/read the sheet, find the header row containing `類別` and `內容`, download rows whose category matches, and generate:
   - `media-manifest.json`
   - `OBS-<sheet>-<category>場景.json`
   - The script reads cell hyperlinks from `.xlsx`; do not rely on CSV export.
   - If `內容` has no hyperlink but a `檔案` column exists, the script can use that linked file as a fallback.
   - YouTube links are downloaded with `yt-dlp --no-playlist` so a single linked video does not accidentally download an entire playlist.
3. Verify every downloaded file is real media, not a Google Drive HTML warning page.
   - The script records `file` command output in the manifest.
   - If a file validates as HTML, retry Drive download using the virus-warning confirm flow or rerun with `--overwrite`.
4. For multi-day courses, optionally collect the per-day OBS JSON files into one central import folder and merge them into a single all-in-one OBS JSON.
5. Import the generated JSON in OBS using `Scene Collection > Import`.

## Script Usage

For a Google Sheet:

```bash
python3 /path/to/obs-course-media/scripts/course_media_obs.py \
  --sheet-url "https://docs.google.com/spreadsheets/d/.../edit?gid=..." \
  --sheet-name "Day 1" \
  --output-dir "/absolute/path/to/day-1/media" \
  --category "影片" \
  --collection-name "Course Day 1 Media"
```

For an already exported `.xlsx`:

```bash
python3 /path/to/obs-course-media/scripts/course_media_obs.py \
  --xlsx "/absolute/path/to/流程表.xlsx" \
  --sheet-name "Day 1" \
  --output-dir "/absolute/path/to/day-1/media"
```

When a sheet stores the actual linked media in a `檔案` column rather than `內容`, keep the default fallback behavior or specify another header:

```bash
python3 /path/to/obs-course-media/scripts/course_media_obs.py \
  --xlsx "/absolute/path/to/流程表.xlsx" \
  --sheet-name "Day 2" \
  --output-dir "/absolute/path/to/day-2/media" \
  --fallback-link-header "檔案"
```

If a flow row has no direct hyperlink but the correct local media is known, append it to the OBS JSON:

```bash
--extra-media "Closing song=/absolute/path/to/closing-song.mov"
```

Use `--no-download` to parse the sheet and build JSON from existing local files without downloading again. Use `--overwrite` when replacing partial or HTML files.

## Multi-Day / Combined OBS Imports

For a course with multiple per-day OBS JSON files, put the final import JSON files in one folder such as `OBS匯入檔/`.
The media files do not need to move because OBS JSON paths are absolute.

To create a single all-in-one import file:

```bash
python3 /path/to/obs-course-media/scripts/merge_obs_collections.py \
  --name "Course Complete Media" \
  --output "/absolute/path/to/obs-imports/OBS-complete-media.json" \
  "D0=/absolute/path/to/day-0/media/OBS-day-0-media.json" \
  "D1=/absolute/path/to/day-1/media/OBS-day-1-media.json" \
  "D2=/absolute/path/to/day-2/media/OBS-day-2-media.json" \
  "D3=/absolute/path/to/day-3/media/OBS-day-3-media.json"
```

Keep the per-day JSON files too. The all-in-one file is convenient for one-time setup; per-day files are easier to recover or re-import if only one day changes.

## Google Drive Notes

- Google Sheets CSV export loses cell hyperlinks; prefer `.xlsx` export.
- Google Drive large files often return a virus-scan warning page on first request. The script detects the warning form and follows the confirm link.
- Some rows may describe a needed asset without linking it. In that case, inspect same-row columns such as `檔案` and `資訊備註`, search nearby detail worksheets, download the correct file, then include it with `--extra-media` or a project-specific plan.

## YouTube Notes

- `yt-dlp` and `ffmpeg` must be available for YouTube downloads.
- Always use `--no-playlist` for YouTube links from flow sheets; links often include playlist parameters.
- If a YouTube download creates `.part` files or fails with 403, remove partial files and rerun. Prefer a Google Drive copy for live-course reliability when available.

## OBS JSON Conventions

- The script creates one OBS scene per media file.
- Each scene contains a single `ffmpeg_source` media source.
- Media is bounded to 1920x1080 and `restart_on_activate` is enabled so switching to the scene starts playback from the beginning.
- Generated source paths are absolute. If the media folder moves, regenerate the JSON.
- Quick transitions include `Cmd+P` for fade and `Cmd+B` for fade-to-black.
- Scene names should be operationally clear. If a row groups several videos, include the actual media title in the label, e.g. `Video 1 - Opening Story`.

## Verification Checklist

Before responding to the user:

- Confirm the OBS JSON is valid JSON with `python3 -m json.tool`.
- Confirm all paths referenced by `ffmpeg_source.settings.local_file` exist.
- Confirm `file <media>` reports MP4/MOV/audio/image as expected, not HTML.
- Confirm YouTube links resolved to local `.mp4` files, not web URLs.
- Confirm centralized/merged import files still reference valid absolute media paths.
- Confirm expected hotkeys are present in `quick_transitions`.
- Report any rows with missing direct links and explain how they were handled.

