Metadata-Version: 2.4
Name: smfetch
Version: 1.5.0
Summary: Web interface for downloading simfiles from Zenius and StepManiaOnline
Author-email: Raeed Ahmed <code@raeed.me>
Requires-Python: <4,>=3.10
Requires-Dist: beautifulsoup4<5,>=4.12.3
Requires-Dist: fastapi>=0.115.0
Requires-Dist: loguru<0.8,>=0.7.2
Requires-Dist: requests<3,>=2.31.0
Requires-Dist: urllib3<3,>=2.0
Requires-Dist: uvicorn[standard]>=0.30.0
Description-Content-Type: text/markdown

# smfetch

Web interface for downloading simfiles from [Zenius-i-Vanisher](https://zenius-i-vanisher.com) and [StepManiaOnline](https://stepmaniaonline.net).

## Usage

```bash
usage: smfetch [-h] [-p PORT]

smfetch web interface

options:
  -h, --help       show this help message and exit
  -p, --port PORT  port for the web interface (default: 8095)
```

Starts a web interface at `http://localhost:<port>`.

## Features

### Downloads

Both sites are downloaded from the same way. A transfer that gives out partway
is resumed from where it stopped rather than started over, and one that keeps
dropping is retried for as long as it keeps gaining ground — a pack whose
connection breaks every few hundred megabytes still arrives. A server that is
merely busy is waited out and asked again; one that says the archive is not
there is believed the first time.

A download either lands or fails, and a job says which. Nothing partial is left
on disk, and nothing that failed is recorded as installed — an error page
served in place of an archive is a failure, not an empty chart.

Quitting is prompt whatever is in flight: downloads are told to stop and give
up where they are, rather than being waited out. A pack with gigabytes still to
fetch does not hold the process open, and neither does a backoff with minutes
left to run.

### Update tracking

smfetch remembers which packs you have installed and when, then watches Zenius'
[latest official](https://zenius-i-vanisher.com/v5.2/simfiles.php?category=latest-official)
and [latest user](https://zenius-i-vanisher.com/v5.2/simfiles.php?category=latest-user)
listings for changes to those packs. When a chart is revised or added, it is
downloaded **individually into the pack it belongs to** — no re-downloading a
whole pack for one chart.

- **Review before applying.** Detections collect in the Updates view. You choose
  what to pull and when; nothing downloads on its own.
- **One entry per chart.** If a chart changes several times between applies it
  stays a single pending item that advances to the newest version, so applying
  fetches the latest revision once. If a chart changes again *while* its
  download is running it stays pending for the next pass, rather than being
  silently marked current. A download that fails stays pending too, with the
  error against it, so a chart is never marked current at a version that is not
  on disk.
- **Charts a pack download missed.** A Zenius pack with no downloadable archive
  is fetched a chart at a time, and any chart that does not arrive is queued as
  pending instead of being inventoried as installed. What is missing is visible
  in the Updates view and one click from being fetched again.
- **Packs you added yourself.** A pack folder copied into the songs directory is
  picked up on its own — at startup, when you open Installed Packs, and on every
  check — and listed as `local`. Nothing is guessed about where it came from, so
  it stays out of update checking until you link it. A folder with no song
  folders in it yet is left alone, so a copy still in progress is not adopted
  half-finished.
- **Linking to Zenius.** Any pack without a category can be matched to one by
  name, from the Installed Packs view or a scan. Charts already on disk are
  treated as current, so only changes published after linking are reported.
- **StepManiaOnline packs too.** The two sites carry many of the same packs, so
  a pack downloaded from StepManiaOnline can be linked to its Zenius
  counterpart and updated from there. Linking records the origin separately, so
  the pack still shows where it came from. Song folders are matched by name
  when linking — `attitude` on disk against `Attitude` on Zenius — so an update
  replaces the song already installed instead of dropping a second copy beside
  it. A chart the pack does not have yet arrives under its Zenius title.

Update checking joins on a Zenius pack, so a pack with no link is never
checked. The Installed Packs view marks those `unlinked` and offers to link
them.

Checks run on a configurable interval (6 hours by default) and on demand. The
user listing only reaches back about a week, so an interval of a day or less is
worthwhile if you follow user packs.

### Zenius-i-Vanisher
- **Song search** — search by song name and/or artist, select and download individual charts
- **Pack search** — filter official or user packs, download selected packs
- **Bundles** — one-click download of arcade, spinoff, or all official packs

### StepManiaOnline
- **Title search** — search packs that contain a chart with a song title
- **Artist search** — search packs that contain a chart by an artist
- **Pack search** — filter all packs by name

## Interface

The UI is dependency-free HTML, CSS and JavaScript served straight from the
package — no build step, and no network fetches at runtime. It follows your
system light/dark preference and has a manual toggle.

| Key | Action |
|---|---|
| `/` | Focus the current view's search box |
| `J` | Toggle the downloads panel |
| `U` | Jump to Updates |
| `Esc` | Close any panel or dialog |

## Storage Defaults

| | Config | Songs | Logs |
|---|---|---|---|
| **Linux** | `~/.config/smfetch/` | `~/.local/share/smfetch/` | `~/.local/state/smfetch/logs/` |
| **macOS** | `~/Library/Application Support/smfetch/` | `~/Music/StepMania Songs/` | `~/Library/Logs/smfetch/` |
| **Windows** | `%APPDATA%/smfetch/` | `~/Music/StepMania Songs/` | `%LOCALAPPDATA%/smfetch/logs/` |

Installed packs and pending updates live in `library.json` inside the config
directory. Deleting it only loses update history and your Zenius links — nothing
on disk is touched, and your packs are picked up again from the songs directory
on the next start.

## Development

```bash
make run      # start the app
make format   # ruff format
make check    # ruff lint + ty
make test     # pytest
make install  # clean, format, check, sync, test
```

The test suite is fully offline and takes well under a second. Scrapers run
against trimmed captures of real Zenius pages in `tests/fixtures/`, so changes
to the parsing are checked against the markup the site actually serves; the rest
runs against a scriptable fake site with Zenius' clock pinned, and any
unexpected request fails the test rather than reaching the network. Config,
songs and library state are redirected to a temporary directory, so running the
tests never touches your real collection.

## Notes on upstream timestamps

Zenius reports ages relative to its own clock ("2.6 days ago") and exact times
only on an individual simfile's page. smfetch derives the site's clock offset
from a recent simfile so relative ages resolve to absolute times, screens the
listings cheaply with those, and confirms anything that looks changed against
the exact timestamp before recording it. Rounded ages carry real slop — a value
in weeks is only good to a few hours — so comparisons allow a tolerance scaled
to the unit, which is what keeps unchanged charts from reappearing as updates.
