using Kit.Helpers.SqlHelperAbstract; using Server.WatchBody.Core.Dal.Src.Domain; using Server.WatchBody.Core.Dal.Src.Repository; using Server.WatchBody.Import; namespace Server.WatchBody.Fetch; /// Ошибка конфигурации/атлета fetch (CLI → код 1, spec 02 §3). public class FetchException(string message) : Exception(message); /// /// Цикл забора (spec 02 m07): логин → список → фильтр strength_training /// → скачивание в downloadDir → разбор и запись конвейером m06 → /// дедуп двух уровней (файл-кэш по activityId, fit_file_id в БД). /// 🛡 Записи в БД — только через WorkoutImporter; сбой файла пакет /// не останавливает. /// public class FitFetcher(IConnectionString cs, GarminClient client, string downloadDir, string athleteName) { public (int Downloaded, int Skipped, int Failed) FetchNew( int limit = 20, CancellationToken ct = default) { // 1. атлет (🛡1 — факты без атлета не пишутся) var athlete = new AthleteRepository(cs).Select(null) .FirstOrDefault(a => a.Name == athleteName) ?? throw new FetchException($"атлет '{athleteName}' не найден (Garmin.AthleteName)"); // 2. логин (исключения — наверх) client.Login(ct); // 3. список, только силовые (Ф-8.1) var activities = client.ListActivities(limit, ct) .Where(a => a.ActivityType.Equals("strength_training", StringComparison.OrdinalIgnoreCase)) .ToList(); Directory.CreateDirectory(downloadDir); var workouts = new WorkoutRepository(cs); var importer = new WorkoutImporter(workouts); var parser = new FitParser(); var downloaded = 0; var skipped = 0; var failed = 0; foreach (var activity in activities) { var cached = Path.Combine(downloadDir, $"{activity.ActivityId}.fit"); var temp = cached + ".downloading"; try { // рубеж 1: файл-кэш — без сети (🛡2) if (File.Exists(cached)) { skipped++; continue; } // 4. скачать и разобрать var bytes = client.DownloadFit(activity.ActivityId, ct); ActivityFacts facts; try { File.WriteAllBytes(temp, bytes); facts = parser.Parse(temp); } catch (FitParseException) { // битый download — в .bad, кэш не занимает (spec 02 §4) File.Move(temp, cached + ".bad", overwrite: true); throw; } File.Delete(temp); // рубеж 2: fit_file_id уже в БД — дубль, файл не нужен (🛡2) var day = DateOnly.FromDateTime(facts.StartedAt); var duplicate = workouts.Select(new WorkoutFilter { AthleteId = athlete.Id, DateFrom = day.AddDays(-1), DateTo = day.AddDays(1), }).Any(w => w.FitFileId == facts.FitFileId); if (duplicate) { skipped++; continue; } // 5. запись и кэш importer.Import(athlete.Id, facts); File.WriteAllBytes(cached, bytes); downloaded++; } catch (Exception) { failed++; // файл остаётся/фиксируется как .bad; пакет продолжается (🛡3) } } return (downloaded, skipped, failed); } }