[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CJ3DJ8HF0C4RJGDHHZKYW4":3,"skill-same:01M6CJ3DJ8HF0C4RJGDHHZKYW4":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":13,"repositoryHtmlUrl":46,"repositoryHomepageUrl":47,"ownerHtmlUrl":48,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M6CJ3DJ8HF0C4RJGDHHZKYW4","audiflow-preset",null,"Create and update audiflow preset patterns and definitions. Use this skill whenever the user wants to create a new playlist pattern, modify an existing pattern, analyze a podcast RSS feed for grouping, search for podcast feeds, validate patterns, or discuss any aspect of the audiflow playlist schema (resolver types, title extractors, episode filters, groups, numbering). Also trigger when the user mentions pattern IDs, feed URLs, the audiflow-editor CLI, or refers to files under patterns\u002F. Even if the user just says \"add a podcast\" or \"new pattern\" or names a specific podcast, this skill applies.","audiflow\u002Faudiflow-preset","audiflow","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F177991168?v=4","01M6CJ39YSRJCAAYKJZK3QZJKX","01M6CJ3A37RY1WBM2875TMT1AM",0,"CC-BY-SA-4.0","https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faudiflow-preset\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"audiflow preset","preset","podcast","patterns","rss feed","validate","rss","update","editor","cli","create","analyze","use","filters","files","schema","search","just","ids","feeds","2026-11-10 10:12:30",false,"---\nname: audiflow-preset\ndescription: Create and update audiflow preset patterns and definitions. Use this skill whenever the user wants to create a new playlist pattern, modify an existing pattern, analyze a podcast RSS feed for grouping, search for podcast feeds, validate patterns, or discuss any aspect of the audiflow playlist schema (resolver types, title extractors, episode filters, groups, numbering). Also trigger when the user mentions pattern IDs, feed URLs, the audiflow-editor CLI, or refers to files under patterns\u002F. Even if the user just says \"add a podcast\" or \"new pattern\" or names a specific podcast, this skill applies.\n---\n\n# audiflow-preset\n\nCreate and manage preset patterns for the audiflow ecosystem. This skill covers the full workflow: searching for podcasts, analyzing RSS feeds, writing pattern JSON, and validating with the editor CLI.\n\n## Quick Reference\n\n### File structure\n```\npatterns\u002F\n  meta.json                         # Root index (all patterns)\n  {patternId}\u002F\n    meta.json                       # Pattern meta (feed matching, playlist order)\n    DESIGN.md                       # Design rationale (optional but recommended)\n    playlists\u002F\n      {playlistId}.json             # Playlist definition (grouping, display)\n```\n\n### Four resolver types\n| Resolver | Groups by | Best for |\n|----------|-----------|----------|\n| `seasonNumber` | Season number (title or RSS) | Podcasts with S01E01 or bracket numbering |\n| `year` | Publication year | Long-running podcasts, no clear title pattern |\n| `titleDiscovery` | Auto-detected title patterns | Story arcs, recurring series |\n| `titleClassifier` | Regex rules you define | Bracket prefixes, category markers, mixed formats |\n\n### Two presentation modes\n| Mode | Behavior |\n|------|----------|\n| `separate` | Each group becomes its own selectable playlist |\n| `combined` | Groups shown as cards inside a single playlist |\n\nFor full schema details, read `references\u002Fschema-reference.md`.\n\n---\n\n## Workflow: Creating a New Pattern\n\n### Step 1: Find the podcast feed\n\nIf the user provides a feed URL, skip to Step 2. Otherwise, search by title:\n\n```bash\npython3 \u003Cskill_path>\u002Fscripts\u002Fsearch-feeds.py \"podcast name\" --country jp\n```\n\nThe script uses the iTunes Search API. Adjust `--country` as needed (us, gb, etc.). Present results and let the user pick the right podcast. Note the `feedUrl` from the chosen result.\n\n### Step 2: Analyze the feed\n\nPrerequisite: requires Python 3.9+ and the `defusedxml` package (`pip install defusedxml`).\n\n```bash\npython3 \u003Cskill_path>\u002Fscripts\u002Fanalyze-feed.py \"\u003Cfeed_url>\"\n```\n\nThis fetches the RSS feed and reports:\n- Feed metadata (title, episode count, date range)\n- RSS season\u002Fepisode tag coverage\n- Title numbering patterns detected\n- Bracket formats\n- Recurring title prefixes\n- Suggested resolver type with reasoning\n- Suggested groups (for titleClassifier)\n- Sample episode titles\n\nReview the output carefully. The suggestions are starting points -- episode titles can be messy and the heuristics may miss nuances. Always look at the sample titles yourself and consider:\n- Are there distinct series\u002Fformats that should be separate playlists?\n- Which episodes should be filtered out (reruns, trailers, ads)?\n- Do title patterns have enough consistency for regex extraction?\n\n### Step 3: Derive the pattern ID\n\nThe ID is deterministic:\n```python\nimport hashlib\n# Prefer podcastGuid when available\nsource = podcast_guid if podcast_guid else feed_url\npattern_id = hashlib.md5(source.encode()).hexdigest()[:12]\n```\n\n### Step 4: Design the pattern\n\nBefore writing JSON, think through these decisions:\n\n**How many playlists?** Look at the episode types. A podcast with regular episodes, bonus content, and live recordings might need 3 playlists. A simple seasonal podcast might need just 1.\n\n**Episode ownership**: Playlists with `episodeFilters` claim episodes first, in the order listed in pattern meta's `playlists` array. Filter-less playlists get the remainder. Use this to separate distinct episode types.\n\n**Resolver choice per playlist**: Different playlists under the same pattern can use different resolvers. The \"regular series\" playlist might use `seasonNumber` while the \"extras\" playlist uses `titleClassifier`.\n\n**Resolver selection guide** -- choose based on the podcast's structure, not just what metadata exists:\n- `seasonNumber`: Use when RSS season tags are reliable (high coverage) AND each season maps to a meaningful unit (a story arc, a topic, a guest run). Many seasons (10+) make this worthwhile. If the podcast only has 2-3 RSS seasons, this gives too few groups.\n- `year`: Use for long-running podcasts (200+ episodes) where no clear title patterns exist. Also good as a secondary \"alternative view\" playlist alongside a primary grouping.\n- `titleDiscovery`: Use when episodes have recurring title patterns that naturally form groups (professor names, series arcs, recurring segments) but there are too many groups to enumerate manually, or new groups appear regularly without pattern changes.\n- `titleClassifier`: Use when you need explicit control over grouping rules -- bracket prefixes, category markers, or mixed formats where regex rules define each group. Best when the number of groups is stable and manageable.\n\n**Group design for titleClassifier**: Groups evaluate in order, first match wins. Put specific patterns first, catch-all (no pattern) last. Consider:\n- Guest episodes (match guest name in title)\n- Topic clusters (match recurring topic keywords)\n- Format variants (daily, weekly, special editions)\n- Catch-all bucket at the end\n\nIf the user already has a specific structure in mind, follow their design. The analysis from Step 2 is a suggestion, not a prescription.\n\n### Step 5: Write the JSON files\n\nCreate three levels of files:\n\n**1. Pattern directory and meta**:\n```\npatterns\u002F{patternId}\u002Fmeta.json\n```\n```json\n{\n  \"dataVersion\": 1,\n  \"id\": \"{patternId}\",\n  \"podcastGuid\": \"guid-if-available\",\n  \"feedUrls\": [\"https:\u002F\u002F...\"],\n  \"yearGroupedEpisodes\": false,\n  \"playlists\": [\"playlist_id_1\", \"playlist_id_2\"]\n}\n```\n\nSet `yearGroupedEpisodes: true` for podcasts with 200+ episodes where year grouping helps navigation in the main episode list.\n\n**2. Playlist definitions**:\n```\npatterns\u002F{patternId}\u002Fplaylists\u002F{playlistId}.json\n```\n\nRefer to `references\u002Fschema-reference.md` for all field options. Here are templates for common setups:\n\n**Understanding the two titleExtractors**: There are two different places titleExtractor appears, and they do completely different things:\n- **Top-level `titleExtractor`**: Generates **group titles** (the name of a season or year). It runs against a representative episode from each group. For a seasonNumber resolver, this produces the label for each season card -- e.g., \"Lincoln Arc\" or \"Season 5\". If the podcast doesn't have named series arcs, use `\"source\": \"seasonNumber\"` with a template like `\"シーズン {value}\"` or `\"Season {value}\"` to generate clean season labels.\n- **`episodeList.titleExtractor`**: Transforms **episode display names** within groups. It strips redundant info (like the series name) that's already conveyed by the group context. For example, stripping `【COTEN RADIO リンカン編15】` down to just the episode title.\n\nDo not confuse these. A pattern that extracts episode-level text (like `「(.+?)」`) should go in `episodeList.titleExtractor`, not at the top level.\n\n**seasonNumber + combined** (most common for season-based podcasts):\n```json\n{\n  \"id\": \"seasons\",\n  \"displayName\": \"Seasons\",\n  \"resolverType\": \"seasonNumber\",\n  \"presentation\": \"combined\",\n  \"titleExtractor\": {\n    \"source\": \"title\",\n    \"pattern\": \"regex to extract the SERIES\u002FSEASON NAME from a representative episode title\",\n    \"group\": 1,\n    \"fallback\": {\n      \"source\": \"seasonNumber\",\n      \"template\": \"Season {value}\"\n    }\n  },\n  \"episodeList\": {\n    \"titleExtractor\": {\n      \"source\": \"title\",\n      \"pattern\": \"regex to clean up EPISODE DISPLAY NAMES (strip redundant series prefix etc.)\",\n      \"group\": 1\n    }\n  },\n  \"groupList\": {\n    \"sort\": { \"field\": \"playlistNumber\", \"order\": \"ascending\" },\n    \"showDateRange\": true,\n    \"yearBinding\": \"pinToYear\"\n  }\n}\n```\n\n**titleClassifier + combined** (most common for category-based podcasts):\n```json\n{\n  \"id\": \"by_category\",\n  \"displayName\": \"Categories\",\n  \"resolverType\": \"titleClassifier\",\n  \"presentation\": \"combined\",\n  \"groups\": [\n    { \"id\": \"series_a\", \"displayName\": \"Series A\", \"pattern\": \"regex\" },\n    { \"id\": \"series_b\", \"displayName\": \"Series B\", \"pattern\": \"regex\" },\n    { \"id\": \"other\", \"displayName\": \"Other\" }\n  ]\n}\n```\n\n**Catch-all playlist** (claims remaining episodes):\n```json\n{\n  \"id\": \"extras\",\n  \"displayName\": \"Extras\",\n  \"resolverType\": \"titleClassifier\",\n  \"presentation\": \"combined\",\n  \"groups\": [\n    { \"id\": \"bonus\", \"displayName\": \"Bonus\", \"pattern\": \"bonus|extra\" },\n    { \"id\": \"other\", \"displayName\": \"Other\" }\n  ]\n}\n```\n\n**3. Update the root index** (`presets\u002Fmeta.json`):\n\nAdd an entry to the `presets` array:\n```json\n{\n  \"id\": \"{presetId}\",\n  \"dataVersion\": 1,\n  \"displayName\": \"Podcast Title\",\n  \"feedUrlHints\": [\"https:\u002F\u002Fanchor.fm\u002Fs\u002F{id}\u002Fpodcast\u002Frss\"],\n  \"playlistCount\": 2\n}\n```\n\nAdd exactly one entry per preset. `feedUrlHints` lists every URL in the preset's `feedUrls`, in the same order (the editor writes it this way). The app treats the preset as a candidate if any hint is a substring of the podcast's feed URL. `audiflow-editor validate` fails if an `id` appears twice.\n\n### Step 6: Write a DESIGN.md\n\nCreate `patterns\u002F{patternId}\u002FDESIGN.md` documenting:\n- Feed characteristics (title format variations, metadata gaps)\n- Playlist breakdown (what each captures and why)\n- Episode identification strategy (primary patterns, fallbacks)\n- Grouping decisions (why this structure, catch-all strategy)\n\nThis helps future maintainers understand the reasoning behind regex choices.\n\n### Step 7: Preview in browser\n\nAfter writing the pattern files, launch the editor web UI so the user can see the actual grouping result with live RSS data:\n\n```bash\n# Start the editor in the background (downloads binary automatically)\nnohup .\u002Feditor.sh > \u002Fdev\u002Fnull 2>&1 &\nEDITOR_PID=$!\n```\n\nTell the user: \"Editor is running at http:\u002F\u002Flocalhost:8080 -- open it to preview your pattern with live feed data. Let me know when you're done and I'll stop the server.\"\n\nWhen the user is done previewing:\n```bash\nkill $EDITOR_PID 2>\u002Fdev\u002Fnull\n```\n\nIf the preview reveals issues (wrong grouping, mismatched regex, episodes in the wrong playlist), fix the JSON files and reload the browser -- the editor picks up file changes.\n\nThe preview step is optional but recommended for new patterns. Skip it if the user just wants to validate and commit.\n\n### Step 8: Validate\n\n```bash\nschema\u002Fscripts\u002Fvalidate.sh patterns\u002F\n```\n\nThis downloads the `audiflow-editor` binary (matching `schema\u002FVERSION`) and runs validation. Fix any errors before committing.\n\n---\n\n## Workflow: Updating an Existing Pattern\n\n1. Read the existing pattern files and DESIGN.md\n2. Understand what needs to change (new playlist, updated groups, filter tweaks)\n3. If modifying regex patterns, analyze sample titles first -- run `analyze-feed.py` to see current episode titles\n4. Make changes, keeping the DESIGN.md in sync\n5. Preview with `.\u002Feditor.sh` to verify grouping looks correct in the browser (optional)\n6. Validate with `schema\u002Fscripts\u002Fvalidate.sh patterns\u002F`\n7. Bump `dataVersion` is handled by CI (`audiflow-editor bump-versions`), but for local testing set it manually\n\n---\n\n## Editor CLI Commands\n\nThe `audiflow-editor` binary is downloaded automatically by the scripts in `schema\u002Fscripts\u002F`.\n\n| Command | Usage | Purpose |\n|---------|-------|---------|\n| `validate` | `audiflow-editor validate patterns\u002F` | Validates all JSON against schemas |\n| `bump-versions` | `audiflow-editor bump-versions HEAD~1` | Auto-increment dataVersion for changed files |\n| `--version` | `audiflow-editor --version` | Show binary version |\n\nSchema version is tracked in `schema\u002FVERSION` (currently v4). The binary is fetched from `audiflow\u002Faudiflow-preset-editor` GitHub releases, cached in `.cache\u002Fbin\u002F{version}\u002F`.\n\n---\n\n## Title Pattern Conventions\n\n### Japanese podcasts\n\nCommon patterns to look for:\n- Full-width brackets: `【content】` -- very common for categorization\n- Numbered brackets: `【{season}-{episode}】` -- structured numbering\n- Date brackets: `【{month}月{day}日】` -- daily\u002Fweekly shows\n- Series markers: `{series}編`, `{topic}シリーズ`\n- Special markers: `番外編`, `特別編`, `ゲスト回`\n- Counter words: `第{n}回`, `第{n}話`\n\n### English podcasts\n\n- `S{n}E{n}` or `Season {n}, Episode {n}` -- standard season\u002Fepisode\n- `{Series Title} - {Episode}` or `{Series Title} | {Episode}` -- delimiter-based\n- `Ep. {n}:`, `Episode {n}:`, `#{n}` -- episode numbering\n- `Best of`, `Encore:`, `Replay:` -- rerun markers (often filtered out)\n- `Bonus:`, `Extra:`, `Special:` -- special content markers\n\n### Writing robust regex patterns\n\n- Escape special regex characters in literal text\n- Use `\\\\s*` or `\\\\s+` for flexible whitespace\n- Use non-greedy quantifiers (`+?`, `*?`) to avoid over-matching\n- For Japanese brackets, match both half-width `[]` and full-width `【】`\n- Test patterns against the sample titles from `analyze-feed.py`\n- Remember: patterns in episodeFilters and groups are case-insensitive\n\n---\n\n## Common Pitfalls\n\n1. **Forgetting the catch-all group**: titleClassifier groups without a catch-all will leave unmatched episodes ungrouped. Always add a pattern-less group at the end.\n\n2. **Pattern order matters**: Groups evaluate first-to-last. Put narrow patterns before broad ones, or episodes get claimed by the wrong group.\n\n3. **One root entry per preset**: A podcast that moved feeds still gets one root index entry. Put both URLs in its `feedUrlHints`; a second entry with the same `id` fails validation.\n\n4. **Missing `id` match**: The `id` in meta.json must match the directory name. The `id` in playlist JSON must match the filename.\n\n5. **episodeFilters logic**: `require` is AND across rules, `exclude` is OR. A definition with both `require` and `exclude` first includes matching episodes, then removes excluded ones.\n\n6. **titleExtractor on titleClassifier**: titleClassifier ignores the top-level titleExtractor. Group names come from GroupDef.displayName. But `episodeList.titleExtractor` still works for transforming episode titles within groups.\n\n7. **Confusing the two titleExtractors**: The top-level `titleExtractor` generates **group titles** (season\u002Fseries names). The `episodeList.titleExtractor` transforms **episode display names**. Putting an episode-title pattern (like `「(.+?)」`) at the top level produces nonsensical group names. When the podcast has no named arcs, use `\"source\": \"seasonNumber\"` with a template for clean group labels.\n","# audiflow-preset\n\nCreate and manage preset patterns for the audiflow ecosystem. This skill covers the full workflow: searching for podcasts, analyzing RSS feeds, writing pattern JSON, and validating with the editor CLI.\n\n## Quick Reference\n\n### File structure\n```\npatterns\u002F\n  meta.json                         # Root index (all patterns)\n  {patternId}\u002F\n    meta.json                       # Pattern meta (feed matching, playlist order)\n    DESIGN.md                       # Design rationale (optional but recommended)\n    playlists\u002F\n      {playlistId}.json             # Playlist definition (grouping, display)\n```\n\n### Four resolver types\n| Resolver | Groups by | Best for |\n|----------|-----------|----------|\n| `seasonNumber` | Season number (title or RSS) | Podcasts with S01E01 or bracket numbering |\n| `year` | Publication year | Long-running podcasts, no clear title pattern |\n| `titleDiscovery` | Auto-detected title patterns | Story arcs, recurring series |\n| `titleClassifier` | Regex rules you define | Bracket prefixes, category markers, mixed formats |\n\n### Two presentation modes\n| Mode | Behavior |\n|------|----------|\n| `separate` | Each group becomes its own selectable playlist |\n| `combined` | Groups shown as cards inside a single playlist |\n\nFor full schema details, read `references\u002Fschema-reference.md`.\n\n---\n\n## Workflow: Creating a New Pattern\n\n### Step 1: Find the podcast feed\n\nIf the user provides a feed URL, skip to Step 2. Otherwise, search by title:\n\n```bash\npython3 \u003Cskill_path>\u002Fscripts\u002Fsearch-feeds.py \"podcast name\" --country jp\n```\n\nThe script uses the iTunes Search API. Adjust `--country` as needed (us, gb, etc.). Present results and let the user pick the right podcast. Note the `feedUrl` from the chosen result.\n\n### Step 2: Analyze the feed\n\nPrerequisite: requires Python 3.9+ and the `defusedxml` package (`pip install defusedxml`).\n\n```bash\npython3 \u003Cskill_path>\u002Fscripts\u002Fanalyze-feed.py \"\u003Cfeed_url>\"\n```\n\nThis fetches the RSS feed and reports:\n- Feed metadata (title, episode count, date range)\n- RSS season\u002Fepisode tag coverage\n- Title numbering patterns detected\n- Bracket formats\n- Recurring title prefixes\n- Suggested resolver type with reasoning\n- Suggested groups (for titleClassifier)\n- Sample episode titles\n\nReview the output carefully. The suggestions are starting points -- episode titles can be messy and the heuristics may miss nuances. Always look at the sample titles yourself and consider:\n- Are there distinct series\u002Fformats that should be separate playlists?\n- Which episodes should be filtered out (reruns, trailers, ads)?\n- Do title patterns have enough consistency for regex extraction?\n\n### Step 3: Derive the pattern ID\n\nThe ID is deterministic:\n```python\nimport hashlib\n# Prefer podcastGuid when available\nsource = podcast_guid if podcast_guid else feed_url\npattern_id = hashlib.md5(source.encode()).hexdigest()[:12]\n```\n\n### Step 4: Design the pattern\n\nBefore writing JSON, think through these decisions:\n\n**How many playlists?** Look at the episode types. A podcast with regular episodes, bonus content, and live recordings might need 3 playlists. A simple seasonal podcast might need just 1.\n\n**Episode ownership**: Playlists with `episodeFilters` claim episodes first, in the order listed in pattern meta's `playlists` array. Filter-less playlists get the remainder. Use this to separate distinct episode types.\n\n**Resolver choice per playlist**: Different playlists under the same pattern can use different resolvers. The \"regular series\" playlist might use `seasonNumber` while the \"extras\" playlist uses `titleClassifier`.\n\n**Resolver selection guide** -- choose based on the podcast's structure, not just what metadata exists:\n- `seasonNumber`: Use when RSS season tags are reliable (high coverage) AND each season maps to a meaningful unit (a story arc, a topic, a guest run). Many seasons (10+) make this worthwhile. If the podcast only has 2-3 RSS seasons, this gives too few groups.\n- `year`: Use for long-running podcasts (200+ episodes) where no clear title patterns exist. Also good as a secondary \"alternative view\" playlist alongside a primary grouping.\n- `titleDiscovery`: Use when episodes have recurring title patterns that naturally form groups (professor names, series arcs, recurring segments) but there are too many groups to enumerate manually, or new groups appear regularly without pattern changes.\n- `titleClassifier`: Use when you need explicit control over grouping rules -- bracket prefixes, category markers, or mixed formats where regex rules define each group. Best when the number of groups is stable and manageable.\n\n**Group design for titleClassifier**: Groups evaluate in order, first match wins. Put specific patterns first, catch-all (no pattern) last. Consider:\n- Guest episodes (match guest name in title)\n- Topic clusters (match recurring topic keywords)\n- Format variants (daily, weekly, special editions)\n- Catch-all bucket at the end\n\nIf the user already has a specific structure in mind, follow their design. The analysis from Step 2 is a suggestion, not a prescription.\n\n### Step 5: Write the JSON files\n\nCreate three levels of files:\n\n**1. Pattern directory and meta**:\n```\npatterns\u002F{patternId}\u002Fmeta.json\n```\n```json\n{\n  \"dataVersion\": 1,\n  \"id\": \"{patternId}\",\n  \"podcastGuid\": \"guid-if-available\",\n  \"feedUrls\": [\"https:\u002F\u002F...\"],\n  \"yearGroupedEpisodes\": false,\n  \"playlists\": [\"playlist_id_1\", \"playlist_id_2\"]\n}\n```\n\nSet `yearGroupedEpisodes: true` for podcasts with 200+ episodes where year grouping helps navigation in the main episode list.\n\n**2. Playlist definitions**:\n```\npatterns\u002F{patternId}\u002Fplaylists\u002F{playlistId}.json\n```\n\nRefer to `references\u002Fschema-reference.md` for all field options. Here are templates for common setups:\n\n**Understanding the two titleExtractors**: There are two different places titleExtractor appears, and they do completely different things:\n- **Top-level `titleExtractor`**: Generates **group titles** (the name of a season or year). It runs against a representative episode from each group. For a seasonNumber resolver, this produces the label for each season card -- e.g., \"Lincoln Arc\" or \"Season 5\". If the podcast doesn't have named series arcs, use `\"source\": \"seasonNumber\"` with a template like `\"シーズン {value}\"` or `\"Season {value}\"` to generate clean season labels.\n- **`episodeList.titleExtractor`**: Transforms **episode display names** within groups. It strips redundant info (like the series name) that's already conveyed by the group context. For example, stripping `【COTEN RADIO リンカン編15】` down to just the episode title.\n\nDo not confuse these. A pattern that extracts episode-level text (like `「(.+?)」`) should go in `episodeList.titleExtractor`, not at the top level.\n\n**seasonNumber + combined** (most common for season-based podcasts):\n```json\n{\n  \"id\": \"seasons\",\n  \"displayName\": \"Seasons\",\n  \"resolverType\": \"seasonNumber\",\n  \"presentation\": \"combined\",\n  \"titleExtractor\": {\n    \"source\": \"title\",\n    \"pattern\": \"regex to extract the SERIES\u002FSEASON NAME from a representative episode title\",\n    \"group\": 1,\n    \"fallback\": {\n      \"source\": \"seasonNumber\",\n      \"template\": \"Season {value}\"\n    }\n  },\n  \"episodeList\": {\n    \"titleExtractor\": {\n      \"source\": \"title\",\n      \"pattern\": \"regex to clean up EPISODE DISPLAY NAMES (strip redundant series prefix etc.)\",\n      \"group\": 1\n    }\n  },\n  \"groupList\": {\n    \"sort\": { \"field\": \"playlistNumber\", \"order\": \"ascending\" },\n    \"showDateRange\": true,\n    \"yearBinding\": \"pinToYear\"\n  }\n}\n```\n\n**titleClassifier + combined** (most common for category-based podcasts):\n```json\n{\n  \"id\": \"by_category\",\n  \"displayName\": \"Categories\",\n  \"resolverType\": \"titleClassifier\",\n  \"presentation\": \"combined\",\n  \"groups\": [\n    { \"id\": \"series_a\", \"displayName\": \"Series A\", \"pattern\": \"regex\" },\n    { \"id\": \"series_b\", \"displayName\": \"Series B\", \"pattern\": \"regex\" },\n    { \"id\": \"other\", \"displayName\": \"Other\" }\n  ]\n}\n```\n\n**Catch-all playlist** (claims remaining episodes):\n```json\n{\n  \"id\": \"extras\",\n  \"displayName\": \"Extras\",\n  \"resolverType\": \"titleClassifier\",\n  \"presentation\": \"combined\",\n  \"groups\": [\n    { \"id\": \"bonus\", \"displayName\": \"Bonus\", \"pattern\": \"bonus|extra\" },\n    { \"id\": \"other\", \"displayName\": \"Other\" }\n  ]\n}\n```\n\n**3. Update the root index** (`presets\u002Fmeta.json`):\n\nAdd an entry to the `presets` array:\n```json\n{\n  \"id\": \"{presetId}\",\n  \"dataVersion\": 1,\n  \"displayName\": \"Podcast Title\",\n  \"feedUrlHints\": [\"https:\u002F\u002Fanchor.fm\u002Fs\u002F{id}\u002Fpodcast\u002Frss\"],\n  \"playlistCount\": 2\n}\n```\n\nAdd exactly one entry per preset. `feedUrlHints` lists every URL in the preset's `feedUrls`, in the same order (the editor writes it this way). The app treats the preset as a candidate if any hint is a substring of the podcast's feed URL. `audiflow-editor validate` fails if an `id` appears twice.\n\n### Step 6: Write a DESIGN.md\n\nCreate `patterns\u002F{patternId}\u002FDESIGN.md` documenting:\n- Feed characteristics (title format variations, metadata gaps)\n- Playlist breakdown (what each captures and why)\n- Episode identification strategy (primary patterns, fallbacks)\n- Grouping decisions (why this structure, catch-all strategy)\n\nThis helps future maintainers understand the reasoning behind regex choices.\n\n### Step 7: Preview in browser\n\nAfter writing the pattern files, launch the editor web UI so the user can see the actual grouping result with live RSS data:\n\n```bash\n# Start the editor in the background (downloads binary automatically)\nnohup .\u002Feditor.sh > \u002Fdev\u002Fnull 2>&1 &\nEDITOR_PID=$!\n```\n\nTell the user: \"Editor is running at http:\u002F\u002Flocalhost:8080 -- open it to preview your pattern with live feed data. Let me know when you're done and I'll stop the server.\"\n\nWhen the user is done previewing:\n```bash\nkill $EDITOR_PID 2>\u002Fdev\u002Fnull\n```\n\nIf the preview reveals issues (wrong grouping, mismatched regex, episodes in the wrong playlist), fix the JSON files and reload the browser -- the editor picks up file changes.\n\nThe preview step is optional but recommended for new patterns. Skip it if the user just wants to validate and commit.\n\n### Step 8: Validate\n\n```bash\nschema\u002Fscripts\u002Fvalidate.sh patterns\u002F\n```\n\nThis downloads the `audiflow-editor` binary (matching `schema\u002FVERSION`) and runs validation. Fix any errors before committing.\n\n---\n\n## Workflow: Updating an Existing Pattern\n\n1. Read the existing pattern files and DESIGN.md\n2. Understand what needs to change (new playlist, updated groups, filter tweaks)\n3. If modifying regex patterns, analyze sample titles first -- run `analyze-feed.py` to see current episode titles\n4. Make changes, keeping the DESIGN.md in sync\n5. Preview with `.\u002Feditor.sh` to verify grouping looks correct in the browser (optional)\n6. Validate with `schema\u002Fscripts\u002Fvalidate.sh patterns\u002F`\n7. Bump `dataVersion` is handled by CI (`audiflow-editor bump-versions`), but for local testing set it manually\n\n---\n\n## Editor CLI Commands\n\nThe `audiflow-editor` binary is downloaded automatically by the scripts in `schema\u002Fscripts\u002F`.\n\n| Command | Usage | Purpose |\n|---------|-------|---------|\n| `validate` | `audiflow-editor validate patterns\u002F` | Validates all JSON against schemas |\n| `bump-versions` | `audiflow-editor bump-versions HEAD~1` | Auto-increment dataVersion for changed files |\n| `--version` | `audiflow-editor --version` | Show binary version |\n\nSchema version is tracked in `schema\u002FVERSION` (currently v4). The binary is fetched from `audiflow\u002Faudiflow-preset-editor` GitHub releases, cached in `.cache\u002Fbin\u002F{version}\u002F`.\n\n---\n\n## Title Pattern Conventions\n\n### Japanese podcasts\n\nCommon patterns to look for:\n- Full-width brackets: `【content】` -- very common for categorization\n- Numbered brackets: `【{season}-{episode}】` -- structured numbering\n- Date brackets: `【{month}月{day}日】` -- daily\u002Fweekly shows\n- Series markers: `{series}編`, `{topic}シリーズ`\n- Special markers: `番外編`, `特別編`, `ゲスト回`\n- Counter words: `第{n}回`, `第{n}話`\n\n### English podcasts\n\n- `S{n}E{n}` or `Season {n}, Episode {n}` -- standard season\u002Fepisode\n- `{Series Title} - {Episode}` or `{Series Title} | {Episode}` -- delimiter-based\n- `Ep. {n}:`, `Episode {n}:`, `#{n}` -- episode numbering\n- `Best of`, `Encore:`, `Replay:` -- rerun markers (often filtered out)\n- `Bonus:`, `Extra:`, `Special:` -- special content markers\n\n### Writing robust regex patterns\n\n- Escape special regex characters in literal text\n- Use `\\\\s*` or `\\\\s+` for flexible whitespace\n- Use non-greedy quantifiers (`+?`, `*?`) to avoid over-matching\n- For Japanese brackets, match both half-width `[]` and full-width `【】`\n- Test patterns against the sample titles from `analyze-feed.py`\n- Remember: patterns in episodeFilters and groups are case-insensitive\n\n---\n\n## Common Pitfalls\n\n1. **Forgetting the catch-all group**: titleClassifier groups without a catch-all will leave unmatched episodes ungrouped. Always add a pattern-less group at the end.\n\n2. **Pattern order matters**: Groups evaluate first-to-last. Put narrow patterns before broad ones, or episodes get claimed by the wrong group.\n\n3. **One root entry per preset**: A podcast that moved feeds still gets one root index entry. Put both URLs in its `feedUrlHints`; a second entry with the same `id` fails validation.\n\n4. **Missing `id` match**: The `id` in meta.json must match the directory name. The `id` in playlist JSON must match the filename.\n\n5. **episodeFilters logic**: `require` is AND across rules, `exclude` is OR. A definition with both `require` and `exclude` first includes matching episodes, then removes excluded ones.\n\n6. **titleExtractor on titleClassifier**: titleClassifier ignores the top-level titleExtractor. Group names come from GroupDef.displayName. But `episodeList.titleExtractor` still works for transforming episode titles within groups.\n\n7. **Confusing the two titleExtractors**: The top-level `titleExtractor` generates **group titles** (season\u002Fseries names). The `episodeList.titleExtractor` transforms **episode display names**. Putting an episode-title pattern (like `「(.+?)」`) at the top level produces nonsensical group names. When the podcast has no named arcs, use `\"source\": \"seasonNumber\"` with a template for clean group labels.\n",{"name":5,"description":7},"main","b7e0da46ded8a26032723b55c146dbbf3ae67bf8",".claude\u002Fskills\u002Faudiflow-preset","https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset","https:\u002F\u002Faudiflow.github.io\u002Faudiflow-preset\u002F","https:\u002F\u002Fgithub.com\u002Faudiflow","2026-11-12 13:17:43",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add audiflow\u002Faudiflow-preset --skill audiflow-preset",{"tool":56,"command":57},"bunx","bunx skills add audiflow\u002Faudiflow-preset --skill audiflow-preset",{"tool":59,"command":60},"pnpm","pnpm dlx skills add audiflow\u002Faudiflow-preset --skill audiflow-preset","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faudiflow-preset\u002FSKILL.md",true,[64,70,76],{"path":65,"resourceType":66,"resourceTypeDesc":67,"sizeBytes":68,"sourceHtmlUrl":69},"references\u002Fschema-reference.md","REFERENCE","参考文档",24627,"https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faudiflow-preset\u002Freferences\u002Fschema-reference.md",{"path":71,"resourceType":72,"resourceTypeDesc":73,"sizeBytes":74,"sourceHtmlUrl":75},"scripts\u002Fanalyze-feed.py","SCRIPT","脚本",21377,"https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faudiflow-preset\u002Fscripts\u002Fanalyze-feed.py",{"path":77,"resourceType":72,"resourceTypeDesc":73,"sizeBytes":78,"sourceHtmlUrl":79},"scripts\u002Fsearch-feeds.py",2513,"https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faudiflow-preset\u002Fscripts\u002Fsearch-feeds.py",[81,88],{"skillNo":82,"name":83,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":38},"01M6CJ3H1V25NS02C2PS4X0KKH","audiflow-playlist","https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset\u002Fblob\u002Fmain\u002F.cursor\u002Fskills\u002Faudiflow-playlist\u002FSKILL.md",[],[87],"audiflow playlist",{"skillNo":89,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":92,"publishedAt":38},"01M6CJ3BT3K1VW8MR0FBVYG4V4","https:\u002F\u002Fgithub.com\u002Faudiflow\u002Faudiflow-preset\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Faudiflow-preset\u002FSKILL.md",[],[19,18]]