101 lines
4.0 KiB
C#
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);
|
|
}
|
|
}
|