# 🎵 Global Music Scanner — Аудит кода

**Дата:** 13 июля 2026  
**Проект:** Global Music Scanner v4.3.0  
**Статус запуска:** ✅ Frontend (port 3000) + API Server (port 3001) — оба работают

---

## 📊 Общая оценка

| Категория | Оценка | Критичность |
|-----------|--------|-------------|
| Архитектура | ⭐⭐☆☆☆ | 🔴 Высокая |
| Типобезопасность | ⭐⭐☆☆☆ | 🔴 Высокая |
| Производительность | ⭐⭐⭐☆☆ | 🟡 Средняя |
| Безопасность | ⭐⭐⭐☆☆ | 🟡 Средняя |
| Тестирование | ⭐☆☆☆☆ | 🔴 Высокая |
| Код-стиль | ⭐⭐⭐⭐☆ | 🟢 Низкая |

---

## 🔴 Критические проблемы

### 1. App.tsx — «God Component» (~3985 строк)

**Проблема:** Весь бизнес-логик, state management, pipeline сканирования, UI-рендеринг — всё в одном файле. Это делает код:
- Не поддерживаемым: невозможно быстро найти нужный фрагмент
- Не тестируемым: нет возможности unit-test'ить отдельные функции
- Опасным при рефакторинге: изменение одной части может сломать другую

**Рекомендация:** Разделить на:
```
src/
  hooks/
    useArtistSearch.ts        — поиск артиста
    useScanPipeline.ts         — pipeline сканирования (все этапы)
    useSonglinkAuto.ts         — авто-этап Songlink
    useDeezerScan.ts           — Deezer-этап
    useTidalScan.ts            — Tidal-этап
    useQobuzScan.ts            — Qobuz-этап
    useMusicBrainzScan.ts      — MB-этап
    useDeepRescan.ts           — глубокий перескан
    useXlsxExport.ts           — экспорт
  state/
    scannerStore.ts            — Zustand/Context store для state
  App.tsx                      — только layout + orchestration (~300 строк)
```

---

### 2. Реестр сервисов пустой — миграция не завершена

**Проблема:** Файл `src/services/registry.ts` содержит:
```ts
export const SERVICES: StreamingService[] = [];
```
Массив пуст. Интерфейс `StreamingService` определён, но ни один сервис его не реализует. Весь pipeline по-прежнему живёт inline в App.tsx.

**Рекомендация:** Завершить миграцию — реализовать адаптеры `apple`, `deezer`, `tidal`, `qobuz` по контракту `StreamingService` и зарегистрировать в `SERVICES`. Это позволит:
- Добавлять новые платформы (Spotify, YouTube Music) без правки App.tsx
- Параллельно запускать сканеры
- Унифицировать прогресс-колбэки

---

### 3. Массовое использование `any` — TypeScript не защищает

**Проблема:** Практически все структуры данных — `any`:
```ts
function findBestAlbumMatch(items: any[], candidate: any, fallbackArtist: string, requireSource?: string)
let bestMatch: any = null;
```
Это полностью обнуляет ценность TypeScript: нет autocomplete, нет compile-time проверок, опечатки в именах полей не ловятся.

**Рекомендация:** Определить interfaces:
```ts
interface AlbumItem {
  id: string | number;
  title: string;
  displayTitle: string;
  displayArtist: string;
  displayYear: number | null;
  sources: ServiceId[];
  urls: Partial<Record<ServiceId, string>>;
  albumDurationMillis?: number;
  upc?: string;
  // ...
}
```
Начать с ключевых структур: `AlbumItem`, `ArtistSearchResult`, `ScanDiagnostics`.

---

### 4. Нет тестов

**Проблема:** Полное отсутствие тестов — ни unit, ни integration, ни e2e. Любое изменение — потенциальный регресс.

**Рекомендация:** Внедрить Vitest (совместим с Vite):
```
src/lib/__tests__/
  utils.test.ts         — normalizeTitle, durationsEqual, etc.
  cors-proxies.test.ts
src/services/__tests__/
  apple.test.ts
  deezer.test.ts
  songlink.test.ts
src/components/__tests__/
  AlbumCard.test.tsx
```
Приоритет: утилиты (чистые функции) → сервисы (mock fetch) → компоненты (React Testing Library).

---

## 🟡 Проблемы средней важности

### 5. Дублирование CORS-прокси

**Проблема:** Список прокси дублируется в двух местах:
- `src/lib/cors-proxies.ts` → `PUBLIC_CORS_PROXIES`
- `src/services/songlink/proxies.ts` → `SONGLINK_PROXIES`

Оба списка идентичны. Изменение в одном не обновляет другой.

**Рекомендация:** Вынести в общий `src/lib/proxy-list.ts` и переиспользовать.

