Skip to content

Commit f11c7ab

Browse files
committed
initial
1 parent 2ebdb67 commit f11c7ab

14 files changed

+546
-2
lines changed

CatalogTcx.csproj

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp3.1</TargetFramework>
6+
<RootNamespace>catalog_tcx</RootNamespace>
7+
<Authors>Steven M. Cohn</Authors>
8+
<Company>River Software</Company>
9+
<Description>Organize and catalog Garmin TCX files</Description>
10+
<Copyright>Copyright © 2010 Steven M Cohn</Copyright>
11+
<PackageLicenseExpression>MIT License</PackageLicenseExpression>
12+
<PackageIcon>Garmin.ico</PackageIcon>
13+
<PackageIconUrl />
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<Folder Include="assets\" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<None Include="assets\Garmin.ico">
22+
<Pack>True</Pack>
23+
<PackagePath></PackagePath>
24+
</None>
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<PackageReference Include="System.Management" Version="4.7.0" />
29+
</ItemGroup>
30+
31+
</Project>

CatalogTcx.sln

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29613.14
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CatalogTcx", "CatalogTcx.csproj", "{F9E800B4-AC6E-44B6-8248-83FFAD3D031B}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{F9E800B4-AC6E-44B6-8248-83FFAD3D031B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{F9E800B4-AC6E-44B6-8248-83FFAD3D031B}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{F9E800B4-AC6E-44B6-8248-83FFAD3D031B}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{F9E800B4-AC6E-44B6-8248-83FFAD3D031B}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {65B57550-BDD6-4B51-AD6E-5C4213AE4B2E}
24+
EndGlobalSection
25+
EndGlobal

Program.cs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//************************************************************************************************
2+
// Copyright © 2010 Steven M. Cohn. All Rights Reserved.
3+
//************************************************************************************************
4+
5+
namespace CatalogTcx
6+
{
7+
using System;
8+
using System.Collections.Generic;
9+
using System.IO;
10+
using System.Linq;
11+
using System.Xml.Linq;
12+
13+
14+
class Program
15+
{
16+
/// <summary>
17+
/// blah blah blah
18+
/// </summary>
19+
/// <param name="args">blah blah blah</param>
20+
21+
static void Main(string[] args)
22+
{
23+
var path = (args.Length == 0 ? "." : args[0]);
24+
25+
// scan for tcx files in the current or specified path
26+
Console.WriteLine();
27+
Console.WriteLine("... scanning files in current directory");
28+
29+
var count = ScanFiles(path, path, true);
30+
Console.WriteLine("... found: " + count.ToString());
31+
32+
// determine if a Garmin USB is mounted
33+
var disks = new UsbFactory().GetAvailableDisks();
34+
if ((disks != null) && (disks.Count > 0))
35+
{
36+
var disk = disks.FirstOrDefault(
37+
e => e.Model.StartsWith("Garmin", StringComparison.InvariantCulture));
38+
39+
if (disk != null)
40+
{
41+
// scan for tcx files on the Garmin, disk.Name would be simply like "G:"
42+
43+
// Edge 820 stores fit files in Garmin\Activities
44+
var sourcePath = disk.Name + @"\Garmin\Activities";
45+
if (!Directory.Exists(sourcePath))
46+
{
47+
// Edge 705 stores tcx files in Garmin\History
48+
sourcePath = disk.Name + @"\Garmin\History";
49+
}
50+
51+
if (Directory.Exists(sourcePath))
52+
{
53+
Console.WriteLine();
54+
Console.WriteLine("... scanning files in " + sourcePath);
55+
count = ScanFiles(sourcePath, path, false);
56+
Console.WriteLine("... found: " + count);
57+
}
58+
}
59+
}
60+
else
61+
{
62+
Console.WriteLine();
63+
Console.WriteLine("... Garmin not found");
64+
}
65+
66+
// scan for Zwift files
67+
Console.WriteLine();
68+
Console.WriteLine("... scanning for Zwift files");
69+
count += ScanZwift(path, true);
70+
Console.WriteLine("... found: " + count);
71+
72+
Console.WriteLine();
73+
Console.Write("... Press any key: ");
74+
Console.ReadKey();
75+
}
76+
77+
78+
private static int ScanFiles(string sourcePath, string targetPath, bool clean)
79+
{
80+
var count = 0;
81+
var dirnam = Path.GetDirectoryName(targetPath);
82+
if (dirnam == null) return 0;
83+
84+
foreach (var filnam in GetFiles(sourcePath, new[] { ".fit", ".tcx" }))
85+
{
86+
var ext = Path.GetExtension(filnam);
87+
if (ext.Equals(".fit"))
88+
{
89+
// TODO: convert fit to tcx
90+
// TODO: set filnam to converted tcx file
91+
}
92+
93+
// open each TCX file and look for its activity start time
94+
95+
var root = XElement.Load(filnam);
96+
var ns = root.GetDefaultNamespace();
97+
98+
var lap = (from e in root
99+
.Element(ns + "Activities")?
100+
.Element(ns + "Activity")?
101+
.Elements(ns + "Lap")
102+
where e.Attribute("StartTime") != null
103+
select e).FirstOrDefault();
104+
105+
var startTime = lap?.Attribute("StartTime");
106+
if (startTime == null) continue;
107+
108+
// parse the start time so we can reformat into a filename
109+
var dttm = DateTime.Parse(startTime.Value);
110+
111+
// the year is also used as a directory name
112+
var year = dttm.Year.ToString("0000");
113+
114+
// build the final file name
115+
var stamp = $"{year}{dttm.Month:00}{dttm.Day:00}_{dttm.Hour:00}{dttm.Minute:00}.tcx";
116+
117+
// ensure the archive directory (year name) exists
118+
var dirpath = Path.Combine(dirnam, year);
119+
if (!Directory.Exists(dirpath))
120+
{
121+
Directory.CreateDirectory(dirpath);
122+
}
123+
124+
var target = Path.Combine(dirpath, stamp);
125+
if (File.Exists(target))
126+
{
127+
// delete target so there's no problem overwriting it
128+
File.Delete(target);
129+
}
130+
131+
// save as formatted XML
132+
root.Save(target, SaveOptions.None);
133+
Console.WriteLine(" " + Path.GetFileName(filnam) + " --> " + Path.GetFileName(target));
134+
count++;
135+
136+
// set the timestamps of the file to the activity start time
137+
File.SetCreationTime(target, dttm);
138+
File.SetLastWriteTime(target, dttm);
139+
140+
if (clean)
141+
{
142+
// delete the source
143+
File.Delete(filnam);
144+
}
145+
}
146+
147+
return count;
148+
}
149+
150+
// gpsbabel -t -i garmin_fit
151+
// -f C:/Users/steven/Documents/Zwift/Activities/2016-12-08-18-33-29.fit
152+
// -o gtrnctr,course=0,sport=Biking
153+
// -F C:/Users/steven/Desktop/qwe.tcx
154+
155+
private static int ScanZwift(string targetPath, bool clean)
156+
{
157+
var dirnam = Path.GetDirectoryName(targetPath);
158+
if (dirnam == null) return 0;
159+
160+
var sourcePath = Path.Combine(
161+
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
162+
@"Zwift\Activities");
163+
164+
if (!Directory.Exists(sourcePath)) return 0;
165+
166+
// the year is also used as target directory name
167+
var year = DateTime.Now.Year.ToString("0000");
168+
// ensure the archive directory (year name) exists
169+
var dirpath = Path.Combine(targetPath, year);
170+
if (!Directory.Exists(dirpath))
171+
{
172+
Directory.CreateDirectory(dirpath);
173+
}
174+
175+
var count = 0;
176+
177+
foreach (var filnam in GetFiles(sourcePath, new[] { ".fit" }))
178+
{
179+
var name = Path.GetFileName(filnam);
180+
if (name != null)
181+
{
182+
var target = Path.Combine(dirpath, name);
183+
if (File.Exists(target))
184+
{
185+
// delete target so there's no problem overwriting it
186+
File.Delete(target);
187+
}
188+
189+
if (clean)
190+
{
191+
File.Move(filnam, target);
192+
}
193+
else
194+
{
195+
196+
File.Copy(filnam, target);
197+
}
198+
199+
count++;
200+
201+
// set the timestamps of the file to the activity start time
202+
//File.SetCreationTime(target, dttm);
203+
//File.SetLastWriteTime(target, dttm);
204+
}
205+
}
206+
207+
return count;
208+
}
209+
210+
211+
private static IEnumerable<string> GetFiles(string path, string[] types)
212+
{
213+
return Directory.GetFiles(path)
214+
.Where(file => types.Any(t => t.Equals(Path.GetExtension(file)))).ToList();
215+
}
216+
}
217+
}

README.md

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,81 @@
1-
# CatalogTcx
2-
Organizes Garmin Track .tcx files into a catalog of folders
1+
2+
<h1>Catalog TCX</h1>
3+
4+
Organizes Garmin Track .tcx files into a catalog of folders.
5+
6+
* Files are renamed according to their internal activity date and time to comply with the format yyyymmdd_hhmm.tcx, making them easy to sort and find.
7+
* The timestamp on the file is set to reflect the activity date and time.
8+
* Files are stored in yearly folders (\2020, \2019, \2018, etc.) so they can be easily Zipped and archived.
9+
10+
Discovers Track files from:
11+
12+
1. The current folder
13+
1. A connected Garmin device (USB)
14+
1. Your Zwift user folder (MyDocuments\Zwift\Activities)
15+
16+
```
17+
MyDocuments
18+
\2018
19+
\2019
20+
20191201_0813.tcx
21+
20191202_0504.tcx
22+
20191203_0522.tcx
23+
20191204_0617.tcx
24+
\2020
25+
20200101_1154.tcx
26+
20200102_1656.tcx
27+
20200104_1645.tcx
28+
```
29+
30+
<h2>How to run</h2>
31+
32+
Download the latest release and copy to the folder of your choice and run the executable.
33+
34+
Or, download the source, compile with Visual Studio, VSCode, or the dotnet command line.
35+
36+
<h1>Download Individual Strava Activity as TCX File</h1>
37+
38+
Strava lets you download any activity as a .tcx simply by appending /export_tcx to the activity URL.
39+
40+
For example, given the activity URL:
41+
42+
https://www.strava.com/activities/2590236689
43+
44+
Append /export_tcx to this to download its .tcx file:
45+
46+
https://www.strava.com/activities/2590236689/export_tcx
47+
48+
<h1>Bulk Download Strava Activities as TCX Files</h1>
49+
50+
If you're tech-savvy (or just adventerous) then it is possible to download up to 20 activities at a time from the Strava activities page using the Google Chrome Web browser.
51+
52+
First, you'll need to create a new code Snippet in Chrome.
53+
54+
1. Open Chrome and press F12. This will display the developer tools pannel.
55+
2. Click the Sources tab
56+
3. Click the Snippets tab
57+
4. Click + New Snippet
58+
5. Name the snippet "Strava activities downloader"
59+
6. Paste the following code into the source code panel and press Ctrl-S to save it
60+
```
61+
var links = jQuery("a[data-field-name='name']");
62+
for (var i=0; i < links.length; i++) {
63+
if (links[i].href.indexOf('export_tcx') < 0) {
64+
links[i].href = links[i].href + '/export_tcx';
65+
}
66+
links[i].download = "activity" + i + ".tcx";
67+
window.setTimeout(function(link) {
68+
console.log('downloading', link.href, link.download);
69+
link.click();
70+
}, 1000 * i, links[i]);
71+
}
72+
```
73+
<img src="assets/chrome-snippet.png" width="790" height="312"/>
74+
75+
Navigate to your activities page. Enter any desired filters to find the data you want. Note that only 20 activities are displayed at a time so you need to run this multiple times if you want more data.
76+
77+
Press Ctrl-Enter (or click the >Ctrl+Enter link at the bottom of the source panel) to execute the snippet.
78+
79+
Each file will be downloaded, one per second so wait for them all to finish before moving on.
80+
81+
Move to the next page of activities and re-run script, repeat as necessary.

0 commit comments

Comments
 (0)