Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
482 changes: 382 additions & 100 deletions README.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"id": "strava-sync",
"name": "Strava Sync",
"version": "1.1.0",
"version": "1.1.3",
"minAppVersion": "0.15.0",
"description": "Sync activities from Strava.",
"author": "Howard Wilson",
"authorUrl": "https://github.com/watsonbox",
"myForkUrl": "https://github.com/Janxyxy/obsidian-strava-sync",
"isDesktopOnly": false
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@types/mustache": "^4.2.5",
"@types/node": "^16.11.6",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"esbuild": "^0.25.10",
"jest": "^29.7.0",
"obsidian": "latest",
"ts-jest": "^29.2.5",
Expand Down
33 changes: 24 additions & 9 deletions src/ActivityImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,34 @@ export class ActivityImporter {

const activities = await this.stravaApi.listActivities(params);

const detailedActivities = await Promise.all(
activities.map(async (activity: any) => {
const detailedActivity = await this.stravaApi.getActivity(
activity.id,
);
return this.mapStravaActivityToActivity(detailedActivity);
}),
);
// Process activities with rate limiting to avoid 429 errors
const detailedActivities = [];
for (const activity of activities) {
try {
const detailedActivity = await this.stravaApi.getActivity(activity.id);
detailedActivities.push(this.mapStravaActivityToActivity(detailedActivity));

// Add a small delay between requests to respect rate limits
// Strava allows 100 requests per 15 minutes, so ~400ms between requests is safe
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
if (error.message?.includes('429')) {
console.warn(`Rate limit hit while fetching activity ${activity.id}. Stopping import.`);
break; // Stop processing more activities if we hit rate limit
}
throw error; // Re-throw other errors
}
}

return detailedActivities;
} catch (error) {
console.error("Error fetching activities from Strava:", error);
throw new Error("Failed to import activities from Strava");

if (error.message?.includes('429')) {
throw new Error("Strava API rate limit exceeded. Please wait 15 minutes before trying again, or disable 'Rewrite existing activities' to import only new activities.");
}

throw new Error(`Failed to import activities from Strava: ${error.message || error}`);
}
}

Expand Down
21 changes: 13 additions & 8 deletions src/ActivitySerializer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type App, TFolder, normalizePath } from "obsidian";
import { type App, TFile, TFolder, normalizePath } from "obsidian";
import type { Activity } from "./Activity";
import { ActivityRenderer } from "./ActivityRenderer";
import type { Settings } from "./Settings";
Expand Down Expand Up @@ -46,16 +46,21 @@ export class ActivitySerializer {
this.settings.activity.frontMatterProperties,
).render(activity);

const existingFile = this.app.vault.getAbstractFileByPath(filePath);

try {
await this.app.vault.create(filePath, fileContent);
} catch (error) {
if (error.toString().includes("File already exists")) {
return false;
if (existingFile && existingFile instanceof TFile) {
// File exists, update it
await this.app.vault.modify(existingFile, fileContent);
return false; // File was updated, not created
} else {
// File doesn't exist, create it
await this.app.vault.create(filePath, fileContent);
return true; // File was created
}

} catch (error) {
console.error(`Error serializing activity to ${filePath}:`, error);
throw error;
}

return true;
}
}
2 changes: 2 additions & 0 deletions src/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const DEFAULT_SETTINGS: Settings = {
folderDateFormat: "yyyy-MM-dd",
filename: "{{id}} {{name}}",
filenameDateFormat: "yyyy-MM-dd",
rewriteExistingActivities: false,
},
activity: {
contentDateFormat: "yyyy-MM-dd HH:mm:ss",
Expand Down Expand Up @@ -65,6 +66,7 @@ interface SyncSettings {
filename: string;
filenameDateFormat: string;
lastActivityTimestamp?: number;
rewriteExistingActivities: boolean;
}

interface ActivitySettings {
Expand Down
14 changes: 14 additions & 0 deletions src/SettingsTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,20 @@ export class SettingsTab extends PluginSettingTab {
}),
);

new Setting(containerEl)
.setName("Rewrite existing activities")
.setDesc(
"When enabled, all activities will be downloaded and rewritten, ignoring the last activity timestamp. This will download all activities from Strava, even if they were previously imported. Warning: This may hit Strava's rate limits (100 requests per 15 minutes) if you have many activities."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.sync.rewriteExistingActivities)
.onChange(async (value) => {
this.plugin.settings.sync.rewriteExistingActivities = value;
await this.plugin.saveSettings();
}),
);

new Setting(containerEl).setName("Activity").setHeading();

new Setting(containerEl)
Expand Down
12 changes: 7 additions & 5 deletions src/StravaSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,12 @@ export default class StravaSync extends Plugin {

this.activities = await new ActivityImporter(
this.stravaApi,
this.settings.sync.lastActivityTimestamp,
this.settings.sync.rewriteExistingActivities ? undefined : this.settings.sync.lastActivityTimestamp,
).importLatestActivities();

await this.serializeActivities(true);
await this.serializeActivities(!this.settings.sync.rewriteExistingActivities);

if (this.activities.length > 0) {
if (this.activities.length > 0 && !this.settings.sync.rewriteExistingActivities) {
this.settings.sync.lastActivityTimestamp = Math.max(
...this.activities.map(
(activity) => activity.start_date.getTime() / 1000,
Expand All @@ -131,8 +131,9 @@ export default class StravaSync extends Plugin {
await this.saveSettings();
} catch (error) {
console.error("Unexpected error during Strava import:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
new Notice(
"🛑 An unexpected error occurred during import. Check the console for details.",
`🛑 Import failed: ${errorMessage}. Check the console for full details.`,
ERROR_NOTICE_DURATION,
);
}
Expand Down Expand Up @@ -163,7 +164,8 @@ export default class StravaSync extends Plugin {
let message = `🏃 ${createdCount} ${newLabel ? "new " : ""}activities created`;

if (updatedCount > 0) {
message += `, ${updatedCount} already existing`;
const action = this.settings.sync.rewriteExistingActivities ? "updated" : "already existing";
message += `, ${updatedCount} ${action}`;
}

new Notice(`${message}.`, SUCCESS_NOTICE_DURATION);
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/ActivitySerializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe("ActivitySerializer", () => {
folderDateFormat: "yyyy-MM-dd",
filename: "{{id}} {{name}}",
filenameDateFormat: "yyyy-MM-dd",
rewriteExistingActivities: false,
},
activity: {
contentDateFormat: "yyyy-MM-dd HH:mm:ss",
Expand Down
Loading