Skip to content

Liki4/keep_virtual_ride_to_strava

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Keep → Strava

每天两次定时把 Keep 上的室内骑行训练记录同步到 Strava,跑在 GitHub Actions 上,无服务器、零运维。

Fork → 配置 6 个 secrets → 触发首次同步即可,全程约 15 分钟。


这是什么

Keep 是国产室内骑行平台,但没有官方的 Strava 集成。这个项目做的事:

  1. 模拟 Android 客户端调 Keep 内部 API,把训练记录的功率、心率、速度、踏频、阻力时间序列拉下来
  2. 重建成 1Hz 数据(PCHIP 单调三次样条插值,把 1s/5s/15s 混采样统一到秒级)
  3. 生成 Garmin 兼容的 FIT 文件sport=cycling, sub_sport=virtual_activity → Strava 识别为 "Virtual Ride")
  4. 上传到 Strava,自动去重,描述里附阻力分布

部署到 GitHub Actions 之后每天 12:00 / 00:00 Asia/Shanghai 自动跑,全程无人值守。


主要特性

  • 三层去重external_id / start_time (±5s) / 本地 FIT 缓存,重跑不会重复传,也不会浪费 Keep API
  • 阻力可视化:阻力档位变化用 FIT 的 REAR_GEAR_CHANGE 事件编码——这正是 Di2/eTap 电子变速器写入 FIT 的方式,Strava 自带渲染逻辑,能画成阻力时间线
  • 断点续跑:CI 中途失败、网络抖动都没关系,下次自动接着跑,已传的会被跳过
  • 凭据隔离:全走 GitHub Action Secrets,仓库代码里没有任何凭据
  • 可选 gear 绑定STRAVA_GEAR_ID 让同步过来的活动自动挂到 Strava 上指定的单车装备
  • 个人备注追溯:每个上传的活动 private_note 里写 Keep 的 sport_id,方便对账

工作流程

┌───────────┐     ┌─────────────────┐     ┌──────────────┐
│ Keep API  │ ──▶ │ PCHIP 重采样到   │ ──▶ │ Strava API   │
│ (4 endpoints)   │ 1Hz + FIT 编码  │     │ upload+PUT   │
└───────────┘     └─────────────────┘     └──────────────┘
     │                    │                       │
     │                    │                       │
  cookie+token         step_hold              external_id
  spider sleep         (阻力非连续)             dedup 索引
  分页 sport list

每个 workout 的处理(见 process_workout):

步骤 动作 何时跳过
1 查 Strava external_id 索引 sport_id 已存在 → 不调 Keep
2 get_sport_data 拿 startTime
3 按 startTime 算 FIT 文件名
4 查 Strava start_time 索引(±5s) 时间匹配 → 不调 fence/chart
5 查本地 FIT_OUT/{filename} 本地有 → 不调 fence/chart
6 get_sport_fence + get_sport_chart
7 PCHIP / step-hold 重采样到 1Hz
8 写 FIT 文件
9 上传 + PUT 元数据

部署

步骤 1:Fork 这个仓库

点右上角 Fork 按钮,把仓库 fork 到你自己的账号。

强烈建议把你的 fork 设成 private:Fork 完之后到 Settings → General → Danger Zone → Change visibility → Make private。代码里没有凭据,但 workflow 跑起来的 logs 会包含你的训练时间、sport_id 等数据,私库更稳。

步骤 2:在 Strava 创建 OAuth app 并拿 refresh token

strava.com/settings/api 创建 app,记下 Client IDClient Secret。Authorization Callback Domain 填 localhost 即可(只用于一次性拿 refresh_token)。

然后走一次 OAuth:

# 浏览器打开(替换 CLIENT_ID)
open "https://www.strava.com/oauth/authorize?client_id=CLIENT_ID&redirect_uri=http://localhost&response_type=code&scope=activity:write,activity:read_all&approval_prompt=force"

# 授权后浏览器跳到 http://localhost/?code=XXXXX&...
# 页面会显示连接失败 (因为本地没有 localhost server),但 URL 里的 code 就是要的,复制下来

# 用 code 换 refresh_token(三处都要替换)
curl -s -X POST https://www.strava.com/oauth/token \
  -d client_id=CLIENT_ID \
  -d client_secret=CLIENT_SECRET \
  -d code=CODE \
  -d grant_type=authorization_code

返回 JSON 里 refresh_token 字段就是要存的(不是 access_token,那个 6 小时就过期)。

Scope 必须同时包含 activity:write(上传)和 activity:read_all(dedup 用),缺一不可。如果你以前已经授权过这个 app 但 scope 不全,加 approval_prompt=force 会强制重新授权。

步骤 3:(可选)找你的 Strava bike gear_id

如果想让同步过来的活动自动挂到你 Strava 的某辆 bike 装备上:

网页strava.com/settings/gear → 点你的单车 → URL 是 https://www.strava.com/bikes/12345678 → gear_id 就是 b12345678记得加 b 前缀

API

curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  https://www.strava.com/api/v3/athlete | jq '.bikes'

不需要的话跳过即可,少配一个 secret。

备注:如果你 Strava 账号里有"默认装备"设置(在 Settings → My Gear 里把某辆 bike 标为 default),新上传的活动会自动挂上去;这种情况下也不需要 STRAVA_GEAR_ID

步骤 4:配置 GitHub Secrets

打开你的 fork 仓库 → Settings → Secrets and variables → Actions → New repository secret,依次加:

Name 必需?
KEEP_PHONE Keep 账号手机号
KEEP_PASSWORD Keep 账号密码
STRAVA_CLIENT_ID Strava app client id
STRAVA_CLIENT_SECRET Strava app client secret
STRAVA_REFRESH_TOKEN 步骤 2 拿到的 refresh token
STRAVA_GEAR_ID b12345678 这种格式 可选

