Created
May 20, 2026 14:39
-
-
Save ndeloof/bb5a331a0ae58f627bd998904427603d to your computer and use it in GitHub Desktop.
Garmin workout cleanup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (async () => { | |
| const DRY_RUN = true; | |
| const PAGE_WAIT = 800; | |
| const DELETE_DELAY = 300; | |
| const MAX_PAGES = 50; | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| const csrf = document.querySelector('meta[name="csrf-token"]')?.content; | |
| if (!csrf) throw new Error('CSRF token introuvable dans <meta name="csrf-token">'); | |
| function readWorkoutsFromTbody(tbody) { | |
| const k = Object.keys(tbody).find(x => x.startsWith('__reactFiber')); | |
| if (!k) return null; | |
| let fiber = tbody[k]; | |
| for (let i = 0; i < 30 && fiber; i++) { | |
| const p = fiber.memoizedProps; | |
| if (p && Array.isArray(p.workouts) && p.workouts.length && p.workouts[0].workoutId) { | |
| return p.workouts; | |
| } | |
| fiber = fiber.return; | |
| } | |
| return null; | |
| } | |
| function findWorkoutsTbody() { | |
| for (const tb of document.querySelectorAll('tbody')) { | |
| if (readWorkoutsFromTbody(tb)) return tb; | |
| } | |
| return null; | |
| } | |
| function readPageState() { | |
| const wrap = document.querySelector('.Pagination_wrapper__mYZ9R'); | |
| if (!wrap) return { current: 1, total: 1 }; | |
| const m = wrap.innerText.match(/(\d+)\s*\D+\s*(\d+)/); | |
| return m ? { current: +m[1], total: +m[2] } : { current: 1, total: 1 }; | |
| } | |
| function clickNext() { | |
| const svg = document.querySelector('.Pagination_wrapper__mYZ9R svg[aria-label="Suivant"]'); | |
| const btn = svg && svg.closest('button'); | |
| if (!btn || btn.disabled) return false; | |
| btn.click(); | |
| return true; | |
| } | |
| async function collectAll() { | |
| const seen = new Map(); | |
| let pages = 0; | |
| while (pages < MAX_PAGES) { | |
| const tbody = findWorkoutsTbody(); | |
| if (!tbody) throw new Error('Tbody « Mes entraînements » introuvable'); | |
| const workouts = readWorkoutsFromTbody(tbody) || []; | |
| for (const w of workouts) { | |
| if (!seen.has(w.workoutId)) seen.set(w.workoutId, w.workoutName); | |
| } | |
| const { current, total } = readPageState(); | |
| console.log(`page ${current}/${total} — ${workouts.length} workouts (cumulé : ${seen.size})`); | |
| if (current >= total) break; | |
| if (!clickNext()) break; | |
| const before = current; | |
| let waited = 0; | |
| while (waited < 5000 && readPageState().current === before) { | |
| await sleep(150); | |
| waited += 150; | |
| } | |
| await sleep(PAGE_WAIT); | |
| pages++; | |
| } | |
| return [...seen.entries()].map(([id, name]) => ({ id, name })); | |
| } | |
| console.log('Collecte…'); | |
| const all = await collectAll(); | |
| console.log(`Total : ${all.length} workout(s)`); | |
| console.table(all); | |
| if (DRY_RUN) { | |
| console.warn('DRY_RUN actif — passe-le à false pour supprimer.'); | |
| return all; | |
| } | |
| const results = []; | |
| for (const { id, name } of all) { | |
| const res = await fetch(`https://connect.garmin.com/gc-api/workout-service/workout/${id}`, { | |
| method: 'DELETE', | |
| credentials: 'include', | |
| headers: { | |
| 'accept': '*/*', | |
| 'connect-csrf-token': csrf, | |
| }, | |
| }); | |
| results.push({ id, name, ok: res.ok, status: res.status }); | |
| console.log(res.ok ? `✓ ${id} — ${name}` : `✗ ${id} — ${name} (${res.status})`); | |
| await sleep(DELETE_DELAY); | |
| } | |
| const ok = results.filter(r => r.ok).length; | |
| console.log(`Terminé. ok=${ok} ko=${results.length - ok}`); | |
| return results; | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment