Metadata-Version: 2.4
Name: beets-pysacd
Version: 0.1.0
Summary: beets plugin: import SACD ISO images through pysacd
License-Expression: GPL-2.0-or-later
Project-URL: Homepage, https://github.com/aclater/beets-pysacd
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: beets<3,>=2.12
Requires-Dist: pysacd>=0.2
Provides-Extra: dev
Requires-Dist: pytest==9.1.1; extra == "dev"
Requires-Dist: ruff==0.17.0; extra == "dev"
Requires-Dist: mypy==2.4.0; extra == "dev"
Dynamic: license-file

# beets-pysacd

A [beets](https://beets.io) plugin that imports SACD ISO images, using
[pysacd](https://github.com/aclater/pysacd).

beets can't tag an `.iso`. For each disc image this plugin reads the TOC,
finds the MusicBrainz release and the medium (the SACD stereo or multichannel
layer) that the disc is, converts each selected area to DSF or FLAC in a
staging directory, and imports that through beets' normal importer. Each area
becomes an album of its own, on its own medium of the release.

## Install

```sh
pip install beets-pysacd
```

Then enable the plugin, with beets' MusicBrainz plugin, in your beets config:

```yaml
plugins: musicbrainz sacd
```

Requirements: Python 3.11 or later, beets 2.12 or later (2.x), pysacd 0.2 or
later (its engine, and libFLAC for `format: flac`).

## Use

```sh
beet sacd-import Album.iso              # one image
beet sacd-import ~/rips/                # every .iso under a directory
beet sacd-import -a both -f flac Album.iso
beet sacd-import --release 1d1f0a6e-... Album.iso   # use this MusicBrainz release
beet sacd-info Album.iso                # print the TOC; changes nothing
beet sacd-info ~/rips/                  # ...of every image under a directory
```

Plain `beet import` also converts the ISO images it finds (an `.iso` given on
the command line, or inside a directory being imported) before importing them.
Your other files in the same run are imported exactly as your settings say.
Turn this off with `auto: no`.

An area that is already in the library is skipped. Imported albums carry two
flexible attributes, `sacd_iso_sha` (a fingerprint of the disc's TOCs) and
`sacd_area` (`stereo` or `multichannel`), so `beet ls sacd_area:multichannel`
lists the multichannel albums.

### DVD-Audio, DVD-Video and Blu-ray images

Disc-image folders often hold DVD and Blu-ray images next to SACDs. The plugin
identifies them and lists them, but doesn't convert them: `beet sacd-info`
prints one line for each, and imports log why each was left alone.

```text
== .../Bluray/LIVE_CONCERT.iso
  Blu-ray image, volume 'LIVE_CONCERT', 4 playlists: catalogued, not converted
== .../DVDA/SURROUND_MIX.iso
  DVD-Audio + DVD-Video image, volume 'SURROUND_MIX', 5 title sets: catalogued, not converted
```

It reads the image's file system (UDF, including the UDF 2.50 metadata
partition that Blu-ray uses, or ISO 9660) and looks for the standard folders:
`AUDIO_TS` with `AUDIO_TS.IFO` for DVD-Audio, `VIDEO_TS` for DVD-Video and
`BDMV` for Blu-ray. The count is of DVD title sets or Blu-ray playlists.
This lives in the plugin (`beetsplug/_sacd_discimage.py`), not in pysacd, and
reads only directory structures, never the audio.

## Configuration

```yaml
sacd:
    areas: stereo        # stereo | multichannel | both
    format: dsf          # dsf | flac (24-bit PCM)
    pcm_rate: 88200      # flac: 88200 | 176400
    auto: yes            # convert ISOs found by plain `beet import`
    remove_iso: no       # delete an ISO once its import is verified
    staging: ''          # where to convert; default: a hidden directory in the library directory
    musicbrainz: yes     # let pysacd find the release (barcode, catalogue number, ISRCs)
    contact: ''          # contact (e.g. an e-mail address) for pysacd's MusicBrainz User-Agent
    threads: 0           # conversion threads; 0 = one per CPU
```

`sacd-import` options override `areas`, `format`, `pcm_rate` and `remove_iso`
for one run.

### Keeping or removing ISO images

ISO images are kept by default, and never written to. With `remove_iso: yes`
(or `--remove-iso`) an image is deleted only after every selected area has
been imported and verified: an album with that disc's fingerprint and area
exists in the library, with all of the area's tracks, and every file exists.
An import you skip, or one that fails, keeps the image.

### Files and staging

Converted files always move from the staging directory into your library,
whatever `copy`, `move` or in-place settings you use for other files: the
staging directory is temporary and is deleted afterwards, also after an error.
By default it is created inside the library directory, so the move is a
rename on the same filesystem; set `staging` to put it elsewhere.

## Hybrid SACDs

MusicBrainz lists a hybrid SACD as one release with several media: the CD
layer, the SACD stereo layer and sometimes the SACD multichannel layer, all
with the same recordings. beets puts the tracks of every medium into one
candidate and then matches files to tracks. With the same recordings on every
layer, it maps the SACD files to the CD layer (medium 1), overwrites their
release track IDs, and writes the album-level `media` as "Media". See the
[beets discussion](https://github.com/beetbox/beets/discussions/6614).

This plugin keeps each area on its own medium:

- When pysacd matched the release, it knows the medium: that release's
  candidates are cut down to that medium before beets compares them, and the
  album's `media` is that medium's format, e.g. "Hybrid SACD (SACD layer,
  2 channels)".
- Without pysacd's match, the medium is the one SACD-layer medium of the
  candidate with the area's track count and channel type. If that is
  ambiguous (a set of two SACDs, say), the candidate is left as beets made it.
- A final check after you choose a match maps the files to the medium if
  they still point elsewhere, and logs it.

## How it works

For each ISO image, and each selected area of it:

1. pysacd reads the TOC (read-only) and fingerprints the disc from its TOCs.
   An area whose fingerprint and area are already in the library is skipped.
2. pysacd matches the MusicBrainz release and medium, and converts the area
   into the staging directory, writing the release's IDs into the files.
   A match on a medium that isn't an SACD layer (a vinyl, CD or digital
   edition sharing the catalogue number or barcode) isn't used: the area is
   converted without IDs and beets matches it the normal way. A match on the
   CD layer of a hybrid release keeps the release; the area goes to that
   release's SACD layer with its track count and channels.
3. beets imports the staging directory. Because the files agree on the
   release ID, beets looks that release up directly. For this one import task
   only, the plugin then:
   - **hides the physical-disc tags from matching.** pysacd tags the physical
     disc ("disc 1 of 1", media "Hybrid SACD"), but MusicBrainz counts each
     layer as a medium ("disc 2 of 3", "Hybrid SACD (SACD layer, 2
     channels)"). beets would count those tags against the right candidate,
     enough to keep a correct match from being applied automatically. beets
     writes the medium's own values when it applies the match;
   - **cuts the release's candidates down to the area's medium** (see
     [Hybrid SACDs](#hybrid-sacds));
   - **counts only an album on the same medium as a duplicate.** beets' default
     duplicate keys are album artist and album, which both areas of a disc
     (and a CD rip of the same release) share: without this, importing the
     multichannel area after the stereo one would be skipped as a duplicate.
     Another copy of the same layer is still a duplicate;
   - **moves the files** into the library (see [Files and staging](#files-and-staging)).
4. The album gets `sacd_iso_sha` and `sacd_area`; with `remove_iso`, the ISO is
   deleted once every area is verified. The staging directory is removed.

Nothing here changes how beets treats your other files in the same import.

## Development

```sh
pip install -e '.[dev]'
ruff check . && mypy beetsplug
PYSACD_TESTS=/path/to/pysacd/tests pytest -q
```

The tests build synthetic SACD images with pysacd's test generator
(`tests/sacdgen.py` in the pysacd source tree, found through `PYSACD_TESTS`,
default `../pysacd/tests`) and import them into a throwaway beets library.
MusicBrainz is mocked with a hybrid release whose media share recordings, so
the tests need no network. DVD and Blu-ray images for the cataloguing tests
come from `tests/discgen.py`, which writes minimal UDF and ISO 9660 file
systems.

## Licence

GPL-2.0-or-later (see [LICENSE](LICENSE)). The plugin imports pysacd, which is
GPL-2.0-or-later.
