Metadata-Version: 2.4
Name: shazamio_core
Version: 2.0.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
License-File: LICENSE
License-File: THIRD-PARTY-NOTICES.md
Summary: Audio fingerprinting for Shazam, written in Rust and exposed to Python
Author-email: dotX12 <dev@shitposting.team>
Maintainer-email: Danipulok <danipulok@gmail.com>
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Issues, https://github.com/shazamio/shazamio-core/issues
Project-URL: Repository, https://github.com/shazamio/shazamio-core

# shazamio-core

Audio fingerprinting for Shazam, written in Rust and exposed to Python.

It turns an audio file, or its bytes, into the signature Shazam's endpoint accepts. It does not talk to Shazam itself: [ShazamIO](https://github.com/shazamio/ShazamIO) is the client that sends the signature and reads the answer back.

## Install

```sh
pip install shazamio-core
```

Or with [uv](https://docs.astral.sh/uv/):

```sh
uv add shazamio-core
```

Python 3.10 and newer. Prebuilt wheels:

| Platform                                     | CPython 3.10+ (`abi3`) | PyPy 3.11 |
|----------------------------------------------|------------------------|-----------|
| Linux `x86_64`, `aarch64` (`manylinux_2_28`) | yes                    | yes       |
| Linux `x86_64`, `aarch64` (`musllinux_1_2`)  | yes                    | no        |
| macOS `x86_64` (10.12+), `arm64` (11.0+)     | yes                    | no        |
| Windows `win_amd64`, `win32`                 | yes                    | no        |

Everything else builds from the source distribution, which needs a Rust toolchain 1.87 or newer, CMake and a C compiler. `libopus` is built from vendored sources rather than linked against a system copy, so without CMake the build stops on `is 'cmake' not installed?`.

## Usage

Both entry points return an `asyncio.Future` rather than a coroutine, so they need a running event loop at the call and the work starts there rather than at the `await`. `await`, `asyncio.ensure_future` and `asyncio.gather` all accept what they return; `asyncio.create_task` takes a coroutine alone and rejects it. Cancelling one drops its result but not its work: the decode runs to the end on a worker thread, and later calls are unaffected.

```python
import asyncio
from pathlib import Path

from shazamio_core import Recognizer


async def main() -> None:
    recognizer = Recognizer()

    from_path = await recognizer.recognize_path("track.mp3")
    from_bytes = await recognizer.recognize_bytes(Path("track.mp3").read_bytes())

    print(from_path.signature.uri == from_bytes.signature.uri)


asyncio.run(main())
```

`recognize_path` accepts a `str` or an `os.PathLike[str]`. A `__fspath__` returning `bytes` is rejected.

### What comes back

Both return a `Signature`:

| Field               | Meaning                                                                                       |
|---------------------|-----------------------------------------------------------------------------------------------|
| `signature.uri`     | the fingerprint itself, base64 inside a `data:audio/vnd.shazam.sig` URI                       |
| `signature.samples` | duration of the analysed segment in milliseconds                                              |
| `timestamp`         | when the signature was produced, in epoch milliseconds                                        |
| `timezone`          | a fixed value the request envelope carries. It is not read from the machine and means nothing |

Every field above is read only: assigning to one raises `AttributeError`. Neither class can be built by hand: `Signature()` raises `TypeError`.

The URI is the part a client sends on:

```
data:audio/vnd.shazam.sig;base64,gCX+ypQoAnWcBQAAAJwRlAAAAAA...
```

### How much audio is analysed

Ten seconds by default, taken from the middle of the file. A file shorter than the segment is used whole. Audio is converted to mono and downsampled to 16 kHz before analysis, whatever it started as.

Set it per recognizer, or per call:

```python
import asyncio

from shazamio_core import Recognizer, SearchParams


async def main() -> None:
    recognizer = Recognizer(segment_duration_seconds=5)

    signature = await recognizer.recognize_path(
        "track.mp3",
        SearchParams(segment_duration_seconds=15),
    )

    print(signature.signature.samples)


asyncio.run(main())
```

`SearchParams` wins where both are given. The duration must be at least 1; zero
raises `ValueError`. A value at or above the length of the file analyses it whole,
whatever the value.

### Errors

Anything the decoder cannot use raises `SignatureError`, and the message opens with the input that failed: the path for `recognize_path`, `the byte payload` for `recognize_bytes`.

| Given                                                | Message                                                                                         |
|------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| a path that is not there                             | `<input>: No such file or directory (os error 2)`                                               |
| a stream nothing in this build reads, a directory included | `<input>: unsupported feature: no reader in this build recognises the stream`              |
| a container that is read, holding a codec that is not | `<input>: unsupported feature: the stream carries no track with a codec this build can decode`  |
| a recording cut short                                | `<input>: unexpected end of file`                                                               |

A recording cut short is refused rather than fingerprinted from what is left, with three exceptions. MP4 and Matroska fail before decoding starts and report that no reader recognises the stream. MP3 and MP2 decode up to the cut and are not refused, because a run of MPEG frames does not reliably say how long it is. A chained Ogg file cut inside a later link loses that link and is not refused either, which is a bug in `symphonia`.

What follows the input is `symphonia`'s wording, or the operating system's, and differs between platforms.

An argument of the wrong type raises `TypeError` instead, and raises it at the call rather than on the `await`, so nothing is scheduled.

A panic inside the extension is a bug here rather than bad input. It raises `RustPanic`, an `Exception` subclass the package does not export, on the `await`, with the panic message after `rust future panicked: `.

```python
import asyncio

from shazamio_core import Recognizer, SignatureError


async def main() -> None:
    try:
        await Recognizer().recognize_path("not-audio.txt")
    except SignatureError as error:
        print(error)


asyncio.run(main())
```

## Formats

Decoding goes through [`symphonia`](https://github.com/pdeljanov/Symphonia) with every codec and container it ships enabled, and resampling to mono 16 kHz through [`rubato`](https://github.com/HEnquist/rubato). Opus is the one codec `symphonia` has no decoder for, so it goes through [`libopus`](https://github.com/SpaceManiac/opus-rs), which is compiled into the wheel rather than loaded from the system. Nothing is shelled out to, so no external binary has to be installed.

That leaves these codecs, each one probed through the public API in the container named beside it:

| Codec  | Probed in      | In the test suite |
|--------|----------------|-------------------|
| AAC    | ADTS           | yes               |
| ADPCM  | WAV            | yes               |
| ALAC   | MP4            | yes               |
| FLAC   | FLAC           | yes               |
| MP1    | not probed     |                   |
| MP2    | MPEG           | yes               |
| MP3    | MPEG           | yes               |
| Opus   | Ogg            | yes               |
| PCM    | WAV, AIFF, CAF | yes               |
| Vorbis | Ogg            | yes               |

MP1 is the one row taken from what `symphonia` registers rather than from a run: nothing here encodes it. The containers recognised are ADTS, AIFF, CAF, FLAC, Matroska and WebM, MP4, MPEG, Ogg and WAV, and the test suite runs on Linux, macOS and Windows.

Anything else raises `SignatureError`, and a container from that list is no guarantee: what has to be decodable is the codec inside it. The two refusals differ in how far the file gets:

| Refused          | Fails as                                        | Why                                     |
|------------------|-------------------------------------------------|-----------------------------------------|
| AC-3 in Matroska | `no track with a codec this build can decode`   | the container is read, the codec is not |
| WMA in ASF       | `no reader in this build recognises the stream` | there is no ASF demuxer at all          |

Windows Media Audio decoded in earlier releases through an `ffmpeg` fallback that has since been removed.

## Development

Setting up a checkout, what to run before a pull request and how commits are shaped: [`CONTRIBUTING.md`](CONTRIBUTING.md).

## License

MIT. See [LICENSE](LICENSE).

The wheel statically links its Rust dependencies, so the terms in
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) apply to it as well. That file
says how it is generated; `just licenses-check` fails once a dependency change
has left it behind.

