Перейти к содержанию

General List

General List — отладочный реестр, фиксирующий каждый TrackData, который когда-либо видело приложение, вместе с полным снимком состояния всех слоёв кэша на момент регистрации. Это основной инструмент для диагностики промахов кэша, сбоев загрузки обложек и проблем с обогащением метаданных.

Исходники: src/persistence/generalList.ts, src/persistence/generalListCacheProbe.ts, src-tauri/src/general_list.rs


Форма записи

interface GeneralListEntry {
  id: string;
  track: TrackData;          // полный TrackData на момент последнего обнаружения
  meta: GeneralListMeta;
  cacheKeys: GeneralListCacheKeys;
  cache: GeneralListCache;   // живой снимок кэша, сделанный при регистрации
}

interface GeneralListMeta {
  firstSeenAt: number;       // epoch ms
  lastSeenAt: number;
  seenCount: number;
  contexts: string[];        // например ["search_results", "player"]
  lastContext: string;
  lastSearchQuery?: string;
}

seenCount увеличивается при каждом вызове registerEntity для одного и того же track ID. Это позволяет легко обнаружить треки, которые постоянно появляются в результатах поиска при разных запросах.


Двойная запись

Каждое сохранение пишет в два места:

  1. localStorage ключ neegde.generalList.v1 — переживает перезагрузку страницы, запись мгновенная.
  2. Диск через Tauri-команду general_list_write{app_data_dir}/general-list.json — переживает перезапуск приложения, доступно из файловой системы.

Обе записи дебоунсируются на 500 мс. Пачка вызовов recordGeneralList (например, поиск вернул 100 строк) инициирует одну запись, а не 100.

flushGeneralList() обходит дебоунс — используется перед закрытием приложения или перед показом файла в Finder/Explorer.

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

Путь записи: recordGeneralList

Вызывается из registerEntity в stores/entities.ts при каждой регистрации трека:

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

При первом обнаружении — создаёт новую запись с seenCount=1.

При повторном обнаружении — обновляет track (новые данные могут содержать свежеполученный coverUrl), увеличивает seenCount, добавляет context если он новый, обновляет снимок cache.

После записи _scheduleSave() взводит таймер дебоунса.


Обновление кэша: refreshGeneralListCache

Вызывается после завершения загрузки обложки или обогащения через Deezer (например, из RutrackerTrack.startCoverFetch, SoulseekTrack.startCoverFetch):

refreshGeneralListCache(id: string): void

Повторно запускает probeCache для существующей записи, не затрагивая meta. Это обновляет снимок cache для отражения нового состояния обложки — полезно для проверки того, приземлился ли вызов rutracker_get_cover в LRU в памяти.


Ключи кэша: buildCacheKeys

buildCacheKeys(data) выводит логический ключ кэша для каждого слоя, который потенциально может хранить данные для этого трека:

interface GeneralListCacheKeys {
  trackCacheId: string;                   // = track.id
  rutrackerTopicCover?: string;           // "${mirror}\n${topicId}"
  deezerCanonical?: string;               // нормализованный "artist|title"
  deezerAlbumArt?: string;               // нормализованный "deezer-album-art|artist|album"
  torrentFileB64?: string;               // topicId (суффикс ключа localStorage)
  streamIdentity?: { magnet?, fileIdx?, slskUsername?, slskFilepath? };
  soulseekCover?: string;                // "${username}\n${filepath}"
}

Ключи — логические идентификаторы, а не хэшированные идентификаторы слотов. probeCache использует их для поиска в реальном хранилище.


Зондирование кэша: probeCache

probeCache(data, keys) делает снимок каждого задействованного кэша в момент вызова:

interface GeneralListCache {
  capturedAt: number;
  trackCache: CacheEntry;               // localStorage "neegde.trackCache.v1"
  covers?: {
    rutrackerTopic?: CacheEntry;        // localStorage LRU "neegde.rtCover.v1.*"
    soulseekFile?: CacheEntry;          // дисковый кэш Rust "soulseek/covers/"
    inlinedOnTrack?: { present, kind }; // data: URL / https URL в 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"hit", "miss" или "unknown". Поле payloadHint содержит первые 80 символов payload при попадании (удобно для проверки наличия data: URL вместо пустой строки).

Зондирование SoulSeek cover вызывает peekSlskCover — синхронный peek в память без запуска сетевого запроса. Если обложка ни разу не загружалась или кэш в памяти был сброшен, состояние будет "miss", даже если файл существует на диске.


Tauri-команды

Команда Назначение
general_list_write(json) Записывает JSON-строку в {app_data_dir}/general-list.json
general_list_path Возвращает абсолютный путь к файлу
factory_reset Удаляет всё содержимое app_data_dir, затем возвращает управление

factory_reset вызывается через Настройки → Сбросить данные. Удаляет general-list.json вместе со всеми остальными файлами данных (session cookies, обложки, BT-состояние и т.д.).


Открытие файла

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

Вызывает tauri-plugin-opener для открытия родительской папки в Finder/Explorer. Путь к файлу разрешается через Rust, поэтому фронтенд никогда не хардкодит его.