Metadata-Version: 2.3
Name: pyofsDB
Version: 0.1.0
Summary: Read-only access to the SQLite databases produced by OF-Scraper
Author: Jakan
Requires-Python: >=3.12,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: aiosqlite (>=0.21.0)
Requires-Dist: greenlet (>=3.5.0)
Requires-Dist: sqlalchemy (>=2.0.49)
Project-URL: Homepage, https://git.jakan.co/Jakan/pyofsDB
Description-Content-Type: text/markdown

# pyofsDB

Read-only access to the SQLite databases produced by [OF-Scraper](https://github.com/datawhores/OF-Scraper).

Extracted from [pyofscraperstash](https://git.jakan.co/Jakan/pyofscraperstash) so more than one consumer can share it. The databases are treated as strictly read-only — this package never writes to them.

## What it gives you

- **Discovery** — find creator databases from a save location and metadata template, optionally filtered by creator or modification date.
- **Loading** — copy a database into memory via SQLite's backup API before querying, which matters when the files live on NFS.
- **Schema detection** — OF-Scraper's schema varies by the version that created each file; `SchemaDetector` reports the `schema_flags` a given database carries.
- **Models** — SQLAlchemy models for `medias`, `labels`, `profiles`, `models`, and the five content tables (`posts`, `stories`, `messages`, `products`, `others`).
- **Queries** — read-only helpers over those models.

## Usage

```python
import asyncio
from pyofsdb import DatabaseLoader, get_labels_by_post, get_profile_by_username


async def main() -> None:
    loader = DatabaseLoader(save_location="/data/OnlyFans")

    # Discover every creator database, or load one path directly
    for path in await loader.find_database_files():
        model_db = await loader.load_database(path)
        session = model_db["session"]

        profile = await get_profile_by_username(session, model_db["model_username"])
        if profile:
            labels = await get_labels_by_post(session, profile.user_id)
            print(model_db["model_username"], len(labels))

        await loader.dispose_model(path)

    await loader.close()


asyncio.run(main())
```

`DatabaseLoader` takes its configuration as arguments rather than reading a global:

| Argument | Meaning |
| --- | --- |
| `save_location` | Root directory OF-Scraper writes to; substituted for `{save_location}` |
| `metadata_format` | Template for a creator's metadata directory. Defaults to `{save_location}/meta/OnlyFans/{model_username}/Metadata`. A template without `{model_username}` denotes a combined database |
| `models` | Restrict discovery to these creator usernames; omit to discover all |

## Schema notes

OF-Scraper's schema differs between the versions that wrote each file. Measured across 78 production databases:

- `posts` appears in **six** distinct column shapes.
- `labels`, `models`, and `profiles` are **absent entirely** from some databases.
- `medias` may lack `posted_at`, `duration`, `unlocked`, `hash`, and `model_id`; the models mark these `deferred` so they are not selected unless accessed.
- `alembic_version` is **not** a usable discriminator — most databases have no row, and those that do carry several different versions. Use `schema_flags`, which `load_database` returns.

`profiles` has no `model_id` column in any known database. The creator's OF id lives in `profiles.user_id`, and carries the same value as `models.model_id`.

## Logging

This package logs through the standard library `logging` module so the consuming application keeps control of handlers. Applications using loguru can forward records with loguru's `InterceptHandler` pattern.

## Requirements

Python >=3.12, plus `sqlalchemy`, `aiosqlite`, and `greenlet`.

