Metadata-Version: 2.4
Name: offshore-podcast
Version: 0.1.1
Summary: A local-first podcast feed generator and editor.
Project-URL: Homepage, https://offshore.pressed.press
Project-URL: Repository, https://git.sr.ht/~lamons/offshore
Project-URL: Issues, https://todo.sr.ht/~lamons/offshore
Author: tianshi
License: MIT
License-File: LICENSE
Keywords: feed,generator,podcast,rss
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Multimedia :: Sound/Audio
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.111
Requires-Dist: feedparser>=6.0.11
Requires-Dist: httpx>=0.27
Requires-Dist: jinja2>=3.1
Requires-Dist: langcodes>=3.4
Requires-Dist: markdown-it-py>=3.0
Requires-Dist: markdownify>=0.11
Requires-Dist: mutagen>=1.47
Requires-Dist: platformdirs>=4.2
Requires-Dist: pydantic[email]>=2.6
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn>=0.30
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Provides-Extra: image
Requires-Dist: pillow>=10.0; extra == 'image'
Description-Content-Type: text/markdown

# offshore

[Homepage](https://offshore.pressed.press) ･ [Repository](https://sr.ht/~lamons/offshore) ･ [PyPI package](https://pypi.org/project/offshore-podcast/) ･ [Tickets](https://todo.sr.ht/~lamons/offshore)

Local-first podcast feed generator and editor. Manage metadata locally and generate feed from assets hosted anywhere.

Conventional podcast hosts like Fireside or Transistor work fine, but they can be expensive for small shows, and handing all your data to a single service is a real dependency. Self-hosted alternatives exist, but most are still designed as full-stack replacements that bundles frontend website, audio storage, and feed generation into one thing.

A podcast feed is just a plain-text XML file. Hosting a handful of audio files and a text file is much easier, and cheaper than running a media server with a web frontend and database. And if you don’t want the website that comes with a podcast host then that part just becomes redundant.

`offshore` aims to be a more basic solution. It simply helps you manages your show metadata locally and generates feed you can host wherever makes sense — a static site, an object storage, a CDN. Audio files and any other asset live wherever you want to host them. `offshore` ensures your metadata and assets comply with podcast standards, but otherwise stays out of the way. It can be used as a local feed generator, or reliably on a server to serve the feed directly.

## Installation

Python 3.11+.

```bash
pip install offshore-podcast
```

Or from source:

```bash
pip install -e .
```

## Data layout

Shows are stored in a dedicated directory outside the project, defaulting to `$XDG_DATA_HOME` or `$HOME`. On first run you will be prompted to confirm or change this.

```
~/offshore/              # default shows root
  <slug>/
    <slug>.json          # show + episodes
    <slug>.lock.json     # feed stability lock 
    podcast.xml          # generated
    rss.xsl              # generated
```

Each show lives in its own subdirectory. The JSON contains all show metadata and episodes. The lock file records each episode's GUID and pub_date to catch accidental edits that would reorder the feed for existing subscribers.

To inspect or change the configured root:

```bash
offshore config                          # show current setting
offshore config --shows-dir ~/mypodcasts # update it
```

## Usage

### Web UI

```bash
offshore serve
```

Opens at `http://127.0.0.1:8765`. On first run with no shows, the landing page offers three options: **New**, **Import**, or **Open** an existing file.

All CLI commands are also available from the UI.

### CLI

**New show:**
```bash
offshore init <slug>
```

**Import from an RSS feed URL or an offshore JSON file:**
```bash
offshore import <url-or-path> --slug <slug> --feed-self-url <public-feed-url>
```

**Validate:**
```bash
offshore check            # run checks (structural + remote assets)
offshore check --no-remote  # skip network checks
```

**Probe audio metadata:**
```bash
offshore probe --all      # all episodes
offshore probe <guid>     # single episode
```

Probing downloads only the first 256 KB of each file to read the header. Results are cached in the episode JSON.

**Generate feed:**
```bash
offshore generate
offshore generate --force           # skip check failures
offshore generate --output-dir <path>  # write to a different directory
```

Output goes to `<shows-root>/<slug>/podcast.xml` by default. Falls back to the current directory if the shows root is not writable.

**Republish an episode** (changes pub_date, updates lock):
```bash
offshore republish <guid> --date <ISO8601>
```

**Open a specific show file:**
```bash
offshore serve --show /path/to/<slug>.json
```

## Serving the feed

`shows/<slug>/` is a static directory. Point nginx (or any web server) at it:

```nginx
server {
    listen 80;
    server_name your.domain.com;
    root /path/to/shows/my-show;

    location /podcast.xml {
        add_header Content-Type "application/rss+xml; charset=utf-8";
    }
}
```

Make sure `feed_self_url` in the show JSON matches the public URL you serve the feed at.

To redirect feed output to a shared web root:

```bash
offshore generate --output-dir /var/www/html
offshore serve --output-dir /var/www/html
```

`offshore serve` can also be exposed directly behind a reverse proxy (`--host 0.0.0.0`). Each show's feed is available at `/download/<slug>/podcast.xml`.

## Audio

MP3 (`audio/mpeg`) and M4A/AAC (`audio/mp4`, `audio/aac`) are accepted. Format is verified when an episode is saved. If the URL is unreachable at save time you can skip verification and probe later.

## Checks

`offshore check` runs before every generate. Fail-level findings block generation; `--force` overrides. Checks cover:

- All required fields on the show and each episode
- Audio probe cache present (duration + byte count are required for a valid enclosure)
- Audio and image URLs reachable with the correct MIME type
- Episode pub_date is not in the future
- All person roster references resolve
- Episode GUID and pub_date match the lock file

## Development

`tests/` includes a sample feed and a group of scripts to validate the output for development.