Disabled obsolete CI
All checks were successful
Publish Metadata Agent Image (dev) / build-and-push-image (push) Successful in 1m17s
Publish Web Player Image (dev) / build-and-push-image (push) Successful in 1m14s

This commit is contained in:
2026-03-20 00:49:27 +00:00
parent 56760be586
commit 8d70a5133a
8 changed files with 0 additions and 0 deletions

87
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,87 @@
# Furumi-ng: Distributed Virtual File System
## Project Overview
This document describes the architectural vision and technical requirements for a modern, high-performance distributed virtual file system built in Rust. The system allows a remote client to connect to a server with authorization, list directories recursively, and mount the remote directory as a local virtual file system. It is designed as a faster, more reliable, and modern alternative to WebDAV.
## Core Requirements
1. **Cross-Platform Support:** Initial support for Linux and macOS. The backend server and client mounting logic must be logically separated so that new OS support (e.g., Windows) can be added solely by writing a new mount layer without altering the core backend or client networking code.
2. **Read-Only First:** The initial implementation will support read-only operations, with an architecture designed to easily accommodate write operations in subsequent phases.
3. **Memory Safety & Reliability:** The entire stack (server, shared client core, and mount layers) will be implemented in **Rust** to leverage its strict compiler guarantees, memory safety, and high-performance asynchronous ecosystem.
## High-Level Architecture
The architecture is divided into three main components: Server, Shared Client Core, and OS-Specific Mount Layers.
### 1. Transport & Protocol (gRPC)
- **Protocol:** gRPC over HTTP/2.
- **Why gRPC:** Provides strong typing via Protobuf, multiplexing, and robust streaming capabilities which are essential for transferring large file chunks efficiently.
- **Security:** Requires TLS (e.g., mTLS or JWT via metadata headers) to secure data in transit.
### 2. Server (Linux Backend)
The server role is to expose local directories to authorized clients safely and asynchronously.
- **Runtime:** `tokio` for non-blocking I/O.
- **Security Validation:** Strict path sanitization (protection against Path Traversal). The server restricts clients strictly to their allowed document root.
- **VFS Abstraction:** Backend logic will be abstracted behind a Rust trait. This allows future swapping of the storage backend (e.g., Local Disk -> AWS S3, or In-Memory for testing) without changing the gRPC transport layer.
### 3. Client Architecture
To maximize code reuse and maintainability, the client is split into two layers:
#### A. Shared Client Core (Cross-Platform)
A Rust library containing all OS-agnostic logic:
- **Network Client:** Handles gRPC connections, request retries, backoff strategies, and error handling.
- **VFS Cache:** An in-memory cache for metadata (TTL-based) to dramatically reduce network latency for high-frequency `stat` / `getattr` calls generated by file managers or terminals.
- **VFS Translator:** Maps VFS operations into remote gRPC RPC calls.
#### B. OS-Specific Mount Layer
Thin executable wrappers that consume the Shared Client Core and handle OS integration:
- **Linux:** Uses the `fuser` crate (binds to `libfuse`) to mount and handle events from `/dev/fuse`.
- **macOS:** Acts as a lightweight local NFSv3/v4 server (`nfssrv` or similar crate). The system natively mounts `localhost:/` via the built-in NFS client, avoiding any need for third-party kernel extensions (like `macFUSE`) or complex FileProvider bindings.
- **Windows (Future):** Will wrap libraries like `WinFSP` or `dokany` to integrate with the Windows internal VFS.
## API Design (Read-Only Foundation)
The initial Protobuf specification will involve core remote procedure calls (RPCs) to support read-only mode:
```protobuf
syntax = "proto3";
package virtualfs;
message PathRequest {
string path = 1;
}
message AttrResponse {
uint64 size = 1;
uint32 mode = 2; // Permissions and file type
uint64 mtime = 3; // Modification time
// ... other standard stat attributes
}
message DirEntry {
string name = 1;
uint32 type = 2; // File or Directory
}
message ReadRequest {
string path = 1;
uint64 offset = 2;
uint32 size = 3;
}
message FileChunk {
bytes data = 1;
}
service RemoteFileSystem {
// Get file or directory attributes (size, permissions, timestamps). Maps to stat/getattr.
rpc GetAttr (PathRequest) returns (AttrResponse);
// List directory contents. Uses Server Streaming to handle massively large directories efficiently.
rpc ReadDir (PathRequest) returns (stream DirEntry);
// Read chunks of a file. Uses Server Streaming for efficient chunk delivery based on offset/size.
rpc ReadFile (ReadRequest) returns (stream FileChunk);
}
```
## Future Expansion: Write Operations
The design ensures seamless expansion to a read-write file system. Future RPCs such as `CreateFile`, `MkDir`, `Remove`, `Rename`, and `WriteChunk` (utilizing Client Streaming or Bi-directional Streaming in gRPC) can be added without restructuring the foundational architecture.

216
docs/CHIPTUNE.md Normal file
View File

@@ -0,0 +1,216 @@
# Chiptune Support Implementation Plan
## Overview
Add playback support for tracker/chiptune module formats (MOD, XM, S3M, IT, MPTM) to the
Furumi web player. The implementation consists of two parts:
1. **Server-side** — lightweight metadata parser in pure Rust (zero external dependencies)
2. **Client-side** — playback via libopenmpt WebAssembly using AudioWorklet API
## Supported Formats
| Format | Extension | Origin |
|--------|-----------|--------|
| MOD | `.mod` | Amiga ProTracker |
| XM | `.xm` | FastTracker II |
| S3M | `.s3m` | Scream Tracker 3 |
| IT | `.it` | Impulse Tracker |
| MPTM | `.mptm` | OpenMPT |
## Part 1: Server-Side Metadata Parser
### Rationale
libopenmpt must NOT be a server dependency. All tracker formats store metadata at fixed byte
offsets in their headers, making manual parsing trivial. Reading the first ~400 bytes of a file
is sufficient to extract all available metadata.
### Extracted Fields
- **Title** — song name embedded in the module header
- **Channels** — number of active audio channels
- **Patterns** — number of unique patterns in the module
- **Message** — song message/comment (IT/MPTM only)
Note: none of these formats have a dedicated "artist" field. Author information, when present,
is typically found in the IT/MPTM song message.
### Binary Format Reference
#### MOD
| Offset | Size | Field |
|--------|------|-------|
| 0 | 20 | Song title (space/null padded) |
| 952 | 128 | Pattern order table |
| 1080 | 4 | Signature (determines channel count) |
Channel count is derived from the 4-byte signature at offset 1080:
- `M.K.`, `M!K!`, `FLT4`, `4CHN` → 4 channels
- `6CHN` → 6, `8CHN` / `OCTA` → 8
- `xCHN` → x channels, `xxCH` → xx channels
Pattern count = max value in the order table (128 bytes at offset 952) + 1.
#### XM
All multi-byte values are little-endian.
| Offset | Size | Field |
|--------|------|-------|
| 0 | 17 | Magic: `"Extended Module: "` |
| 17 | 20 | Module name |
| 58 | 2 | Version number |
| 68 | 2 | Number of channels |
| 70 | 2 | Number of patterns |
#### S3M
| Offset | Size | Field |
|--------|------|-------|
| 0x00 | 28 | Song title (null-terminated) |
| 0x1C | 1 | Signature byte (`0x1A`) |
| 0x24 | 2 | Pattern count (LE u16) |
| 0x2C | 4 | Magic: `"SCRM"` |
| 0x40 | 32 | Channel settings |
Channel count = number of entries in channel settings (32 bytes) that are not `0xFF`.
#### IT
| Offset | Size | Field |
|--------|------|-------|
| 0x00 | 4 | Magic: `"IMPM"` |
| 0x04 | 26 | Song title (null-terminated) |
| 0x26 | 2 | Pattern count (LE u16) |
| 0x2E | 2 | Special flags (bit 0 = message attached) |
| 0x36 | 2 | Message length |
| 0x38 | 4 | Message file offset |
| 0x40 | 64 | Channel panning table |
Channel count = number of entries in channel panning (64 bytes) with value < 128.
Song message: if `special & 1`, read `message_length` bytes from `message_offset`. Uses `\r`
(0x0D) as line separator.
#### MPTM
Parsed identically to IT. Detection:
- Legacy: magic `tpm.` instead of `IMPM`
- Modern: magic `IMPM` with tracker version (offset 0x28) in range `0x0889..=0x0FFF`
### Integration Points
- **`browse.rs`** — add tracker extensions to the audio file whitelist
- **`meta.rs`** — add a chiptune metadata branch that runs before Symphonia (which does not
support tracker formats); return title, channel count, pattern count, and message
- **`stream.rs`** — serve tracker files as-is (no server-side transcoding); these files are
typically under 1 MB
### Implementation Notes
- Zero external crate dependencies — only `std::io::Read` + `std::io::Seek`
- Read at most the first 1084 bytes for header parsing (MOD needs offset 1080 + 4 byte sig)
- For IT/MPTM messages, a second seek to `message_offset` is needed
- All strings should be trimmed of null bytes and trailing whitespace
- Expected code size: ~200300 lines of Rust
## Part 2: Client-Side Playback via libopenmpt WASM
### Rationale
Browsers cannot decode tracker formats natively. libopenmpt compiled to WebAssembly decodes
modules into PCM samples which are then rendered through the Web Audio API. Client-side
decoding keeps the server dependency-free and enables interactive features (pattern display,
channel visualization) in the future.
### libopenmpt WASM Source
Use the **chiptune3** library (npm: `chiptune3`, by DrSnuggles) which bundles libopenmpt as a
self-contained AudioWorklet-compatible ES6 module.
Package contents:
| File | Size | Purpose |
|------|------|---------|
| `chiptune3.js` | ~4 KB | Main API (load, play, pause, seek) |
| `chiptune3.worklet.js` | ~12 KB | AudioWorklet processor glue |
| `libopenmpt.worklet.js` | ~1.7 MB | libopenmpt WASM + JS (single-file bundle) |
Available via jsDelivr CDN or can be vendored into the project.
If a newer libopenmpt version is needed, the official project provides source tarballs with an
Emscripten build target:
```
make CONFIG=emscripten EMSCRIPTEN_TARGET=audioworkletprocessor
```
This produces a single ES6 module with WASM embedded inline (`SINGLE_FILE=1`), which is
required because AudioWorklet contexts cannot fetch separate `.wasm` files.
### Playback Architecture
```
┌──────────────────────────────────────────────────────────┐
│ player.html │
│ │
│ Format detection (by file extension) │
│ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ Standard audio │ │ Tracker module │ │
│ │ (mp3/flac/ogg/...) │ │ (mod/xm/s3m/it/mptm) │ │
│ │ │ │ │ │
│ │ <audio> element │ │ fetch() → ArrayBuffer │ │
│ │ src=/api/stream/path │ │ ↓ │ │
│ │ │ │ libopenmpt WASM decode │ │
│ │ │ │ ↓ │ │
│ │ │ │ AudioWorkletProcessor │ │
│ │ │ │ ↓ │ │
│ │ ↓ │ │ AudioContext.destination │ │
│ └────────┼─────────────┘ └────────────┼───────────────┘ │
│ └──────────┬──────────────────┘ │
│ ↓ │
│ Player controls │
│ (play/pause/seek/volume) │
│ MediaSession API │
└──────────────────────────────────────────────────────────┘
```
### Integration Points
- **`player.html`** — detect tracker format by extension; use chiptune3 API instead of
`<audio>` element for tracker files; unify transport controls (play/pause/seek/volume)
across both playback engines
- **WASM assets** — serve `chiptune3.js`, `chiptune3.worklet.js`, and
`libopenmpt.worklet.js` via a static file endpoint or embed them inline
- **`mod.rs`** (routes) — add endpoint for serving WASM assets if not embedded
### Player Integration Details
The player must abstract over two playback backends behind a common interface:
```
play(path) — start playback (auto-detect engine by extension)
pause() — pause current playback
resume() — resume current playback
seek(seconds) — seek to position
setVolume(v) — set volume (0.01.0)
getDuration() — total duration in seconds
getPosition() — current position in seconds
isPlaying() — playback state
onEnded(cb) — callback when track finishes
```
For tracker modules, `getDuration()` and `getPosition()` are provided by libopenmpt's
`get_duration_seconds()` and `get_position_seconds()` APIs.
### Considerations
- Tracker files are small (typically < 1 MB) — fetch the entire file before playback; no
streaming/range-request needed
- AudioWorklet requires a secure context (HTTPS or localhost)
- The WASM bundle is ~1.7 MB — load it lazily on first tracker file playback
- MediaSession API metadata should display module title from `/api/meta` response

214
docs/PLAYER-API.md Normal file
View File

@@ -0,0 +1,214 @@
# Furumi Web Player API
Base URL: `http://<host>:<port>/api`
All endpoints require authentication when `--token` is set (via cookie `furumi_token=<token>` or query param `?token=<token>`).
All entity references use **slugs** — 12-character hex identifiers (not sequential IDs).
## Artists
### `GET /api/artists`
List all artists that have at least one track.
**Response:**
```json
[
{
"slug": "a1b2c3d4e5f6",
"name": "Pink Floyd",
"album_count": 5,
"track_count": 42
}
]
```
Sorted alphabetically by name.
### `GET /api/artists/:slug`
Get artist details.
**Response:**
```json
{
"slug": "a1b2c3d4e5f6",
"name": "Pink Floyd"
}
```
**Errors:** `404` if not found.
### `GET /api/artists/:slug/albums`
List all albums by an artist.
**Response:**
```json
[
{
"slug": "b2c3d4e5f6a7",
"name": "Wish You Were Here",
"year": 1975,
"track_count": 5,
"has_cover": true
}
]
```
Sorted by year (nulls last), then name.
### `GET /api/artists/:slug/tracks`
List all tracks by an artist across all albums.
**Response:** same as album tracks (see below).
Sorted by album year, album name, track number, title.
## Albums
### `GET /api/albums/:slug`
List all tracks in an album.
**Response:**
```json
[
{
"slug": "c3d4e5f6a7b8",
"title": "Have a Cigar",
"track_number": 3,
"duration_secs": 312.5,
"artist_name": "Pink Floyd",
"album_name": "Wish You Were Here",
"album_slug": "b2c3d4e5f6a7",
"genre": "Progressive Rock"
}
]
```
Sorted by track number (nulls last), then title. Fields `album_name`, `album_slug` may be `null` for tracks without an album.
### `GET /api/albums/:slug/cover`
Serve the album cover image from the `album_images` table.
**Response:** Binary image data with appropriate `Content-Type` (`image/jpeg`, `image/png`, etc.) and `Cache-Control: public, max-age=86400`.
**Errors:** `404` if no cover exists.
## Tracks
### `GET /api/tracks/:slug`
Get full track details.
**Response:**
```json
{
"slug": "c3d4e5f6a7b8",
"title": "Have a Cigar",
"track_number": 3,
"duration_secs": 312.5,
"genre": "Progressive Rock",
"storage_path": "/music/storage/Pink Floyd/Wish You Were Here/03 - Have a Cigar.flac",
"artist_name": "Pink Floyd",
"artist_slug": "a1b2c3d4e5f6",
"album_name": "Wish You Were Here",
"album_slug": "b2c3d4e5f6a7",
"album_year": 1975
}
```
**Errors:** `404` if not found.
### `GET /api/tracks/:slug/cover`
Serve cover art for a specific track. Resolution order:
1. Album cover from `album_images` table (if the track belongs to an album with a cover)
2. Embedded cover art extracted from the audio file metadata (ID3/Vorbis/etc. via Symphonia)
3. `404` if no cover art is available
**Response:** Binary image data with `Content-Type` and `Cache-Control: public, max-age=86400`.
**Errors:** `404` if no cover art found.
## Streaming
### `GET /api/stream/:slug`
Stream the audio file for a track.
Supports HTTP **Range requests** for seeking:
- Full response: `200 OK` with `Content-Length` and `Accept-Ranges: bytes`
- Partial response: `206 Partial Content` with `Content-Range`
- Invalid range: `416 Range Not Satisfiable`
`Content-Type` is determined by the file extension (e.g. `audio/flac`, `audio/mpeg`).
**Errors:** `404` if track or file not found.
## Search
### `GET /api/search?q=<query>&limit=<n>`
Search across artists, albums, and tracks by name (case-insensitive substring match).
| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `q` | yes | — | Search query |
| `limit` | no | 20 | Max results |
**Response:**
```json
[
{
"result_type": "artist",
"slug": "a1b2c3d4e5f6",
"name": "Pink Floyd",
"detail": null
},
{
"result_type": "album",
"slug": "b2c3d4e5f6a7",
"name": "Wish You Were Here",
"detail": "Pink Floyd"
},
{
"result_type": "track",
"slug": "c3d4e5f6a7b8",
"name": "Have a Cigar",
"detail": "Pink Floyd"
}
]
```
`detail` contains the artist name for albums and tracks, `null` for artists.
Sorted by result type (artist → album → track), then by name.
## Authentication
When `--token` / `FURUMI_PLAYER_TOKEN` is set:
- **Cookie:** `furumi_token=<token>` — set after login
- **Query parameter:** `?token=<token>` — redirects to player and sets cookie
When token is empty, authentication is disabled and all endpoints are public.
Unauthenticated requests receive `401 Unauthorized` with a login form.
## Error format
All errors return JSON:
```json
{
"error": "description of the error"
}
```
With appropriate HTTP status code (`400`, `404`, `500`, etc.).

View File

@@ -0,0 +1,56 @@
# Implementation Plan for `furumi-mount-windows` Client
## Architectural Decision
- **VFS Driver:** `WinFSP` (Windows File System Proxy).
- **Justification:** Excellent performance, perfect compatibility with the FUSE model, widely used in similar projects (e.g., rclone, sshfs-win).
- **Installation:** A unified installer (bundle) will be created (for example, using Inno Setup or WiX Toolkit), which will:
- Check if WinFSP is already installed.
- Automatically install the official `winfsp.msi` silently (using `/qn` flags) if the driver is missing.
- Install the `furumi-mount-windows.exe` client itself.
---
## Implementation Details
### 1. Application Scaffold
- Create a new binary crate `furumi-mount-windows` within the workspace.
- Add dependencies: `winfsp` (or `wfd`), `tokio`, `clap`, `tracing`, and an internal dependency on `furumi-client-core`.
### 2. Entry Point (CLI)
- In `main.rs`, configure parsing for command-line arguments and environment variables (`--server`, `--token`, `--mount`), similar to `furumi-mount-macos`.
- Initialize the gRPC connection to the server via `furumi-client-core`.
- Configure directory mounting:
- As a network drive (e.g., `Z:`).
- Or as a transparent folder within an existing NTFS filesystem (depending on driver support/flags).
### 3. VFS Implementation
- Create an `fs.rs` module.
- Implement the trait or callback structure required by WinFSP (e.g., the `WinFspFileSystem` structure).
- Action mapping:
- `GetFileInfo` / `GetSecurityByName` → gRPC `GetAttr` call.
- `ReadDirectory` → Streaming gRPC `ReadDir` call.
- `ReadFile``ReadFile` gRPC call (with support for stream chunking).
- **Crucial Part:** Translating Unix file attributes (from gRPC) into Windows File Attributes to ensure the system permits high-performance continuous stream reading (especially for media).
### 4. Installer Creation
- Write a configuration script for a Windows installer builder (e.g., `windows/setup.iss` for Inno Setup).
- Neatly bundle both `winfsp-x.y.z.msi` and `furumi-mount-windows.exe` together.
- Add Custom Actions / Logic to:
- Check the Windows Registry for an existing WinFSP installation.
- Trigger the `winfsp.msi` installation conditionally.
### 5. CI/CD Integration
- Update the GitHub Actions workflow (`docker-publish.yml` or create a dedicated release workflow).
- Add the target toolchain: `x86_64-pc-windows-msvc`.
- Add a step to compile: `cargo build --release --bin furumi-mount-windows`.
- Add a step to build the installer (e.g., `iscc setup.iss` or via `cargo-wix`).
- Output the final `setup.exe` as a GitHub Release artifact alongside other binaries.
### 6. Testing Strategy
- Write unit tests in Rust covering attribute translation and path mapping (mapping slashes `/` to backslashes `\`).
- Manual System Testing:
- Start `furumi-server` locally.
- Run the installer on a clean Windows machine (VM without pre-installed WinFSP).
- Verify that the drive mounts correctly and seamlessly.
- Launch media playback (e.g., via VLC/mpv) to ensure streaming stability over the VFS connection.