加完后 Secrets 页面只看得到名字、看不到值(GitHub 加密保存)—— 正常。

步骤 5:启用并触发 workflow

Fork 过来的仓库默认 scheduled workflows 是禁用的(GitHub 防滥用机制),需要手动启用一次:

  1. 进 fork 的 Actions tab
  2. 点中间的 "I understand my workflows, go ahead and enable them" 按钮(如果有)
  3. 左侧栏点 Sync Keep to Strava
  4. 右上 Run workflowRun workflow 触发首次同步

首次运行 10–15 分钟(全量同步),之后每次 30 秒左右(只处理新 workout)。

步骤 6:验证

strava.com/athlete/training,新上传的活动 description 应该长这样:

从 Keep 同步
sport_id: 673e065bc0d6dd0001890281_9223370...
阻力: 平均 5 / 最大 6 / 最小 2
阻力时长: lv6 30min(41%) · lv5 19min(26%) · lv4 10min(15%) · lv3 9min(12%) · ...

类型应该是 VirtualRide,挂在你指定的装备上。点进去能看到功率、心率、踏频、速度,以及 Gears(阻力档位)几张图。


配置参考

所有 CLI 参数都有同名大写 env var fallback,本地开发可以走 env,CI 走 args(workflow YAML 里已经配好):

CLI arg Env var 必需? 说明
--keep-phone KEEP_PHONE Keep 账号手机号
--keep-password KEEP_PASSWORD Keep 账号密码
--strava-client-id STRAVA_CLIENT_ID Strava app client id
--strava-client-secret STRAVA_CLIENT_SECRET Strava app client secret
--strava-refresh-token STRAVA_REFRESH_TOKEN 不会过期(除非主动 revoke)
--strava-gear-id STRAVA_GEAR_ID 可选 形如 b12345678
--out-dir 可选 FIT 输出目录,默认 FIT_OUT/
--limit 可选 上限新处理 workout 数(仅 debug 用)

Strava 三个核心凭据是 all-or-nothing:齐了就走 Strava 上传,全不给就只生 FIT 不上传(local debug 模式)。


本地开发

如果你想改代码、调试、或者不想等 CI:

git clone <你 fork 的 URL>
cd keep-to-strava
pip install -r requirements.txt

# 烟雾测试一个 workout
python3 export.py \
  --keep-phone "<phone>" --keep-password "<password>" \
  --strava-client-id "<id>" --strava-client-secret "<secret>" \
  --strava-refresh-token "<token>" \
  --strava-gear-id "<b12345678>" \
  --limit 1

需要 Python ≥ 3.10(脚本里有 match/case 语法)。


后续维护

自动化

每天 04:00 UTC(12:00 Shanghai)和 16:00 UTC(00:00 Shanghai)自动跑。GitHub Actions cron 可能延迟最多 30 分钟,正常。

60 天活跃度规则

GitHub 自动禁用 60 天没 commit 活动的仓库的 scheduled workflow。每两个月手动 push 一次哪怕是 README 改个字就能续命,或者直接到 Actions 页面手动 trigger 一次也行。

Strava refresh token 失效

理论上不会过期不会轮换。万一某次 refresh 调用返回 401,重做一次 OAuth 拿新 refresh_token、更新 STRAVA_REFRESH_TOKEN 这一个 secret 就行。

同步上游更新

原仓库有 bug 修复或新功能时:

git remote add upstream https://github.com/<原仓库 owner>/keep-to-strava.git
git fetch upstream
git merge upstream/main
git push

或者在 GitHub 网页上直接点 fork 页面的 "Sync fork" 按钮。

看历史 run

Actions tab 默认保留 90 天的 run logs。每次都是 ~10–60 秒(首次 15 分钟后稳定下来)。


翻车应急

症状 处理
Strava OAuth refresh failed (400) refresh token 过期/撤销/scope 不对,重做 OAuth
Strava upload 报"duplicate of activity" 不会终止 run,已被自动当成 success 处理;可以忽略
整个 run 红了,stack trace 是 Keep 那边 Keep 改 API 了或者你的账号被风控,等等再看
个别 workout failed,其它都成功 看具体 ERROR 行,多半是该 workout 数据有新的 schema 怪癖
某次 cron 没跑 GitHub 偶尔延迟/丢运行,下一次 cron 会自动接上(resumable 设计)
warn: activity update returned 4xx STRAVA_GEAR_ID 格式错了,应该是 b + 数字(如 b12345678
同一个 sport_id 一直被 skip 但 Strava 上看不到 你之前手动传过同一个 FIT,被 Strava 自动归类成 Ride 类型,名字是「上午/下午/傍晚骑行」之类默认值;手动找到删了,下次 CI 会重传成 VirtualRide
Fork 后 Actions tab 没看到 workflow GitHub 默认禁用 fork 的 scheduled workflows,进 Actions tab 手动启用一下

技术细节

更深入的架构说明、数据格式怪癖、为什么用 PCHIP 不用别的插值方法、为什么 sub_sport 选 virtual_activity 而不是 indoor_cycling、调试过程中踩过的所有坑——都在 CLAUDE.md 里。

法律声明

本项目通过模拟 Keep 的 Android 客户端调用其内部 API。你需要使用自己的 Keep 账号凭据;用于个人数据导出,不要用于任何商业或大规模爬取场景。Keep 平台政策变化或 API 调整可能导致本项目失效,作者不承担相关责任。

About

一个 GitHub Actions 上跑的定时任务,每天两次把 Keep 上的室内骑行记录拉下来、转成 FIT 文件、传到 Strava。兄弟项目 https://github.com/Liki4/Peek

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages