Files
MusicFS/docs/v1/data-flow.md
T
Alexander 1374084135 Reorganize docs into v1 (beetfs) and v2 (new architecture)
docs/v1/ - Original beetfs documentation:
  - analysis.md, components.md, data-flow.md, drawbacks.md
  - features.md, modernization.md, rust-migration.md
  - benchmark-plan.md, benchmark-results.md, e2e-test-plan.md
  - README.md

docs/v2/ - New MusicFS architecture:
  - requirements.md: Full requirements spec (FR-1 to FR-25, NFR-1 to NFR-14)
    - P0: Multi-origin, plugins, CAS, control API
    - P1: Search, album art, prefetch, metadata sources
    - P3: HA, 10M+ files scalability
  - architecture.md: Google BlueDoc style design document
    - PlantUML diagrams for all components
    - Design requirements with quantitative targets
    - Alternatives considered, implementation plan
2026-05-12 16:46:37 +02:00

413 lines
37 KiB
Markdown

# beetfs Data Flow
## Overview
This document details the complete data flow for read and write operations in beetfs.
---
## 1. Initialization Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ beet mount /mountpoint │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ mount(lib, config, opts, args) │
│ │
│ 1. Parse PATH_FORMAT into structure_split │
│ PATH_FORMAT = "$artist/$album ($year) [$format_upper]/..." │
│ structure_split = ["$artist", "$album ($year) [$format_upper]", ...] │
│ structure_depth = 3 │
│ │
│ 2. Store global library reference │
│ library = lib │
│ │
│ 3. Create empty virtual directory tree │
│ directory_structure = FSNode({}, {}) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ for item in lib.items(): │
│ │
│ For each item in beets library: │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Build template mapping │ │
│ │ mapping = { │ │
│ │ 'artist': 'Pink Floyd', │ │
│ │ 'album': 'The Wall', │ │
│ │ 'year': '1979', │ │
│ │ 'format_upper': 'FLAC', │ │
│ │ 'track': '01', │ │
│ │ 'title': 'In The Flesh?', │ │
│ │ } │ │
│ │ │ │
│ │ 2. Substitute template for each level │ │
│ │ level_subbed[0] = "Pink Floyd" │ │
│ │ level_subbed[1] = "The Wall (1979) [FLAC]" │ │
│ │ level_subbed[2] = "01 - Pink Floyd - In The Flesh?.flac" │ │
│ │ │ │
│ │ 3. Add directories to tree │ │
│ │ directory_structure.adddir([], "Pink Floyd") │ │
│ │ directory_structure.adddir(["Pink Floyd"], "The Wall (1979)...") │ │
│ │ │ │
│ │ 4. Add file entry (filename → item.id) │ │
│ │ directory_structure.addfile( │ │
│ │ ["Pink Floyd", "The Wall (1979) [FLAC]"], │ │
│ │ "01 - Pink Floyd - In The Flesh?.flac", │ │
│ │ item.id # e.g., 42 │ │
│ │ ) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ beetFileSystem FUSE Server │
│ │
│ server = beetFileSystem(...) │
│ server.multithreaded = 0 │
│ server.main() ← Enters FUSE event loop │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 2. File Open Flow
```
Application: open("/mount/Pink Floyd/The Wall (1979) [FLAC]/01 - Pink Floyd - In The Flesh?.flac")
┌─────────────────────────────────────────────────────────────────────────────┐
│ beetFileSystem.open(path, flags) │
│ Lines 988-1021 │
│ │
│ path = "/Pink Floyd/The Wall (1979) [FLAC]/01 - Pink Floyd - In The..." │
│ flags = os.O_RDONLY (or O_RDWR) │
│ │
│ if path in self.files: │
│ # File already open - increment reference count │
│ self.files[path].open() │
│ return self.files[path] │
│ else: │
│ # Create new FileHandler │
│ self.files[path] = FileHandler(path, self.lib) │
│ return self.files[path] │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ FileHandler.__init__(path, lib) │
│ Lines 440-483 │
│ │
│ Step 1: Resolve virtual path to beets item │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ pathsplit = ["Pink Floyd", "The Wall (1979) [FLAC]", │ │
│ │ "01 - Pink Floyd - In The Flesh?.flac"] │ │
│ │ │ │
│ │ # Navigate to parent directory in virtual tree │ │
│ │ node = directory_structure.getnode(pathsplit[0:2]) │ │
│ │ # node.files = {"01 - Pink Floyd - In The Flesh?.flac": 42, ...} │ │
│ │ │ │
│ │ # Get beets item by ID │ │
│ │ item_id = node.files[pathsplit[2]] # 42 │ │
│ │ self.item = lib.get_item(id=42) │ │
│ │ self.real_path = self.item.path │ │
│ │ # e.g., "/mnt/music/torrents/pink_floyd_wall.flac" │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: Open real file and detect format │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ self.file_object = open(self.real_path, 'r+') │ │
│ │ self.format = "flac" # from file extension │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 3: Create InterpolatedFLAC with database metadata │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ self.inf = InterpolatedFLAC(self.file_object.read()) │ │
│ │ │ │
│ │ # INJECT DATABASE METADATA (this is the key operation!) │ │
│ │ self.inf["title"] = self.item.title # "In The Flesh?" │ │
│ │ self.inf["album"] = self.item.album # "The Wall" │ │
│ │ self.inf["artist"] = self.item.artist # "Pink Floyd" │ │
│ │ self.inf["genre"] = self.item.genre # "Progressive Rock" │ │
│ │ │ │
│ │ # Generate header with injected metadata │ │
│ │ self.header = self.inf.get_header(self.real_path) │ │
│ │ self.bound = len(self.header) # e.g., 8192 bytes │ │
│ │ self.music_offset = self.inf.offset() │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 4: Cache audio data │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ self.file_object.seek(self.music_offset) │ │
│ │ self.music_data = self.file_object.read() # All audio data │ │
│ │ self.file_object.close() │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 3. File Read Flow
```
Application: read(fd, buffer, 4096) # offset managed by kernel
┌─────────────────────────────────────────────────────────────────────────────┐
│ beetFileSystem.read(path, size, offset, fh) │
│ Lines 1077-1106 │
│ │
│ path = "/Pink Floyd/The Wall (1979) [FLAC]/01 - ..." │
│ size = 4096 │
│ offset = 0 (first read) or previous offset + bytes_read │
│ fh = FileHandler instance │
│ │
│ return self.files[path].read(size, offset) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ FileHandler.read(size, offset) │
│ Lines 497-517 │
│ │
│ Variables: │
│ self.bound = 8192 (header size) │
│ self.header = bytes (generated FLAC header with DB metadata) │
│ self.music_data = bytes (original audio frames) │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Case 1: Header Only │ │ Case 2: Span Both │ │ Case 3: Audio Only │
│ offset < bound │ │ offset < bound │ │ offset >= bound │
│ offset+size < bound │ │ offset+size >= bound│ │ │
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
│ Example: │ │ Example: │ │ Example: │
│ offset=0 │ │ offset=8000 │ │ offset=10000 │
│ size=4096 │ │ size=4096 │ │ size=4096 │
│ bound=8192 │ │ bound=8192 │ │ bound=8192 │
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
│ Return: │ │ Return: │ │ Return: │
│ header[0:4096] │ │ header[8000:8192] │ │ music_data[ │
│ │ │ + music_data[0:3904]│ │ 1808:5904] │
│ (DB metadata!) │ │ │ │ │
│ │ │ (mixed) │ │ (original audio) │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
Visual representation of virtual file:
0 bound (8192) EOF
│ │ │
▼ ▼ ▼
┌───────────────────────┬────────────────────────────────────────────┐
│ HEADER │ AUDIO DATA │
│ (self.header) │ (self.music_data) │
│ │ │
│ Contains: │ Contains: │
│ - "fLaC" magic │ - Original FLAC frames │
│ - STREAMINFO block │ - Unchanged from disk │
│ - VORBIS_COMMENT │ │
│ with DB values: │ │
│ title, artist, │ │
│ album, genre │ │
│ - PADDING block │ │
└───────────────────────┴────────────────────────────────────────────┘
▲ ▲
│ │
From InterpolatedFLAC From original file
with injected DB tags (passed through)
```
---
## 4. File Write Flow
```
Application: write(fd, "TITLE=New Title\0", 16) # Hypothetical tag edit
┌─────────────────────────────────────────────────────────────────────────────┐
│ beetFileSystem.write(path, buf, offset, fh) │
│ Lines 1108-1135 │
│ │
│ return self.files[path].write(offset, buf) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ FileHandler.write(offset, buf) │
│ Lines 519-565 │
│ │
│ if offset >= self.bound: │
│ # Write is in audio area - DISCARD │
│ return # Do nothing, audio is read-only │
│ │
│ # Write is in header area - process tag update │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 1: Reconstruct full virtual file in memory │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ filedata = self.header + self.music_data │ │
│ │ │ │
│ │ # Patch in new data │ │
│ │ filedata = filedata[0:offset] + buf + filedata[offset + len(buf):] │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: Parse patched data as FLAC │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ self.inf = InterpolatedFLAC(filedata) │ │
│ │ # This parses the FLAC structure and extracts Vorbis comments │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 3: Extract tag values from parsed FLAC │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ self.item.title = str(self.inf["title"][0]).encode('utf-8') │ │
│ │ self.item.album = str(self.inf["album"][0]).encode('utf-8') │ │
│ │ self.item.artist = str(self.inf["artist"][0]).encode('utf-8') │ │
│ │ self.item.genre = str(self.inf["genre"][0]).encode('utf-8') │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 4: Save to beets database │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ self.lib.store(self.item) # Update item in library │ │
│ │ self.lib.save() # Persist to SQLite │ │
│ │ │ │
│ │ # NOTE: Original file on disk is NEVER touched! │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 5: Regenerate header for subsequent reads │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ self.inf["title"] = self.item.title │ │
│ │ self.inf["album"] = self.item.album │ │
│ │ self.inf["artist"] = self.item.artist │ │
│ │ self.inf["genre"] = self.item.genre │ │
│ │ │ │
│ │ self.header = self.inf.get_header(self.real_path) │ │
│ │ self.bound = len(self.header) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ return len(buf) # Success │
└─────────────────────────────────────────────────────────────────────────────┘
Write data flow summary:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Application │ │ beetfs │ │ Beets │ │ Original │
│ writes │────▶│ parses │────▶│ database │ │ file │
│ new tags │ │ extracts │ │ updated │ │ UNTOUCHED │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
```
---
## 5. File Release Flow
```
Application: close(fd)
┌─────────────────────────────────────────────────────────────────────────────┐
│ beetFileSystem.release(path, flags, fh) │
│ Lines 1049-1059 │
│ │
│ if self.files[path].release(): │
│ # Reference count reached 0, clean up │
│ del self.files[path] │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ FileHandler.release() │
│ Lines 489-495 │
│ │
│ self.instance_count -= 1 │
│ │
│ if self.instance_count == 0: │
│ return True # OK to delete │
│ else: │
│ return False # Still in use │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 6. Directory Listing Flow
```
Application: ls /mount/Pink\ Floyd/
┌─────────────────────────────────────────────────────────────────────────────┐
│ beetFileSystem.readdir(path, offset, dh) │
│ Lines 931-975 │
│ │
│ path = "/Pink Floyd" │
│ pathsplit = ["Pink Floyd"] │
│ │
│ yield fuse.Direntry(".") │
│ yield fuse.Direntry("..") │
│ │
│ # len(pathsplit) == 1, structure_depth - 1 == 2 │
│ # So we're listing directories (albums), not files │
│ │
│ for dirname in directory_structure.listdir(pathsplit, True): │
│ yield fuse.Direntry(dirname.encode('utf-8')) │
│ # "The Wall (1979) [FLAC]" │
│ # "Animals (1977) [FLAC]" │
│ # etc. │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 7. Complete Request Lifecycle
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ COMPLETE LIFECYCLE │
│ │
│ 1. User mounts: beet mount /mnt/music │
│ ├─ Build virtual tree from beets library │
│ └─ Start FUSE event loop │
│ │
│ 2. Application opens file: open("/mnt/music/Artist/Album/track.flac") │
│ ├─ Resolve virtual path to beets item ID │
│ ├─ Load original file into memory │
│ ├─ Inject database metadata into FLAC structure │
│ ├─ Generate new header with DB tags │
│ └─ Cache audio data │
│ │
│ 3. Application reads file: read(fd, buf, 4096) │
│ ├─ If reading header region → return header (DB metadata) │
│ ├─ If reading audio region → return cached audio (original) │
│ └─ If spanning both → return combined data │
│ │
│ 4. Application writes tags: write(fd, new_tags, offset) │
│ ├─ If audio region → discard (read-only) │
│ ├─ If header region: │
│ │ ├─ Parse new tag values │
│ │ ├─ Update beets database │
│ │ └─ Regenerate header │
│ └─ Original file NEVER modified │
│ │
│ 5. Application closes file: close(fd) │
│ ├─ Decrement reference count │
│ └─ Clean up if count == 0 │
│ │
│ 6. User unmounts: fusermount -u /mnt/music │
│ └─ fsdestroy() called, cleanup │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
```