Skip to content

Commit

Permalink
Merge branch 'upstream'
Browse files Browse the repository at this point in the history
* upstream:
  add run.drink.cafe (yihong0618#628)
  feat: coros sync (yihong0618#623)
  Fix README typo (yihong0618#624)
  fix: ts type error (yihong0618#622)

# Conflicts:
#	.github/workflows/run_data_sync.yml
#	README-CN.md
#	README.md
  • Loading branch information
ben-29 committed Mar 24, 2024
2 parents 37508c4 + 117b9a6 commit b601f55
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: ['3.8', '3.9', '3.10']
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4

Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/run_data_sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
- run_page/strava_sync.py
- run_page/gen_svg.py
- run_page/garmin_sync.py
- run_page/coros_sync.py
- run_page/keep_sync.py
- run_page/gpx_sync.py
- run_page/tcx_sync.py
Expand All @@ -21,7 +22,7 @@ on:

env:
# please change to your own config.
RUN_TYPE: pass # support strava/nike/garmin/garmin_cn/garmin_sync_cn_global/keep/only_gpx/only_fit/nike_to_strava/strava_to_garmin/strava_to_garmin_cn/garmin_to_strava/garmin_to_strava_cn/codoon, Please change the 'pass' it to your own
RUN_TYPE: pass # support strava/nike/garmin/coros/garmin_cn/garmin_sync_cn_global/keep/only_gpx/only_fit/nike_to_strava/strava_to_garmin/strava_to_garmin_cn/garmin_to_strava/garmin_to_strava_cn/codoon, Please change the 'pass' it to your own
ATHLETE: ben_29
TITLE: Workouts
MIN_GRID_DISTANCE: 10 # change min distance here
Expand Down Expand Up @@ -97,6 +98,11 @@ jobs:
run: |
python run_page/keep_sync.py ${{ secrets.KEEP_MOBILE }} ${{ secrets.KEEP_PASSWORD }} --with-gpx
- name: Run sync Coros script
if: env.RUN_TYPE == 'coros'
run: |
python run_page/coros_sync.py ${{ secrets.COROS_ACCOUNT }} ${{ secrets.COROS_PASSWORD }}
- name: Run sync Strava script
if: env.RUN_TYPE == 'strava'
run: |
Expand Down
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ RUN npm config set registry https://registry.npm.taobao.org \
&&corepack enable \
&&pnpm install



FROM develop-py AS data
ARG app
ARG nike_refresh_token
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@
"url": "https://github.com/ben-29/workouts_page"
},
"devDependencies": {
"@types/geojson": "^7946.0.14",
"@types/mapbox__polyline": "^1.0.2",
"@types/node": "^20.3.3",
"@types/prop-types": "^15.7.11",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@typescript-eslint/parser": "^5.59.2",
Expand Down
20 changes: 13 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

166 changes: 166 additions & 0 deletions run_page/coros_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import argparse
import asyncio
import hashlib
import os
import time

import aiofiles
import httpx

from config import JSON_FILE, SQL_FILE, FIT_FOLDER
from utils import make_activities_file

COROS_URL_DICT = {
"LOGIN_URL": "https://teamcnapi.coros.com/account/login",
"DOWNLOAD_URL": "https://teamcnapi.coros.com/activity/detail/download",
"ACTIVITY_LIST": "https://teamcnapi.coros.com/activity/query?&modeList=100,102,103",
}

TIME_OUT = httpx.Timeout(240.0, connect=360.0)


class Coros:
def __init__(self, account, password):
self.account = account
self.password = password
self.headers = None
self.req = None

async def login(self):
url = COROS_URL_DICT.get("LOGIN_URL")
headers = {
"authority": "teamcnapi.coros.com",
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"content-type": "application/json;charset=UTF-8",
"dnt": "1",
"origin": "https://t.coros.com",
"referer": "https://t.coros.com/",
"sec-ch-ua": '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
}
data = {"account": self.account, "accountType": 2, "pwd": self.password}
async with httpx.AsyncClient(timeout=TIME_OUT) as client:
response = await client.post(url, json=data, headers=headers)
resp_json = response.json()
access_token = resp_json.get("data", {}).get("accessToken")
if not access_token:
raise Exception(
"============Login failed! please check your account and password==========="
)
self.headers = {
"accesstoken": access_token,
"cookie": f"CPL-coros-region=2; CPL-coros-token={access_token}",
}
self.req = httpx.AsyncClient(timeout=TIME_OUT, headers=self.headers)
await client.aclose()

async def init(self):
await self.login()

async def fetch_activity_ids(self):
page_number = 1
all_activities_ids = []

while True:
url = f"{COROS_URL_DICT.get('ACTIVITY_LIST')}&pageNumber={page_number}&size=20"
response = await self.req.get(url)
data = response.json()
activities = data.get("data", {}).get("dataList", None)
if not activities:
break
for activity in activities:
label_id = activity["labelId"]
if label_id is None:
continue
all_activities_ids.append(label_id)

page_number += 1

return all_activities_ids

async def download_activity(self, label_id):
download_folder = FIT_FOLDER
download_url = f"{COROS_URL_DICT.get('DOWNLOAD_URL')}?labelId={label_id}&sportType=100&fileType=4"
file_url = None
try:
response = await self.req.post(download_url)
resp_json = response.json()
file_url = resp_json.get("data", {}).get("fileUrl")
if not file_url:
print(f"No file URL found for label_id {label_id}")
return None, None

fname = os.path.basename(file_url)
file_path = os.path.join(download_folder, fname)

async with self.req.stream("GET", file_url) as response:
response.raise_for_status()
async with aiofiles.open(file_path, "wb") as f:
async for chunk in response.aiter_bytes():
await f.write(chunk)
except httpx.HTTPStatusError as exc:
print(
f"Failed to download {file_url} with status code {response.status_code}: {exc}"
)
return None, None
except Exception as exc:
print(f"Error occurred while downloading {file_url}: {exc}")
return None, None

return label_id, fname


def get_downloaded_ids(folder):
return [i.split(".")[0] for i in os.listdir(folder) if not i.startswith(".")]


async def download_and_generate(account, password):
folder = FIT_FOLDER
downloaded_ids = get_downloaded_ids(folder)
coros = Coros(account, password)
await coros.init()

activity_ids = await coros.fetch_activity_ids()
print("activity_ids: ", len(activity_ids))
print("downloaded_ids: ", len(downloaded_ids))
to_generate_coros_ids = list(set(activity_ids) - set(downloaded_ids))
print("to_generate_activity_ids: ", len(to_generate_coros_ids))

start_time = time.time()
await gather_with_concurrency(
10,
[coros.download_activity(label_d) for label_d in to_generate_coros_ids],
)
print(f"Download finished. Elapsed {time.time()-start_time} seconds")
await coros.req.aclose()
make_activities_file(SQL_FILE, FIT_FOLDER, JSON_FILE, "fit")


async def gather_with_concurrency(n, tasks):
semaphore = asyncio.Semaphore(n)

async def sem_task(task):
async with semaphore:
return await task

return await asyncio.gather(*(sem_task(task) for task in tasks))


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("account", nargs="?", help="input coros account")

parser.add_argument("password", nargs="?", help="input coros password")
options = parser.parse_args()

account = options.account
password = options.password
encrypted_pwd = hashlib.md5(password.encode()).hexdigest()

asyncio.run(download_and_generate(account, encrypted_pwd))
1 change: 0 additions & 1 deletion src/components/RunMap/LightsControl.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import styles from './style.module.scss';

interface ILightsProps {
Expand Down
2 changes: 1 addition & 1 deletion src/components/RunMap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const RunMap = ({
const keepWhenLightsOff = ['runs2']
function switchLayerVisibility(map: MapInstance, lights: boolean) {
const styleJson = map.getStyle();
styleJson.layers.forEach(it => {
styleJson.layers.forEach((it: { id: string; }) => {
if (!keepWhenLightsOff.includes(it.id)) {
if (lights)
map.setLayoutProperty(it.id, 'visibility', 'visible');
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const useHover = (): HoverHook => {

const eventHandlers = {
onMouseOver() {
setTimer(setTimeout(() => setHovered(true), 700));
setTimer(setTimeout(() => setHovered(true), 500)); // 500ms delay
},
onMouseOut() {
clearTimeout(timer);
Expand Down
14 changes: 8 additions & 6 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export interface Activity {
type: string;
start_date: string;
start_date_local: string;
location_country: string;
summary_polyline: string;
average_heartrate?: number;
location_country?: string|null;
summary_polyline?: string|null;
average_heartrate?: number|null;
average_speed: number;
streak: number;
}
Expand Down Expand Up @@ -159,6 +159,9 @@ const intComma = (x = '') => {

const pathForRun = (run: Activity): Coordinate[] => {
try {
if (!run.summary_polyline) {
return [];
};
const c = mapboxPolyline.decode(run.summary_polyline);
// reverse lat long for mapbox
c.forEach((arr) => {
Expand Down Expand Up @@ -408,10 +411,9 @@ const filterAndSortRuns = (
};

const sortDateFunc = (a: Activity, b: Activity) => {
// @ts-ignore
return (
new Date(b.start_date_local.replace(' ', 'T')) -
new Date(a.start_date_local.replace(' ', 'T'))
new Date(b.start_date_local.replace(' ', 'T')).getTime() -
new Date(a.start_date_local.replace(' ', 'T')).getTime()
);
};
const sortDateFuncReverse = (a: Activity, b: Activity) => sortDateFunc(b, a);
Expand Down

0 comments on commit b601f55

Please sign in to comment.