Модель трека и альбома¶
Доменная модель фронтенда — это иерархия классов с корнями Track и Album. Provider-ы отправляют плоские объекты данных (TrackData, AlbumData); реестр entities преобразует их в экземпляры классов через factory-функции.
Источники: src/track/, src/album/
TrackData — форма данных по проводу¶
export interface TrackData {
type: "track";
id: string;
title: string;
artist: string | null;
albumTitle?: string | null;
albumId: string | null;
fileName: string;
format: string | null; // "FLAC", "MP3" и т.д.
bitrate: number | null;
duration: number | null;
size: number | null;
coverUrl?: string | null;
sources: [TrackSource, ...TrackSource[]]; // не менее одного источника
score?: number;
mergedFrom?: number;
}
sources — непустой массив дескрипторов источников для конкретного provider-а. Первый элемент определяет подкласс.
Дескрипторы источников¶
// RuTracker
interface RutrackerRefs {
topicId: string | null;
magnet: string;
fileIdx: number;
coverFileIdx: number | null;
albumDirPath: string | null;
}
// SoulSeek
interface SoulseekRefs {
slskUsername: string;
slskFilepath: string;
slskFolder?: string;
}
RutrackerRefs.fileIdx — позиция этого файла с нуля в TorrentDetails.files. Передаётся как fileIdx в torrent_prepare_stream.
SoulseekRefs также несёт alternativePeers в raw (до 5 резервных peer-ов):
Иерархия классов Track¶
abstract Track
├── RutrackerTrack (sources[0].kind === "rutracker")
├── SoulseekTrack (sources[0].kind === "soulseek")
└── MagnetTrack (sources[0].kind === "magnet")
buildTrack(data) (из src/track/factory.ts) диспатчит к правильному подклассу на основе data.sources[0].kind.
Абстрактный API¶
abstract class Track {
// Идентичность (геттеры, делегирующие к this.data)
get id(): string
get title(): string
get artist(): string | null
get albumId(): string | null
get albumTitle(): string | null
get fileName(): string
get size(): number | null
get bitrate(): number | null
get duration(): number | null
get format(): string | null
get kind(): ProviderKind
// Поведение (должно быть реализовано в подклассе)
abstract prepareStream(): Promise<string> // возвращает URL HTTP stream-а
abstract hasPlaybackIdentity(): boolean // возможно ли запустить stream
abstract exportToDisk(onProgress?): Promise<void>
abstract navigationTarget(): NavigationTarget | null
abstract coverUrl(): string | null // реактивное чтение из cache
abstract startCoverFetch(signal?, opts?): void // ленивый запуск загрузки обложки
// Сериализация
toJSON(): TrackData // единственный путь сохранения
}
prepareStream() для каждого подкласса¶
RutrackerTrack:
async prepareStream(): Promise<string> {
return streamUrl(this.refs.magnet, this.refs.fileIdx, {
source: "rutracker",
torrentId: this.refs.topicId,
});
}
SoulseekTrack (с failover):
async prepareStream(): Promise<string> {
// Сначала пробуем основного peer-а
try {
return await streamUrl("", 0, {
source: "soulseek",
slskUsername: this.refs.slskUsername,
slskFilepath: this.refs.slskFilepath,
slskFilesize: this.data.size ?? 0,
});
} catch {
// Перебираем альтернативных peer-ов в порядке peerRank
for (const alt of this.altPeers) {
try {
return await streamUrl(…alt);
} catch { /* следующий */ }
}
throw new Error("All peers failed");
}
}
Модель альбома¶
export interface AlbumData {
type: "album";
id: string;
title: string;
artist: string | null;
year: string | null;
format: string | null;
bitrate: number | null;
size: number | null;
seeders: number | null;
leechers: number | null;
peers: number | null;
trackIds: string[];
sources: AlbumSource[];
score?: number;
mergedFrom?: number;
}
trackIds — упорядоченный массив ID entity треков. Очередь, лайки и TorrentView разрешают фактические экземпляры Track через реестр entities по этим ID.
buildAlbum(data) (из src/album/factory.ts) создаёт экземпляр класса Album. Метод Album.coverUrl() читает из реактивного cache — тот же механизм entitiesVersion, что используют computed-ы очереди.
Factory и нормализация¶
buildTrack(data) и buildAlbum(data) вызываются:
1. normalize() в stores/entities.ts — когда сырые данные приходят от provider-а.
2. hydrateTrack(id) — при восстановлении из trackCache при запуске приложения.
Factory-функции не создают копии — экземпляр класса хранит ссылку на переданный объект data. Геттеры класса (get title(), get artist()) делегируют напрямую к this.data.
registerEntity вызывает withMergedPersisted(entity) перед нормализацией, что объединяет любые сохранённые coverUrl или albumTitle из предыдущей сессии. Это предотвращает исчезновение обогащения каталога при новом поиске для трека, который пользователь уже лайкнул.
toJSON() — контракт сохранения¶
Track.toJSON() возвращает this.data напрямую (исходный объект TrackData). Это единственный путь сериализации. trackCache, likes.json, queue.json и плейлисты — все хранят и читают эту форму.
Отдельных форм «строки» для разных хранилищ не существует — всё проходит через TrackData → реестр entities → экземпляр класса.