Metadata-Version: 2.4
Name: beets-bcgenre
Version: 0.1.0
Summary: Beets plugin that fetches genre tags from Bandcamp release pages
Author-email: JayDawgThaGOAT <25489150+JayDawgThaGOAT@users.noreply.github.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/JayDawgThaGOAT/bcgenre
Project-URL: Repository, https://github.com/JayDawgThaGOAT/bcgenre
Project-URL: Issues, https://github.com/JayDawgThaGOAT/bcgenre/issues
Keywords: beets,bandcamp,genre,music,metadata
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Text Processing :: General
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: beets>=2.0
Requires-Dist: beetcamp>=0.20
Requires-Dist: httpx>=0.27
Requires-Dist: PyYAML>=6.0

# bcgenre

A [beets](https://beets.io/) plugin that fetches genre tags from Bandcamp release pages and merges them into your library’s `genres` field.

It is meant to complement [`lastgenre`](https://beets.readthedocs.io/en/stable/plugins/lastgenre.html): Last.fm often misses artist-authored Bandcamp tags (and Bandcamp’s hyphenated forms like `hip-hop` / `neo-soul`). `bcgenre` reads those tags from the release page, normalizes them with lastgenre’s helpers, and filters them against a MusicBrainz genre whitelist.

## Requirements

- [beets](https://beets.io/) (includes the `lastgenre` package used for whitelist / aliases / canonicalization data)
- [beetcamp](https://github.com/snejus/beetcamp) (`bandcamp` plugin) for Bandcamp HTML parsing and slug helpers
- A Bandcamp URL in each item’s `comments` field (artist root or album URL), e.g. `Visit https://artist.bandcamp.com`

## Install

Clone or copy this package so beets can see `beetsplug/bcgenre`:

```text
beetsplug/
  bcgenre/
    __init__.py
    genres.py
    resolve.py
    README.md
```

Point `pluginpath` at the parent of that `beetsplug` directory (or at the `beetsplug` directory itself — both work with beets), then enable the plugin:

```yaml
plugins: lastgenre bandcamp bcgenre
pluginpath: /path/to/parent   # directory that contains beetsplug/
```

Confirm it loads:

```bash
beet bcgenre -h
```

## Configuration

Options live under `bcgenre:` and mirror lastgenre’s shared settings (they do **not** inherit from your `lastgenre:` block):

```yaml
bcgenre:
    auto: no
    force: no
    keep_existing: no
    count: 1
    whitelist: yes          # yes = lastgenre's genres.txt; or a path to your own list
    aliases: yes            # yes = lastgenre's aliases.yaml; no = off; or an inline mapping
    canonical: no           # yes = add parent genres from lastgenre's genres-tree.yaml
    title_case: yes
    fallback:               # optional genre when Bandcamp yields nothing
    pretend: no
```

| Option | Meaning |
| --- | --- |
| `force` | Modify items that already have genres |
| `keep_existing` | When forcing, union Bandcamp genres with existing ones |
| `count` | Max genres kept from Bandcamp after filtering (union with `keep_existing` can exceed this) |
| `whitelist` | Genre allow-list; `yes` uses lastgenre’s bundled MusicBrainz list |
| `aliases` | Spelling normalization (`hip-hop` → `hip hop`, etc.) |
| `canonical` | Expand matched tags with parent genres from the genre tree |
| `auto` | Run during import (off by default; prefer batch runs) |
| `title_case` | Title-case written genres |

File writes follow your global `import.write` setting (same as `lastgenre`).

## Usage

```bash
# Preview changes (no DB / file writes)
beet bcgenre -p

# Apply (uses config force / keep_existing)
beet bcgenre

# Force + replace existing genres
beet bcgenre -f -K

# Force + union with existing genres
beet bcgenre -f -k

# Limit to a query (albums by default)
beet bcgenre -p albumartist:Aesop\ Rock
beet bcgenre -A artist:Aesop\ Rock    # items instead of albums
```

CLI flags match lastgenre: `-p/--pretend`, `-f/-F` (force), `-k/-K` (keep-existing), `-a/-A` (albums/items).

Output shows the same per-track `genres:` `+` / `-` diffs as `lastgenre`.

## How URL resolution works

For each album, bcgenre:

1. Reuses a cached `bandcamp_album_url` flex field if present
2. Uses a deep `/album/...` (or `/track/...`) URL from `comments` when available
3. Otherwise takes the Bandcamp root from `comments` and guesses `/album/{slug}`
4. On miss, scrapes `{root}/music` and fuzzy-matches the album title (handles Bandcamp’s `-2` slug collisions)

Resolved URLs are stored in `bandcamp_album_url` so re-runs avoid most network work.

Requests are paced at about 1 per second (not configurable), with backoff and abort on repeated HTTP 429 responses.

## Tips

- Prefer a pretend pass first: `beet bcgenre -p -f -k`
- Albums without a Bandcamp URL in `comments` are skipped
- Translated or heavily renamed titles may not match Bandcamp’s discography listing; set `bandcamp_album_url` manually or restore a closer title
- `lastgenre` does not need to be enabled for bcgenre to use its data files, but enabling both is the usual setup
