Advertisement
Guest User

gc-activity-type

a guest
Mar 31st, 2021
2,177
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.33 KB | None | 0 0
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3.  
  4. SCRIPT_VERSION = '0.0.5 (2021-03-31)'
  5. """
  6. Original credits:
  7.  
  8. File: gcexport.py
  9. Original author: Kyle Krafka (https://github.com/kjkjava/)
  10. Date: April 28, 2015
  11. Fork author: Michael P (https://github.com/moderation/)
  12. Date: February 15, 2018
  13.  
  14. Description:    Use this script to export your fitness data from Garmin Connect.
  15.                 See README.md for more information.
  16.  
  17. Activity & event types:
  18.     https://connect.garmin.com/modern/main/js/properties/event_types/event_types.properties
  19.     https://connect.garmin.com/modern/main/js/properties/activity_types/activity_types.properties
  20. """
  21.  
  22. def show_exception_and_exit(exc_type, exc_value, tb):
  23.     import traceback
  24.     traceback.print_exception(exc_type, exc_value, tb)
  25.     input("Press ENTER to exit.")
  26.     sys.exit(-1)
  27.  
  28. import sys
  29. sys.excepthook = show_exception_and_exit
  30.  
  31. # workaround for SSL certificate error
  32.  
  33. import ssl
  34. ssl._create_default_https_context = ssl._create_unverified_context
  35.  
  36.  
  37. # ##############################################
  38.  
  39. from datetime import datetime, timedelta
  40. from getpass import getpass
  41. from os import mkdir, remove, stat
  42. from os.path import isdir, isfile
  43. from subprocess import call
  44. from sys import argv
  45. from xml.dom.minidom import parseString
  46.  
  47. import argparse
  48. import http.cookiejar
  49. import json
  50. import re
  51. import urllib.error
  52. import urllib.parse
  53. import urllib.request
  54. import zipfile
  55.  
  56. #CURRENT_DATE = datetime.now().strftime('%Y-%m-%d')
  57. #ACTIVITIES_DIRECTORY = './' + CURRENT_DATE + '_garmin_connect_export'
  58.  
  59. PARSER = argparse.ArgumentParser()
  60.  
  61. # TODO: Implement verbose and/or quiet options.
  62. # PARSER.add_argument('-v', '--verbose', help="increase output verbosity", action="store_true")
  63. PARSER.add_argument('--version', help="print version and exit", action="store_true")
  64. PARSER.add_argument('--username', help="your Garmin Connect username (otherwise, you will be \
  65.     prompted)", nargs='?')
  66. PARSER.add_argument('--password', help="your Garmin Connect password (otherwise, you will be \
  67.     prompted)", nargs='?')
  68.  
  69. PARSER.add_argument('--startdate', help="the date of the first activity to set to private (e.g. 2018-09-30) (otherwise, you will be \
  70.     prompted)", nargs='?')
  71. PARSER.add_argument('--enddate', help="the date of the last activity to set to private (e.g. 2018-10-30) (otherwise, you will be \
  72.     prompted)", nargs='?')
  73.  
  74. PARSER.add_argument('--privacy', help="public, private, subscribers, groups", nargs='?')
  75.  
  76. PARSER.add_argument('--activity-type', '--activity_type', help="New activity type (default: 'cycling')")
  77. PARSER.add_argument('--activity-type-id', '--activity_type_id', help="New activity type ID (default: 2)")
  78. PARSER.add_argument('--activity-parent-type-id', '--activity_parent_type_id', help="New activity parent type ID (default: 17)")
  79.  
  80. PARSER.add_argument('--match-activity-type', '--match_activity_type', help="Existing activity type to match (e.g. 'running' or 'uncategorized') (default: all activity types are matched)")
  81. PARSER.add_argument('--match-activity-type-id', '--match_activity_type_id', help="Existing activity type ID to match (e.g. '1') (default: all activity type IDs are matched)")
  82. PARSER.add_argument('--match-activity-parent-type-id', '--match_activity_parent_type_id', help="Existing activity type ID to match (e.g. '17') (default: all activity parent type IDs are matched)")
  83.  
  84. PARSER.add_argument('--dry-run', '--dry_run', '--preview', help="preview changes (activities will not be modified)", action="store_true")
  85.  
  86. ARGS = PARSER.parse_args()
  87.  
  88. if ARGS.version:
  89.     print(argv[0] + ", version " + SCRIPT_VERSION)
  90.     exit(0)
  91.  
  92. if ARGS.dry_run:
  93.     print('--dry-run specified. Activities will not be modified\n')
  94.  
  95. COOKIE_JAR = http.cookiejar.CookieJar()
  96. OPENER = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(COOKIE_JAR), urllib.request.HTTPSHandler(debuglevel=0))
  97. # print(COOKIE_JAR)
  98.  
  99.  
  100. # url is a string, post is the raw post data, headers is a dictionary of headers.
  101. def http_req(url, post=None, headers=None):
  102.     """Helper function that makes the HTTP requests."""
  103.     request = urllib.request.Request(url)
  104.     # Tell Garmin we're some supported browser.
  105.     request.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, \
  106.         like Gecko) Chrome/54.0.2816.0 Safari/537.36')
  107.     if headers:
  108.         for header_key, header_value in headers.items():
  109.             request.add_header(header_key, header_value)
  110.     if post:
  111.         #post = urllib.parse.urlencode(post)
  112.         post = post.encode('utf-8')  # Convert dictionary to POST parameter string.
  113.     # print("request.headers: " + str(request.headers) + " COOKIE_JAR: " + str(COOKIE_JAR))
  114.     # print("post: " + str(post) + "request: " + str(request))
  115.     response = OPENER.open((request), data=post)
  116.  
  117.     if response.getcode() == 204:
  118.         # For activities without GPS coordinates, there is no GPX download (204 = no content).
  119.         # Write an empty file to prevent redownloading it.
  120.         #print('Writing empty file since there was no GPX activity data...')
  121.         return ''
  122.     elif response.getcode() != 200:
  123.         raise Exception('Bad return code (' + str(response.getcode()) + ') for: ' + url)
  124.     # print(response.getcode())
  125.  
  126.     return response.read()
  127.  
  128. print('Welcome to the Garmin Connect Activity Type Tool!')
  129. print('')
  130.  
  131. def promptGeneric(prompt, defaultValue, existingValue):
  132.     if existingValue == None:
  133.         INPUT = input('Enter ' + prompt + ': (default: ' + defaultValue + '): ')
  134.         INPUT = INPUT.strip()
  135.         if INPUT == '':
  136.             INPUT = defaultValue
  137.     else:
  138.         INPUT = existingValue
  139.  
  140.     print('Selected ' + prompt + ': ' + str(INPUT))
  141.  
  142.     return INPUT
  143.  
  144. ACTIVITY_TYPE = ARGS.activity_type
  145. ACTIVITY_TYPE_ID = ARGS.activity_type_id
  146. ACTIVITY_PARENT_TYPE_ID = ARGS.activity_parent_type_id
  147.  
  148. DEFAULT_ACTIVITY_TYPE = 'cycling'
  149. DEFAULT_ACTIVITY_TYPE_ID = '2'
  150. DEFAULT_ACTIVITY_PARENT_TYPE_ID = '17'
  151.  
  152. ACTIVITY_TYPE = promptGeneric("new activity type", DEFAULT_ACTIVITY_TYPE, ACTIVITY_TYPE)
  153. ACTIVITY_TYPE_ID = int(promptGeneric("new activity type ID", DEFAULT_ACTIVITY_TYPE_ID, ACTIVITY_TYPE_ID))
  154. ACTIVITY_PARENT_TYPE_ID = int(promptGeneric("new activity parent type ID", DEFAULT_ACTIVITY_PARENT_TYPE_ID, ACTIVITY_PARENT_TYPE_ID))
  155. print('')
  156. if (not ARGS.match_activity_type == None):
  157.     print(f'Matching activity type: {ARGS.match_activity_type}')
  158. if (not ARGS.match_activity_type_id == None):
  159.     ARGS.match_activity_type_id = int(ARGS.match_activity_type_id)
  160.     print(f'Matching activity type ID: {ARGS.match_activity_type_id}')
  161. if (not ARGS.match_activity_parent_type_id == None):
  162.     ARGS.match_activity_parent_type_id = int(ARGS.match_activity_parent_type_id)
  163.     print(f'Matching activity parent type ID: {ARGS.match_activity_parent_type_id}')
  164. print('')
  165.  
  166. USERNAME=''
  167. PASSWORD=''
  168. while not USERNAME:
  169.     USERNAME = ARGS.username if ARGS.username else input('Username: ')
  170.     if not USERNAME:
  171.         print("Please enter a username.")
  172.         print("")
  173. while not PASSWORD:
  174.     PASSWORD = ARGS.password if ARGS.password else getpass()
  175.     if not PASSWORD:
  176.         print("Please enter a password.")
  177.         print("")
  178.  
  179. # Maximum # of activities you can search for at once (URL_GC_LIST)
  180. LIMIT_ACTIVITY_LIST = 9999
  181.  
  182. print('Select Activities')
  183. print('  Up to ' + str(LIMIT_ACTIVITY_LIST) + ' activities can be processed at one time.')
  184. print('  Leave the start date blank to start at the beginning.')
  185. print('  Leave the end date blank to end at the latest activity.')
  186. print("")
  187.  
  188. def promptDate(prompt, defaultValue, errorStr):
  189.     while True:
  190.         DATE = defaultValue if defaultValue else input(prompt)
  191.         DATE = DATE.strip()
  192.         if not DATE:
  193.             break;
  194.         try:
  195.             datetime.strptime(DATE, '%Y-%m-%d')
  196.         except ValueError:
  197.             #raise ValueError("Incorrect data format, should be YYYY-MM-DD")
  198.             print(errorStr)
  199.             print("")
  200.             continue
  201.         break
  202.     return DATE
  203.  
  204. STARTDATE = promptDate('  Start Date (e.g. "2018-09-30" or blank): ', ARGS.startdate, "  Invalid date.")
  205. ENDDATE = promptDate('  End Date (e.g. "2018-10-30" or blank): ', ARGS.enddate, "  Invalid date.")
  206. print('')
  207.  
  208. # Maximum number of activities you can request at once.  Set and enforced by Garmin.
  209. LIMIT_MAXIMUM = 1000
  210.  
  211. WEBHOST = "https://connect.garmin.com"
  212. REDIRECT = "https://connect.garmin.com/modern/"
  213. BASE_URL = "https://connect.garmin.com/en-US/signin"
  214. SSO = "https://sso.garmin.com/sso"
  215. CSS = "https://static.garmincdn.com/com.garmin.connect/ui/css/gauth-custom-v1.2-min.css"
  216.  
  217. DATA = {
  218.     'service': REDIRECT,
  219.     'webhost': WEBHOST,
  220.     'source': BASE_URL,
  221.     'redirectAfterAccountLoginUrl': REDIRECT,
  222.     'redirectAfterAccountCreationUrl': REDIRECT,
  223.     'gauthHost': SSO,
  224.     'locale': 'en_US',
  225.     'id': 'gauth-widget',
  226.     'cssUrl': CSS,
  227.     'clientId': 'GarminConnect',
  228.     'rememberMeShown': 'true',
  229.     'rememberMeChecked': 'false',
  230.     'createAccountShown': 'true',
  231.     'openCreateAccount': 'false',
  232.     'usernameShown': 'false',
  233.     'displayNameShown': 'false',
  234.     'consumeServiceTicket': 'false',
  235.     'initialFocus': 'true',
  236.     'embedWidget': 'false',
  237.     'generateExtraServiceTicket': 'true',
  238.     'generateTwoExtraServiceTickets': 'false',
  239.     'generateNoServiceTicket': 'false',
  240.     'globalOptInShown': 'true',
  241.     'globalOptInChecked': 'false',
  242.     'mobile': 'false',
  243.     'connectLegalTerms': 'true',
  244.     'locationPromptShown': 'true',
  245.     'showPassword': 'true'    
  246. }
  247.  
  248. #print(urllib.parse.urlencode(DATA))
  249.  
  250. # URLs for various services.
  251. URL_GC_LOGIN = 'https://sso.garmin.com/sso/signin?' + urllib.parse.urlencode(DATA)
  252. URL_GC_POST_AUTH = 'https://connect.garmin.com/modern/activities?'
  253. URL_GC_SEARCH = 'https://connect.garmin.com/proxy/activity-search-service-1.2/json/activities?'
  254. URL_GC_LIST = \
  255.     'https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?'
  256. URL_GC_ACTIVITY = 'https://connect.garmin.com/modern/proxy/activity-service/activity/'
  257. URL_GC_ACTIVITY_DETAIL = \
  258.     'https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/'
  259. URL_GC_GPX_ACTIVITY = \
  260.     'https://connect.garmin.com/modern/proxy/download-service/export/gpx/activity/'
  261. URL_GC_TCX_ACTIVITY = \
  262.     'https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/'
  263. URL_GC_ORIGINAL_ACTIVITY = 'http://connect.garmin.com/proxy/download-service/files/activity/'
  264.  
  265. URL_GC_ACTIVITY_PAGE = 'https://connect.garmin.com/modern/activity/'
  266.  
  267. print("Logging in...")
  268.  
  269. # Initially, we need to get a valid session cookie, so we pull the login page.
  270. #print('Request login page')
  271. connect_response = http_req(URL_GC_LOGIN)
  272. # write_to_file('connect_response.html', connect_response, 'w')
  273. #for cookie in COOKIE_JAR:
  274. #   logging.debug("Cookie %s : %s", cookie.name, cookie.value)
  275. #print('Finish login page')
  276.  
  277. # Now we'll actually login.
  278. # Fields that are passed in a typical Garmin login.
  279. POST_DATA = {
  280.     'username': USERNAME,
  281.     'password': PASSWORD,
  282.     'embed': 'false',
  283.     'rememberme': 'on'
  284.     }
  285.    
  286. headers = {
  287.     'referer': URL_GC_LOGIN
  288. }    
  289.  
  290. #print('Post login data')
  291. LOGIN_RESPONSE = http_req(URL_GC_LOGIN + '#', urllib.parse.urlencode(POST_DATA), headers).decode()
  292. #print('Finish login post')
  293.  
  294. # extract the ticket from the login response
  295. PATTERN = re.compile(r".*\?ticket=([-\w]+)\";.*", re.MULTILINE|re.DOTALL)
  296. MATCH = PATTERN.match(LOGIN_RESPONSE)
  297. if not MATCH:
  298.     raise Exception('Did not get a ticket in the login response. Cannot log in. Did \
  299. you enter the correct username and password?')
  300. LOGIN_TICKET = MATCH.group(1)
  301. #print('login ticket=' + LOGIN_TICKET)
  302.  
  303. #print("Request authentication URL: " + URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET)
  304. #print("Request authentication")
  305. LOGIN_AUTH_REP = http_req(URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET).decode()
  306. #print(LOGIN_AUTH_REP)
  307. #print('Finished authentication')
  308.  
  309.  
  310. SEARCH_PARAMS = {'start': 0, 'limit': LIMIT_ACTIVITY_LIST}
  311.  
  312. if STARTDATE:
  313.     SEARCH_PARAMS['startDate'] = STARTDATE
  314. if ENDDATE:
  315.     SEARCH_PARAMS['endDate'] = ENDDATE
  316.  
  317. ACLISTURL = URL_GC_LIST + urllib.parse.urlencode(SEARCH_PARAMS)
  318. #print("Activity list URL: " + ACLISTURL)
  319.  
  320. print('')
  321. print('Searching for activities (this might take a while)...')
  322. print('')
  323. #print('Loading activity list')
  324. ACTIVITY_LIST = http_req(ACLISTURL, None, {
  325.     'NK': 'NT'
  326. })
  327.  
  328. #print('Processing activity list')
  329. #print('')
  330.  
  331. JSON_LIST = json.loads(ACTIVITY_LIST)
  332.  
  333. if len(JSON_LIST) == 0:
  334.     print("No activities found for the given date range.")
  335. else:
  336.     print("Found " + str(len(JSON_LIST)) + " activities.")
  337.  
  338. for a in JSON_LIST:
  339.     print(f'Activity: {a["startTimeLocal"]} | {a["activityName"] if a["activityName"] else ""} | Type: {a["activityType"]["typeKey"]} (typeId: {a["activityType"]["typeId"]}, parentTypeId: {a["activityType"]["parentTypeId"]})')
  340.  
  341.     if (not ARGS.match_activity_type == None):
  342.         if ARGS.match_activity_type == a["activityType"]["typeKey"]:
  343.             print(f'  Matched activity type: {ARGS.match_activity_type}')
  344.         else:
  345.             print(f'  Did not match activity type: {ARGS.match_activity_type}')
  346.             print('  Skipping activity')
  347.             continue
  348.     if (not ARGS.match_activity_type_id == None):
  349.         if ARGS.match_activity_type_id == a["activityType"]["typeId"]:
  350.             print(f'  Matched activity type ID: {str(ARGS.match_activity_type_id)}')
  351.         else:
  352.             print(f'  Did not match activity type ID: {str(ARGS.match_activity_type_id)}')
  353.             print('  Skipping activity')
  354.             continue
  355.     if (not ARGS.match_activity_parent_type_id == None):
  356.         if ARGS.match_activity_parent_type_id == a["activityType"]["parentTypeId"]:
  357.             print(f'  Matched activity parent type ID: {str(ARGS.match_activity_parent_type_id)}')
  358.         else:
  359.             print(f'  Did not match activity parent type ID: {str(ARGS.match_activity_parent_type_id)}')
  360.             print('  Skipping activity')
  361.             continue
  362.  
  363.     newActivityType = f'{ACTIVITY_TYPE} (typeId: {str(ACTIVITY_TYPE_ID)}, parentTypeId: {str(ACTIVITY_PARENT_TYPE_ID)})'
  364.     if (ARGS.dry_run):
  365.         print('  (Dry run) would have set activity type to ' + newActivityType)
  366.         continue
  367.     else:
  368.         print('  Setting activity type to ' + newActivityType)
  369.  
  370.     ACTIVITY_URL = URL_GC_ACTIVITY + str(a['activityId'])
  371.     ACTIVITY_PAGE_URL = URL_GC_ACTIVITY_PAGE + str(a['activityId'])
  372.     ACTIVITY_POST_DATA = {'activityTypeDTO': {'typeId': ACTIVITY_TYPE_ID, 'typeKey': ACTIVITY_TYPE, 'parentTypeId': ACTIVITY_PARENT_TYPE_ID}, 'activityId': a['activityId'] }
  373.     ACTIVITY_POST_HEADERS = {
  374.         'referer': ACTIVITY_PAGE_URL,
  375.         'authority': 'connect.garmin.com',
  376.         'origin': 'https://connect.garmin.com',
  377.         'Content-Type':'application/json',
  378.         'X-HTTP-Method-Override': 'PUT',
  379.         'X-Requested-With': 'XMLHttpRequest',
  380.         'nk': 'NT'
  381.     }
  382.     http_req(ACTIVITY_URL, json.dumps(ACTIVITY_POST_DATA), ACTIVITY_POST_HEADERS)
  383.  
  384. print('')
  385. print('Done!')
  386.  
  387. input('Press ENTER to quit');
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement