Skip to content

General List

The General List is a debug registry that records every TrackData the app has ever seen, together with a full snapshot of every cache layer state at the time of registration. It is the primary tool for diagnosing cache misses, cover loading failures, and enrichment gaps.

Sources: src/persistence/generalList.ts, src/persistence/generalListCacheProbe.ts, src-tauri/src/general_list.rs


Entry shape

interface GeneralListEntry {
  id: string;
  track: TrackData;          // full TrackData as of last seen
  meta: GeneralListMeta;
  cacheKeys: GeneralListCacheKeys;
  cache: GeneralListCache;   // live cache probe captured at registration
}

interface GeneralListMeta {
  firstSeenAt: number;       // epoch ms
  lastSeenAt: number;
  seenCount: number;
  contexts: string[];        // e.g. ["search_results", "player"]
  lastContext: string;
  lastSearchQuery?: string;
}

seenCount increments on every registerEntity call for the same track ID. This makes it easy to spot tracks that keep appearing in search results across multiple queries.


Dual-write storage

Every save writes to two locations:

  1. localStorage key neegde.generalList.v1 — survives page reloads, immediate.
  2. Disk via general_list_write Tauri command → {app_data_dir}/general-list.json — survives app restarts, searchable from the filesystem.

Both writes are debounced at 500 ms. A burst of recordGeneralList calls (e.g. a search returning 100 rows) triggers one write, not 100.

flushGeneralList() bypasses the debounce — used before the app closes or before revealing the file in Finder/Explorer.

const STORAGE_KEY = "neegde.generalList.v1";
const DEBOUNCE_MS = 500;

Write path: recordGeneralList

Called from registerEntity in stores/entities.ts for every track registration:

recordGeneralList(data: TrackData, context: string, query?: string): void

On first sight — creates a new entry with seenCount=1.

On re-sight — updates track (new data may carry freshly enriched coverUrl), increments seenCount, appends context if new, refreshes cache snapshot.

After the entry is written, _scheduleSave() arms the debounce timer.


Cache refresh: refreshGeneralListCache

Called after a cover fetch or Deezer enrichment completes (e.g. from RutrackerTrack.startCoverFetch, SoulseekTrack.startCoverFetch):

refreshGeneralListCache(id: string): void

Re-runs probeCache on the existing entry without touching meta. This updates the cache snapshot to reflect the new cover state — useful for seeing whether a rutracker_get_cover call landed in the in-memory LRU.


Cache keys: buildCacheKeys

buildCacheKeys(data) derives the logical cache key for every cache layer that could hold data for this track:

interface GeneralListCacheKeys {
  trackCacheId: string;                   // = track.id
  rutrackerTopicCover?: string;           // "${mirror}\n${topicId}"
  deezerCanonical?: string;               // normalized "artist|title"
  deezerAlbumArt?: string;               // "deezer-album-art|artist|album" normalized
  torrentFileB64?: string;               // topicId (suffix for localStorage key)
  streamIdentity?: { magnet?, fileIdx?, slskUsername?, slskFilepath? };
  soulseekCover?: string;                // "${username}\n${filepath}"
}

Keys are logical identifiers — they are not hashed slot IDs. probeCache uses these to look up the actual storage.


Cache probe: probeCache

probeCache(data, keys) snapshots every relevant cache at call time:

interface GeneralListCache {
  capturedAt: number;
  trackCache: CacheEntry;               // localStorage "neegde.trackCache.v1"
  covers?: {
    rutrackerTopic?: CacheEntry;        // localStorage LRU "neegde.rtCover.v1.*"
    soulseekFile?: CacheEntry;          // Rust disk "soulseek/covers/"
    inlinedOnTrack?: { present, kind }; // data: URL / https URL on TrackData.coverUrl
  };
  enrichment?: {
    deezerCanonical?: CacheEntry;       // localStorage LRU "neegde.dzTrackCanon.v1.*"
    deezerAlbumArt?: CacheEntry;        // localStorage LRU "neegde.dzAlbumArt.v1.*"
  };
  torrent?: {
    fileListB64?: CacheEntry;           // localStorage "torrent_file_b64_v1_<topicId>"
  };
  streaming?: {
    backendDiskCache: "hit"|"partial"|"unknown"|"na";
    note?: string;
  };
}

CacheEntry.state is "hit", "miss", or "unknown". The payloadHint field carries the first 80 characters of a hit's payload (useful for confirming a data: URL is present vs. an empty string).

The SoulSeek cover probe calls peekSlskCover — a synchronous in-memory peek that does not trigger a network fetch. If the cover was never fetched or the memory cache was cleared, the state is "miss" even if it exists on disk.


Tauri commands

Command Purpose
general_list_write(json) Writes the JSON string to {app_data_dir}/general-list.json
general_list_path Returns the absolute path of the file
factory_reset Deletes everything in app_data_dir, then returns

factory_reset is called by Settings → Reset app data. It deletes general-list.json along with every other data file (session cookies, covers, BT state, etc.).


Revealing the file

export async function revealGeneralListFile(): Promise<void> {
  const filePath = await invoke<string>("general_list_path");
  await openPath(filePath);
}

Calls tauri-plugin-opener to open the parent directory in Finder/Explorer. The file path is resolved from Rust so the frontend never hardcodes it.