---

### 6. Нет retry-логики для большинства API

**Проблема:** Только Songlink имеет retry при 429. Остальные API (Tidal, Qobuz, MusicBrainz, Deezer) при временной ошибке просто падают.

**Рекомендация:** Создать универсальную retry-обёртку:
```ts
async function fetchWithRetry<T>(
  fn: () => Promise<T>,
  options: { retries: number; baseDelayMs: number; retryOnStatus?: number[] }
): Promise<T>
```

---

### 7. Захардкоженные API-токены в исходниках

**Проблема:**
```ts
export const TIDAL_WEB_TOKEN = 'gsFXkJqGrUNoYMQPZe4k3WKwijnrp8iGSwn3bApe';
export const QOBUZ_APP_ID = '712109809';
```
Эти токены — публичные (из web-плеера), но их hardcoded значение:
- Невозможно ротировать без пересборки
- Видны в source map в production
- Не различаются по окружениям (dev/staging/prod)

**Рекомендация:** Перенести в переменные окружения (`VITE_TIDAL_TOKEN`, `VITE_QOBUZ_APP_ID`) или проксировать через API-сервер.

---

### 8. Нет персистентности состояния

**Проблема:** Все результаты сканирования теряются при перезагрузке страницы. Для крупных артистов (сканирование 5-15 минут) это критично.

**Рекомендация:**
- LocalStorage для минимального сохранения (текущий артист, настройки)
- IndexedDB для полных результатов сканирования
- Опционально: экспорт/импорт JSON-снапшота

---

### 9. Нет React.memo и useMemo для тяжёлых списков

**Проблема:** Компонент `AlbumCard` перерисовывается при каждом state-изменении родителя. При 200+ альбомах это заметно.

**Рекомендация:**
```tsx
export const AlbumCard = React.memo(({ album, probeState, onSonglinkProbe }: AlbumCardProps) => {
  // ...
});
```
А также `useMemo` для `displayResults`, отфильтрованных списков, `scannedRegions`.

---

### 10. Bundle Size — 335 KB main + 937 KB exceljs

**Проблема:** 
- Основной бандл 335 KB (97 KB gzip) — многовато для SPA
- exceljs 937 KB — загружается лениво, но всё равно тяжело

**Рекомендация:**
- Динамический импорт `exceljs` через `import()` — уже сделано ✅
- Code splitting: вынести `services/` в отдельный chunk (`vite.config.ts → manualChunks`)
- Tree-shaking lucide-react: импортировать только нужные иконки (частично сделано)
- Рассмотреть замену exceljs на SheetJS (xlsx) — легче на ~40%

---

## 🟢 Рекомендации по улучшению

### 11. API-сервер: нет CORS whitelist

**Проблема:** API-сервер разрешает CORS для всех доменов (`app.use(cors())`). Это значит любой сайт может обращаться к `/api/proxy`.

**Рекомендация:**
```ts
app.use(cors({
  origin: ['https://yourdomain.com', 'http://localhost:3000']
}));
```

---

### 12. API-сервер: отсутствует graceful shutdown

**Проблема:** Нет обработки SIGTERM/SIGINT для корректного закрытия соединений.

**Рекомендация:**
```ts
const server = app.listen(port);
process.on('SIGTERM', () => server.close());
process.on('SIGINT', () => server.close());
```

---

### 13. JSONP — устаревший подход

**Проблема:** JSONP используется для Apple и Deezer. Это:
- Не поддерживает HTTP-методы кроме GET
- Не позволяет читать status codes
- Потенциальный XSS-вектор (исполнение произвольного JS)

**Рекомендация:** Apple/iTunes API поддерживает CORS для публичных endpoints. Проверить возможность перехода на обычный fetch. Deezer — вероятно, останется JSONP, но стоит добавить Subresource Integrity (SRI).

---

### 14. Нет обработки «пустых» ошибок

**Проблема:** Множество `catch {}` в коде:
```ts
} catch {}  // deezer/discography.ts
} catch (e) { break; }  // tidal/api.ts
```
Ошибки проглатываются без логирования.

**Рекомендация:** Минимум `console.warn` или передача в diagnostics:
```ts
catch (e) { console.warn('[Deezer] discography page failed:', e); }
```

---

### 15. i18n — незавершённый

**Проблема:** Переводы есть для RU и EN, но множество строк захардкожены на русском прямо в JSX:
```tsx
<div className="text-[10px] text-[var(--muted-foreground)]">
  по {albumsWithDurationCount} из {albumCount} альбомов
</div>
```

**Рекомендация:** Пройтись по App.tsx и вынести все строки в `i18n.ts`.

---

### 16. Модульные переменные для throttle не потоко-безопасны

