Metadata-Version: 2.3
Name: sub2pod
Version: 0.4.0
Summary: Convert ytdl-sub output directories into podcast RSS feeds.
Author: Marvin8
Author-email: Marvin8 <marvin8@tuta.io>
License: AGPL-3.0-or-later
Requires-Dist: cyclopts~=4.23.0
Requires-Dist: lxml~=6.1.1
Requires-Python: >=3.12
Project-URL: Issues, https://forge.marvin8.zone/marvin8/sub2pod/issues
Project-URL: Source, https://forge.marvin8.zone/marvin8/sub2pod
Project-URL: Changelog, https://forge.marvin8.zone/marvin8/sub2pod/raw/branch/main/CHANGELOG.md
Description-Content-Type: text/markdown

# sub2pod

[![Licence: AGPL-3.0-or-later](https://img.shields.io/badge/licence-AGPL--3.0--or--later-blue)](LICENSE.md)
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/)
[![Docs](https://img.shields.io/badge/docs-sub2pod.marvin8.zone-blue)](https://sub2pod.marvin8.zone/latest/)
[![CI](https://forge.marvin8.zone/marvin8/sub2pod/actions/workflows/ci.yml/badge.svg)](https://forge.marvin8.zone/marvin8/sub2pod/actions)
[![Downloads](https://img.shields.io/pepy/dt/sub2pod)](https://pepy.tech/project/sub2pod)
[![gitleaks](https://img.shields.io/badge/gitleaks-checked-green)](https://github.com/gitleaks/gitleaks)
[![pysentry](https://img.shields.io/badge/pysentry-checked-green)](https://pysentry.com)
[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
[![complexipy](https://img.shields.io/badge/complexipy-checked-blue)](https://github.com/rohaquinlop/complexipy)
[![Codestyle: Ruff](https://img.shields.io/badge/codestyle-ruff-green)](https://docs.astral.sh/ruff/)
[![AI-DECLARATION: copilot](https://img.shields.io/badge/%E4%B7%BC%20AI--DECLARATION-copilot-fee2e2?labelColor=fee2e2)](https://ai-declaration.md)

Convert [ytdl-sub](https://github.com/jmbannon/ytdl-sub) output directories into
standards-compliant podcast RSS feeds. Point it at a directory full of downloaded
audio, and it writes `feed.xml` into each channel subdirectory — ready to serve or
submit to podcast directories.

## What it does

- Reads Kodi `tvshow.nfo` and per-episode `.nfo` sidecars produced by ytdl-sub
- Discovers date-prefixed audio files (`YYYY-MM-DD - <title>.<ext>`) in each channel directory
- Generates RSS 2.0 + iTunes namespace `feed.xml` per channel
- Emits correct MIME types for `.opus`, `.mp3`, and `.m4a` enclosures
- Produces an `opml.xml` index at the root level for one-click feed import
- Falls back to filename parsing when `.nfo` sidecars are absent
- Emits Podcast 2.0 `<podcast:chapters>` for episodes with chapter markers (via the `.info.json` sidecar)

## What it does not do

- No download or subscription management (that's ytdl-sub's job)
- No web server — feed files are static XML; serve them however you like
- No podcast client or player

## Installation

### From PyPI

```bash
pip install sub2pod
```

### From source

```bash
git clone https://forge.marvin8.zone/marvin8/sub2pod.git
cd sub2pod
uv sync
uv run sub2pod --help
```

## Usage

```bash
sub2pod /path/to/ytdl-sub/output https://podcast.example.com
```

This walks the parent directory looking for subdirectories containing `tvshow.nfo`
and generates:

| Output | Location | Description |
|--------|----------|-------------|
| `feed.xml` | Each channel subdirectory | RSS 2.0 + iTunes podcast feed |
| `opml.xml` | Parent directory root | OPML index of all feeds |

### Options

```
--dry-run    Print paths without writing any files
--verbose    Log per-episode details during generation
```

### Example

```bash
# After ytdl-sub downloads new episodes:
sub2pod /home/NAS/ytdl-sub/mark/podcast https://podcasts.example.com

# Preview without writing:
sub2pod /home/NAS/ytdl-sub/mark/podcast https://podcasts.example.com --dry-run

# See what's being generated:
sub2pod /home/NAS/ytdl-sub/mark/podcast https://podcasts.example.com --verbose
```

## How it works

1. **Channel discovery** — Walks the parent directory for subdirectories containing `tvshow.nfo`
2. **Metadata parsing** — Reads channel title and genre from `tvshow.nfo`; reads per-episode title, description, date, runtime, and source URL from `.nfo` sidecars
3. **Episode discovery** — Finds all date-prefixed audio files, pairs them with their `.nfo` sidecars, sorts newest-first
4. **Feed generation** — Builds RSS 2.0 XML with iTunes podcast namespace elements (`<itunes:image>`, `<itunes:duration>`, `<itunes:category>`), writes `feed.xml`
5. **OPML index** — Writes `opml.xml` at the root listing every channel feed

### Input format

Each channel directory is expected to contain:

```
Links with Friends/
├── tvshow.nfo              ← Channel metadata (title, genre)
├── poster.jpg              ← Podcast artwork → <itunes:image>
├── 2026-05-19 - Episode Title.opus   ← Audio file
├── 2026-05-19 - Episode Title.nfo    ← Episode metadata sidecar
├── 2026-05-19 - Episode Title.info.json  ← yt-dlp metadata sidecar (chapters source, optional)
└── 2026-05-19 - Episode Title-thumb.jpg  ← Episode thumbnail
```

## Example feed

A trimmed `feed.xml` for a channel with one episode:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
  <channel>
    <title>Links with Friends</title>
    <link>https://podcasts.example.com</link>
    <description>Links with Friends</description>
    <language>en</language>
    <itunes:image href="https://podcasts.example.com/Links%20with%20Friends/poster.jpg"/>
    <item>
      <title>Episode Title</title>
      <enclosure url="https://podcasts.example.com/Links%20with%20Friends/2026-05-19%20-%20Episode%20Title.opus" type="audio/ogg; codecs=opus" length="12345678"/>
      <guid isPermaLink="false">https://podcasts.example.com/Links%20with%20Friends/2026-05-19%20-%20Episode%20Title.opus</guid>
      <pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>
```

## Troubleshooting

- **No `feed.xml` is generated** — sub2pod only processes subdirectories that contain a `tvshow.nfo`; ensure each channel directory has one.
- **Malformed `base_url`** — sub2pod requires an `http://`/`https://` URL with a host; invalid values fail with an actionable error.
- **Episodes won't play in Apple Podcasts** — `.opus` is unsupported by Apple; re-encode to `.mp3`/`.m4a` (see the [integration guide](docs/ytdl-sub-integration.md#opus-mime-type-caveat-and-re-encode-workaround)).
- **Episode metadata missing** — without a `.nfo` sidecar, sub2pod falls back to parsing the filename; malformed sidecars are skipped with a warning.

## Documentation

- [ytdl-sub Integration Guide](docs/ytdl-sub-integration.md) — Configuring ytdl-sub
  to produce sub2pod-compatible output: NFO tags, episode thumbnails, trigger
  strategies, and Opus MIME type guidance.

## Licence

Copyright (C) 2026 Marvin8

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
