diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24d6285 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "inheritdoc", + "Newtonsoft", + "seealso" + ] +} \ No newline at end of file diff --git a/GarminConnectClient.Console/GarminConnectClient.Console.csproj b/GarminConnectClient.Console/GarminConnectClient.Console.csproj index b2e15d8..6079f60 100644 --- a/GarminConnectClient.Console/GarminConnectClient.Console.csproj +++ b/GarminConnectClient.Console/GarminConnectClient.Console.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + net6.0 1.2.0 Marazt Marazt diff --git a/GarminConnectClient.Lib/GarminConnectClient.Lib.csproj b/GarminConnectClient.Lib/GarminConnectClient.Lib.csproj index f85e1ba..6c1f260 100644 --- a/GarminConnectClient.Lib/GarminConnectClient.Lib.csproj +++ b/GarminConnectClient.Lib/GarminConnectClient.Lib.csproj @@ -1,7 +1,6 @@ - - + - netstandard2.0 + net6.0 GarminConnectClient is a library for communication with GarminConnect. It allows to list, download and upload Garmin activities. It is inspired by https://github.com/La0/garmin-uploader. © 2020 Marek Polak https://github.com/marazt/garmin-connect-client/blob/master/LICENSE.md @@ -15,16 +14,12 @@ Marazt true 1.2.0 + .\GarminConnectClient.Lib.xml - - - F:\Projects\GarminConnectClient\GarminConnectClient.Lib\GarminConnectClient.Lib.xml - - - - - + + + + - - + \ No newline at end of file diff --git a/GarminConnectClient.Lib/GarminConnectClient.Lib.xml b/GarminConnectClient.Lib/GarminConnectClient.Lib.xml index 7b4b6f9..56ed662 100644 --- a/GarminConnectClient.Lib/GarminConnectClient.Lib.xml +++ b/GarminConnectClient.Lib/GarminConnectClient.Lib.xml @@ -1575,12 +1575,12 @@ - + The configuration - + The logger diff --git a/GarminConnectClient.Lib/Services/Client.cs b/GarminConnectClient.Lib/Services/Client.cs index 20508c8..6193971 100644 --- a/GarminConnectClient.Lib/Services/Client.cs +++ b/GarminConnectClient.Lib/Services/Client.cs @@ -1,9 +1,4 @@ -using GarminConnectClient.Lib.Dto; -using GarminConnectClient.Lib.Enum; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -14,6 +9,11 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using GarminConnectClient.Lib.Dto; +using GarminConnectClient.Lib.Enum; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace GarminConnectClient.Lib.Services { @@ -29,7 +29,7 @@ public class Client : IClient private const string USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0"; private const string CONNECT_DNS = "connect.garmin.com"; private const string CONNECT_URL = "https://" + CONNECT_DNS; - private const string CONNECT_URL_MODERN = CONNECT_URL + "/modern/"; + private const string CONNECT_URL_MODERN = CONNECT_URL + "/"; private const string CONNECT_URL_SIGNIN = CONNECT_URL + "/signin/"; private const string SSO_DNS = "sso.garmin.com"; private const string SSO_URL = "https://" + SSO_DNS; @@ -39,33 +39,42 @@ public class Client : IClient private const string CONNECT_MODERN_HOSTNAME = "https://connect.garmin.com/modern/auth/hostname"; private const string CSS_URL = CONNECT_URL + "/gauth-custom-v1.2-min.css"; private const string PRIVACY_STATEMENT_URL = "https://www.garmin.com/en-US/privacy/connect/"; - private const string URL_UPLOAD = CONNECT_URL + "/modern/proxy/upload-service/upload"; + private const string URL_UPLOAD = CONNECT_URL + "/proxy/upload-service/upload"; private const string URL_ACTIVITY_BASE = CONNECT_URL + "/modern/proxy/activity-service/activity"; private const string UrlActivityTypes = - "https://connect.garmin.com/modern/proxy/activity-service/activity/activityTypes"; + "https://connect.garmin.com/proxy/activity-service/activity/activityTypes"; private const string UrlEventTypes = - "https://connect.garmin.com/modern/proxy/activity-service/activity/eventTypes"; + "https://connect.garmin.com/proxy/activity-service/activity/eventTypes"; private const string UrlActivitiesBase = - "https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities"; + "https://connect.garmin.com/proxy/activitylist-service/activities/search/activities"; private const string UrlActivityDownloadFile = - " https://connect.garmin.com/modern/proxy/download-service/export/{0}/activity/{1}"; + "https://connect.garmin.com/modern/proxy/download-service/export/{0}/activity/{1}"; private const string UrlActivityDownloadDefaultFile = - " https://connect.garmin.com/modern/proxy/download-service/files/activity/{0}"; + "https://connect.garmin.com/modern/proxy/download-service/files/activity/{0}"; private const ActivityFileTypeEnum DefaultFile = ActivityFileTypeEnum.Fit; - private static CookieContainer cookieContainer; - private static HttpClientHandler clientHandler; - private HttpClient httpClient; + private static readonly CookieContainer _cookieContainer = new(); + private static readonly HttpClientHandler _clientHandler = new() + { + AllowAutoRedirect = true, + UseCookies = true, + CookieContainer = _cookieContainer + }; + private static HttpClient httpClient = new(_clientHandler) + { + DefaultRequestVersion = HttpVersion.Version20, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher + }; - private static readonly Tuple BaseHeader = new Tuple("NK", "NT"); + private static readonly Tuple BaseHeader = new("NK", "NT"); - private static readonly Dictionary QueryParams = new Dictionary + private static readonly Dictionary QueryParams = new() { {"clientId", "GarminConnect"}, {"connectLegalTerms", "true"}, @@ -99,7 +108,6 @@ public class Client : IClient {"showConnectLegalAge", "false"}, {"showPassword", "true"}, {"source", CONNECT_URL_SIGNIN}, - // {"usernameShown", "false"}, {"useCustomHeader", "false"}, {"webhost", CONNECT_URL_MODERN} }; @@ -107,13 +115,13 @@ public class Client : IClient /// /// The configuration /// - private readonly IConfiguration configuration; + private readonly IConfiguration _configuration; /// /// The logger /// // ReSharper disable once NotAccessedField.Local - private readonly ILogger logger; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -122,8 +130,8 @@ public class Client : IClient /// The logger. public Client(IConfiguration configuration, ILogger logger) { - this.configuration = configuration; - this.logger = logger; + _configuration = configuration; + _logger = logger; } /// @@ -140,19 +148,8 @@ public Client(IConfiguration configuration, ILogger logger) /// public async Task<(CookieContainer, HttpClientHandler)> Authenticate() { - cookieContainer = new CookieContainer(); - clientHandler = - new HttpClientHandler - { - AllowAutoRedirect = true, - UseCookies = true, - CookieContainer = cookieContainer - }; - - this.httpClient = new HttpClient(clientHandler); - - this.httpClient.DefaultRequestHeaders.Add("user-agent", USER_AGENT); - var data = await this.httpClient.GetStringAsync(CONNECT_MODERN_HOSTNAME); + httpClient.DefaultRequestHeaders.Add("user-agent", USER_AGENT); + var data = await httpClient.GetStringAsync(CONNECT_MODERN_HOSTNAME); var ssoHostname = JObject.Parse(data)["host"] == null ? throw new Exception("SSO hostname is missing") @@ -161,56 +158,57 @@ public Client(IConfiguration configuration, ILogger logger) var queryParams = string.Join("&", QueryParams.Select(e => $"{e.Key}={WebUtility.UrlEncode(e.Value)}")); var url = $"{SSO_URL_SSO_SIGNIN}?{queryParams}"; - var res = await this.httpClient.GetAsync(url); + var res = await httpClient.GetAsync(url); ValidateResponseMessage(res, "No login form."); data = await res.Content.ReadAsStringAsync(); var csrfToken = ""; try { - GetValueByPattern(data, @"input type=\""hidden\"" name=\""_csrf\"" value=\""(\w+)\"" \/>", 2, 1); + csrfToken = GetValueByPattern(data, @"input type=\""hidden\"" name=\""_csrf\"" value=\""(\w+)\"" \/>", 2, 1); } catch (Exception e) { - this.logger.LogError("Exception finding token by pattern: ", e); - this.logger.LogError($"data:\n{data}"); - throw e; + _logger.LogError("Exception finding token by pattern: ", e); + _logger.LogError("data:\n", data); + throw; } - this.httpClient.DefaultRequestHeaders.Add("origin", SSO_URL); - this.httpClient.DefaultRequestHeaders.Add("referer", url); + httpClient.DefaultRequestHeaders.Add("origin", SSO_URL); + httpClient.DefaultRequestHeaders.Add("referer", url); + httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); var formContent = new FormUrlEncodedContent(new[] { new KeyValuePair("embed", "false"), - new KeyValuePair("username", this.configuration.Username), - new KeyValuePair("password", this.configuration.Password), + new KeyValuePair("username", _configuration.Username), + new KeyValuePair("password", _configuration.Password), new KeyValuePair("_csrf", csrfToken) }); - res = await this.httpClient.PostAsync(url, formContent); + res = await httpClient.PostAsync(url, formContent); data = await res.Content.ReadAsStringAsync(); ValidateResponseMessage(res, $"Bad response {res.StatusCode}, expected {HttpStatusCode.OK}"); - ValidateCookiePresence(cookieContainer, "GARMIN-SSO-GUID"); + ValidateCookiePresence(_cookieContainer, "GARMIN-SSO-GUID"); var ticket = GetValueByPattern(data, @"var response_url(\s+)= (\""|\').*?ticket=([\w\-]+)(\""|\')", 5, 3); // Second auth step // Needs a service ticket from previous response - this.httpClient.DefaultRequestHeaders.Remove("origin"); + httpClient.DefaultRequestHeaders.Remove("origin"); url = $"{CONNECT_URL_MODERN}?ticket={WebUtility.UrlEncode(ticket)}"; - res = await this.httpClient.GetAsync(url); + res = await httpClient.GetAsync(url); ValidateModernTicketUrlResponseMessage(res, $"Second auth step failed to produce success or expected 302: {res.StatusCode}."); // Check session cookie - ValidateCookiePresence(cookieContainer, "SESSIONID"); + ValidateCookiePresence(_cookieContainer, "SESSIONID"); // Check login - res = await this.httpClient.GetAsync(CONNECT_URL_PROFILE); + res = await httpClient.GetAsync(CONNECT_URL_PROFILE); ValidateResponseMessage(res, "Login check failed."); - return (cookieContainer, clientHandler); + return (_cookieContainer, _clientHandler); } /// @@ -257,9 +255,9 @@ private static void ValidateResponseMessage(HttpResponseMessage responseMessage, } } - private void ValidateModernTicketUrlResponseMessage(HttpResponseMessage responseMessage, string error) + private static void ValidateModernTicketUrlResponseMessage(HttpResponseMessage responseMessage, string error) { - if (!responseMessage.IsSuccessStatusCode && !responseMessage.StatusCode.Equals(HttpStatusCode.MovedPermanently)) + if (!responseMessage.IsSuccessStatusCode && !responseMessage.StatusCode.Equals(HttpStatusCode.OK)) { throw new Exception(error); } @@ -281,11 +279,10 @@ public async Task DownloadActivityFile(long activityId, ActivityFileType : string.Format(UrlActivityDownloadFile, fileFormat.ToString().ToLower(), activityId); Stream streamCopy = new MemoryStream(); - using (var res = await this.httpClient.GetAsync(url)) - { - await (await res.Content.ReadAsStreamAsync()).CopyToAsync(streamCopy); - return streamCopy; - } + var res = await httpClient.GetAsync(url); + + await (await res.Content.ReadAsStreamAsync()).CopyToAsync(streamCopy); + return streamCopy; } /// @@ -307,63 +304,59 @@ public async Task DownloadActivityFile(long activityId, ActivityFileType { var extension = fileFormat.FormatKey; var url = $"{URL_UPLOAD}/.{extension}"; - this.httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); var form = new MultipartFormDataContent( $"------WebKitFormBoundary{DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)}"); - using (var stream = new FileStream(fileName, FileMode.Open)) + using var stream = new FileStream(fileName, FileMode.Open); + using var content = new StreamContent(stream); + + content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { - using (var content = new StreamContent(stream)) - { - content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") - { - Name = "file", - FileName = Path.GetFileName(fileName), - Size = stream.Length - }; - - form.Headers.Add(BaseHeader.Item1, BaseHeader.Item2); - form.Add(content, "file", Path.GetFileName(fileName)); - using (var res = await this.httpClient.PostAsync(url, form)) - { - // HTTP Status can either be OK or Conflict - if (!new HashSet + Name = "file", + FileName = Path.GetFileName(fileName), + Size = stream.Length + }; + + form.Add(content, "file", Path.GetFileName(fileName)); + + var res = await httpClient.PostAsync(url, form); + // HTTP Status can either be OK or Conflict + if (!new HashSet {HttpStatusCode.OK, HttpStatusCode.Created, HttpStatusCode.Conflict} - .Contains(res.StatusCode)) - { - if (res.StatusCode == HttpStatusCode.PreconditionFailed) - { - throw new Exception($"Failed to upload {fileName}"); - } - } - - var responseData = await res.Content.ReadAsStringAsync(); - var response = JObject.Parse(responseData)["detailedImportResult"]; - var successes = response["successes"]; - if (successes.HasValues) - { - return (true, long.Parse(successes[0]["internalId"].ToString())); - } - - var failures = response["failures"]; - if (!failures.HasValues) - { - throw new Exception($"Unknown error: {response}"); - } - - var messages = failures[0]["messages"]; - var code = int.Parse(messages[0]["code"].ToString()); - if (code == (int)HttpStatusCode.Accepted) - { - // Activity already exists - return (false, long.Parse(messages[0]["internalId"].ToString())); - } - - throw new Exception(messages.ToString()); - } + .Contains(res.StatusCode)) + { + if (res.StatusCode == HttpStatusCode.PreconditionFailed) + { + throw new Exception($"Failed to upload {fileName}"); } } + + var responseData = await res.Content.ReadAsStringAsync(); + var response = JObject.Parse(responseData)["detailedImportResult"]; + var successes = response["successes"]; + if (successes.HasValues) + { + _ = long.TryParse(successes[0]["internalId"].ToString(), out long internalId); + return (true, internalId); + } + + var failures = response["failures"]; + if (!failures.HasValues) + { + throw new Exception($"Unknown error: {response}"); + } + + var messages = failures[0]["messages"]; + var code = int.Parse(messages[0]["code"].ToString()); + if (code == (int)HttpStatusCode.Accepted) + { + // Activity already exists + _ = long.TryParse(successes[0]["internalId"].ToString(), out long internalId); + return (false, internalId); + } + + throw new Exception(messages.ToString()); } /// @@ -378,8 +371,7 @@ public async Task DownloadActivityFile(long activityId, ActivityFileType public async Task SetActivityName(long activityId, string activityName) { var url = $"{URL_ACTIVITY_BASE}/{activityId}"; - this.httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); - this.httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); + httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); var data = new { @@ -387,13 +379,12 @@ public async Task SetActivityName(long activityId, string activityName) activityName }; - using (var res = await this.httpClient.PostAsync(url, - new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))) + var res = await httpClient.PostAsync(url, + new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json")); + + if (!res.IsSuccessStatusCode) { - if (!res.IsSuccessStatusCode) - { - throw new Exception($"Activity name not set: {await res.Content.ReadAsStringAsync()}"); - } + throw new Exception($"Activity name not set: {await res.Content.ReadAsStringAsync()}"); } } @@ -406,7 +397,7 @@ public async Task SetActivityName(long activityId, string activityName) /// public async Task> LoadActivityTypes() { - return await this.ExecuteUrlGetRequest>(UrlActivityTypes, + return await ExecuteUrlGetRequest>(UrlActivityTypes, "Error while getting activity types"); } @@ -416,7 +407,7 @@ public async Task> LoadActivityTypes() /// public async Task> LoadEventTypes() { - return await this.ExecuteUrlGetRequest>(UrlEventTypes, + return await ExecuteUrlGetRequest>(UrlEventTypes, "Error while getting event types"); } @@ -432,9 +423,8 @@ public async Task> LoadEventTypes() public async Task SetActivityType(long activityId, ActivityType activityType) { var url = $"{URL_ACTIVITY_BASE}/{activityId}"; - this.httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); - this.httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); + httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); var data = new { @@ -442,13 +432,12 @@ public async Task SetActivityType(long activityId, ActivityType activityType) activityTypeDTO = activityType }; - using (var res = await this.httpClient.PostAsync(url, - new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))) + var res = await httpClient.PostAsync(url, + new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json")); + + if (!res.IsSuccessStatusCode) { - if (!res.IsSuccessStatusCode) - { - throw new Exception($"Activity type not set: {await res.Content.ReadAsStringAsync()}"); - } + throw new Exception($"Activity type not set: {await res.Content.ReadAsStringAsync()}"); } } @@ -461,9 +450,8 @@ public async Task SetActivityType(long activityId, ActivityType activityType) public async Task SetEventType(long activityId, ActivityType eventType) { var url = $"{URL_ACTIVITY_BASE}/{activityId}"; - this.httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); - this.httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); + httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); var data = new { @@ -471,13 +459,12 @@ public async Task SetEventType(long activityId, ActivityType eventType) eventTypeDTO = eventType }; - using (var res = await this.httpClient.PostAsync(url, - new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))) + var res = await httpClient.PostAsync(url, + new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json")); + + if (!res.IsSuccessStatusCode) { - if (!res.IsSuccessStatusCode) - { - throw new Exception($"Event type not set: {await res.Content.ReadAsStringAsync()}"); - } + throw new Exception($"Event type not set: {await res.Content.ReadAsStringAsync()}"); } } @@ -493,9 +480,8 @@ public async Task SetEventType(long activityId, ActivityType eventType) public async Task SetActivityDescription(long activityId, string description) { var url = $"{URL_ACTIVITY_BASE}/{activityId}"; - this.httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); - this.httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); + httpClient.DefaultRequestHeaders.Add("X-HTTP-Method-Override", "PUT"); var data = new { @@ -503,13 +489,12 @@ public async Task SetActivityDescription(long activityId, string description) description }; - using (var res = await this.httpClient.PostAsync(url, - new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))) + var res = await httpClient.PostAsync(url, + new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json")); + + if (!res.IsSuccessStatusCode) { - if (!res.IsSuccessStatusCode) - { - throw new Exception($"Activity description not set: {await res.Content.ReadAsStringAsync()}"); - } + throw new Exception($"Activity description not set: {await res.Content.ReadAsStringAsync()}"); } } @@ -524,9 +509,8 @@ public async Task SetActivityDescription(long activityId, string description) public async Task LoadActivity(long activityId) { var url = $"{URL_ACTIVITY_BASE}/{activityId}"; - this.httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); - return await this.ExecuteUrlGetRequest(url, "Error while getting activity"); + return await ExecuteUrlGetRequest(url, "Error while getting activity"); } /// @@ -564,9 +548,8 @@ private static string CreateActivitiesUrl(int limit, int start, DateTime date) public async Task> LoadActivities(int limit, int start, DateTime from) { var url = CreateActivitiesUrl(limit, start, from); - this.httpClient.DefaultRequestHeaders.Add(BaseHeader.Item1, BaseHeader.Item2); - return await this.ExecuteUrlGetRequest>(url, "Error while getting activities"); + return await ExecuteUrlGetRequest>(url, "Error while getting activities"); } private static T DeserializeData(string data) where T : class @@ -583,7 +566,7 @@ private static T DeserializeData(string data) where T : class /// private async Task ExecuteUrlGetRequest(string url, string errorMessage) where T : class { - var res = await this.httpClient.GetAsync(url); + var res = await httpClient.GetAsync(url); var data = await res.Content.ReadAsStringAsync(); if (!res.IsSuccessStatusCode) { @@ -598,13 +581,13 @@ private async Task ExecuteUrlGetRequest(string url, string errorMessage) w /// ~Client() { - if (this.httpClient == null) + if (httpClient == null) { return; } - this.httpClient.Dispose(); - this.httpClient = null; + httpClient.Dispose(); + httpClient = null; } } } diff --git a/GarminConnectClient.Lib/Services/Downloader.cs b/GarminConnectClient.Lib/Services/Downloader.cs index 3ca50a8..90d5fa9 100644 --- a/GarminConnectClient.Lib/Services/Downloader.cs +++ b/GarminConnectClient.Lib/Services/Downloader.cs @@ -1,13 +1,13 @@ -using GarminConnectClient.Lib.Dto; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using GarminConnectClient.Lib.Dto; using GarminConnectClient.Lib.Enum; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace GarminConnectClient.Lib.Services { @@ -113,14 +113,14 @@ public async Task> DownloadLastUserActivities() public async Task GetAndStoreGpsData(long activityId, ActivityFileTypeEnum fileFormat, string fileName) { - this.logger.LogInformation($"Downloading of GPS data in {fileFormat.ToString()} format started."); + this.logger.LogInformation("Downloading of GPS data in {fileFormat} format started.", fileFormat); using (var data = await this.client.DownloadActivityFile(activityId, fileFormat)) { //data.Position = 0; await this.storage.StoreData(fileName, data); } - this.logger.LogInformation($"Downloading of GPS data in {fileFormat.ToString()} format done."); + this.logger.LogInformation("Downloading of GPS data in {fileFormat} format done.", fileFormat); } /// @@ -156,7 +156,7 @@ public async Task DownloadActivity(Activity activity, bool checkFileEx { if (await this.storage.FileExists(activityDataFile)) { - this.logger.LogInformation($"Activity {activity.ActivityId} is already downloaded."); + this.logger.LogInformation("Activity {activity.ActivityId} is already downloaded.", activity.ActivityId); return null; } } @@ -171,14 +171,12 @@ public async Task DownloadActivity(Activity activity, bool checkFileEx this.logger.LogInformation("Saving of activity data done."); this.logger.LogInformation("Saving of activity media started."); - foreach (var image in activity.Metadata.ActivityImages ?? new ActivityImage[0]) + foreach (var image in activity.Metadata.ActivityImages ?? Array.Empty()) { - using (var httpClient = new HttpClient()) - { - var data = await httpClient.GetByteArrayAsync(image.Url); - var urlParts = image.Url.ToString().Split('/'); - await this.storage.StoreData(CreateFileName(mediaDir, urlParts[urlParts.Length - 1]), data); - } + using var httpClient = new HttpClient(); + var data = await httpClient.GetByteArrayAsync(image.Url); + var urlParts = image.Url.ToString().Split('/'); + await this.storage.StoreData(CreateFileName(mediaDir, urlParts[^1]), data); } this.logger.LogInformation("Saving of activity media done."); @@ -193,7 +191,7 @@ public async Task DownloadActivity(Activity activity, bool checkFileEx await this.GetAndStoreGpsData(activity.ActivityId, exportFormat, CreateFileName(activityDir, this.CreateGpsFileMapName(exportFormat))); } - this.logger.LogInformation($"Downloading of activity {activity.ActivityId} done."); + this.logger.LogInformation("Downloading of activity {activity.ActivityId} done.", activity.ActivityId); return activity; } @@ -207,13 +205,13 @@ public async Task DownloadActivity(Activity activity, bool checkFileEx /// public async Task DownloadActivity(long activityId) { - this.logger.LogInformation($"Downloading of activity {activityId} started."); + this.logger.LogInformation("Downloading of activity {activityId} started.", activityId); var activityDir = string.IsNullOrWhiteSpace(this.configuration.BackupDir) ? activityId.ToString() : Path.Combine(this.configuration.BackupDir, activityId.ToString()); var activityDataFile = CreateFileName(activityDir, ActivityDataFile); if (await this.storage.FileExists(activityDataFile)) { - this.logger.LogInformation($"Activity {activityId} is already downloaded."); + this.logger.LogInformation("Activity {activityId} is already downloaded.", activityId); return null; }