**Проблема:**
```ts
let songlinkLastCallAt = 0;
let lastCallAt = 0; // MusicBrainz
```
Если пользователь откроет приложение в двух вкладках — каждая вкладка будет independently throttle'ить, что может привести к суммарному превышению лимита Odesli (10 RPM × 2 вкладки = 20 RPM).

**Рекомендация:** Использовать BroadcastChannel API для синхронизации throttle между вкладками.

---

### 17. Нет health-check мониторинга CORS-прокси

**Проблема:** 8 публичных CORS-прокси используются как fallback. Если все упадут — приложение молча перестанет работать для Tidal/Qobuz/Songlink.

**Рекомендация:**
- Периодический ping прокси (каждые 5 минут)
- Визуальный индикатор в UI: «CORS proxy degraded»
- Метрики успешности (уже есть `proxyResponseStats`) — экспортировать в UI

---

### 18. Проксирование ответа API-сервера: отсутствие валидации

**Проблема:** В `/api/proxy` ответ upstream передаётся клиенту as-is:
```ts
const contentType = upstream.headers.get("content-type") || "application/json";
res.status(upstream.status);
res.setHeader("Content-Type", contentType);
const body = await upstream.arrayBuffer();
res.send(Buffer.from(body));
```
Нет лимита на размер тела ответа. Злоумышленник может направить запрос, который вернёт огромный ответ, нагружая сервер.

**Рекомендация:** Добавить `Content-Length` check или streaming с ограничением:
```ts
const MAX_BODY_SIZE = 5 * 1024 * 1024; // 5 MB
if (body.byteLength > MAX_BODY_SIZE) {
  res.status(502).json({ error: 'Response too large' });
  return;
}
```

---

## 📋 Приоритетный план действий

| # | Задача | Сложность | Приоритет |
|---|--------|-----------|-----------|
| 1 | Добавить тесты (utils, services) | Средняя | 🔴 P0 |
| 2 | Определить interfaces вместо `any` | Средняя | 🔴 P0 |
| 3 | Вынести бизнес-логику из App.tsx в hooks | Высокая | 🔴 P0 |
| 4 | Завершить миграцию на StreamingService | Высокая | 🔴 P1 |
| 5 | Персистентность (LocalStorage/IndexedDB) | Средняя | 🟡 P1 |
| 6 | React.memo для AlbumCard | Низкая | 🟡 P1 |
| 7 | Retry-обёртка для всех API | Средняя | 🟡 P1 |
| 8 | CORS whitelist на сервере | Низкая | 🟡 P2 |
| 9 | Дедупликация proxy-list | Низкая | 🟡 P2 |
| 10 | Graceful shutdown API-сервера | Низкая | 🟢 P2 |
| 11 | Валидация response size proxy | Низкая | 🟢 P2 |
| 12 | Bundle optimization (manualChunks) | Средняя | 🟢 P3 |
| 13 | Перевести hardcoded строки в i18n | Низкая | 🟢 P3 |
| 14 | BroadcastChannel для throttle sync | Средняя | 🟢 P3 |

---

## 💡 Архитектурные предложения

### Добавить новый сервис (Spotify)

Текущий процесс (после завершения миграции):
1. Создать `src/services/spotify/api.ts` — API-адаптер
2. Создать `src/services/spotify/index.ts` — реализация `StreamingService`
3. Создать `src/icons/SpotifyIcon.tsx`
4. Добавить в `registry.ts` → `SERVICES.push(spotify)`
5. Добавить перевод в `i18n.ts`

### Внедрить state management

Рекомендация — Zustand (лёгкий, 1 KB):
```ts
// src/state/scannerStore.ts
interface ScannerState {
  selectedArtist: ArtistResult | null;
  albums: AlbumItem[];
  diagnostics: ScanDiagnostics;
  isScanning: boolean;
  scanProgress: number;
  // actions
  search: (query: string) => Promise<void>;
  scan: (options: ScanOptions) => Promise<void>;
  reset: () => void;
}
```

---

## 🏁 Заключение

Проект функционально зрелый и хорошо документированный (комментарии на высшем уровне). Основная проблема — **технический долг от монолитного App.tsx** и **отсутствие типизации**. Если проект будет развиваться (добавление Spotify, YouTube Music, бэкенд для сохранения), эти проблемы станут критическими.

**Quick wins** (малые усилия, большой эффект):
1. `React.memo(AlbumCard)` — мгновенное улучшение производительности
2. CORS whitelist на сервере — 5 минут, закрывает дыру
3. Тесты на `normalizeTitle`, `durationsEqual` — 30 минут работы, покрывают ключевую логику
4. Заменить `any` на interfaces в `findBestAlbumMatch` — повышает безопасность матчинга
