VEU.WathBody/3-app/2-cli/Server.WatchBody.Fetch/FitFetcher.cs

101 lines
4.0 KiB
C#

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;
/// <summary>Ошибка конфигурации/атлета fetch (CLI → код 1, spec 02 §3).</summary>
public class FetchException(string message) : Exception(message);
/// <summary>
/// Цикл забора (spec 02 m07): логин → список → фильтр strength_training
/// → скачивание в downloadDir → разбор и запись конвейером m06 →
/// дедуп двух уровней (файл-кэш по activityId, fit_file_id в БД).
/// 🛡 Записи в БД — только через WorkoutImporter; сбой файла пакет
/// не останавливает.
/// </summary>
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);
}
}