Advertisement
Guest User

gc-deletebydevice

a guest
Feb 27th, 2021
2,248
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.03 KB | None | 0 0
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3.  
  4. # Revision 2021-02-27
  5.  
  6. """
  7. Original credits:
  8.  
  9. File: gcexport.py
  10. Original author: Kyle Krafka (https://github.com/kjkjava/)
  11. Date: April 28, 2015
  12. Fork author: Michael P (https://github.com/moderation/)
  13. Date: February 15, 2018
  14.  
  15. Description:    Use this script to export your fitness data from Garmin Connect.
  16.                See README.md for more information.
  17.  
  18. Activity & event types:
  19.    https://connect.garmin.com/modern/main/js/properties/event_types/event_types.properties
  20.    https://connect.garmin.com/modern/main/js/properties/activity_types/activity_types.properties
  21. """
  22.  
  23. def show_exception_and_exit(exc_type, exc_value, tb):
  24.     import traceback
  25.     traceback.print_exception(exc_type, exc_value, tb)
  26.     input("Press ENTER to exit.")
  27.     sys.exit(-1)
  28.  
  29. import sys
  30. sys.excepthook = show_exception_and_exit
  31.  
  32. # workaround for SSL certificate error
  33.  
  34. import ssl
  35. ssl._create_default_https_context = ssl._create_unverified_context
  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. SCRIPT_VERSION = '0.0.1'
  57. #CURRENT_DATE = datetime.now().strftime('%Y-%m-%d')
  58. #ACTIVITIES_DIRECTORY = './' + CURRENT_DATE + '_garmin_connect_export'
  59.  
  60. PARSER = argparse.ArgumentParser()
  61.  
  62. # TODO: Implement verbose and/or quiet options.
  63. # PARSER.add_argument('-v', '--verbose', help="increase output verbosity", action="store_true")
  64. PARSER.add_argument('--version', help="print version and exit", action="store_true")
  65. PARSER.add_argument('--username', help="your Garmin Connect username (otherwise, you will be \
  66.    prompted)", nargs='?')
  67. PARSER.add_argument('--password', help="your Garmin Connect password (otherwise, you will be \
  68.    prompted)", nargs='?')
  69.  
  70. PARSER.add_argument('--deviceid', help="the device ID for activities to delete (otherwise, you will be \
  71.    prompted)", nargs='?')
  72.  
  73.  
  74. PARSER.add_argument('--startdate', help="the date of the first activity to delete (e.g. 2018-09-30) (otherwise, you will be \
  75.    prompted)", nargs='?')
  76. PARSER.add_argument('--enddate', help="the date of the last activity to delete(e.g. 2018-10-30) (otherwise, you will be \
  77.    prompted)", nargs='?')
  78.  
  79.  
  80.    
  81. ARGS = PARSER.parse_args()
  82.  
  83. if ARGS.version:
  84.     print(argv[0] + ", version " + SCRIPT_VERSION)
  85.     exit(0)
  86.  
  87. COOKIE_JAR = http.cookiejar.CookieJar()
  88. OPENER = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(COOKIE_JAR), urllib.request.HTTPSHandler(debuglevel=0))
  89. # print(COOKIE_JAR)
  90.  
  91.  
  92. # url is a string, post is the raw post data, headers is a dictionary of headers.
  93. def http_req(url, post=None, headers=None):
  94.     """Helper function that makes the HTTP requests."""
  95.     request = urllib.request.Request(url)
  96.     # Tell Garmin we're some supported browser.
  97.     request.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, \
  98.        like Gecko) Chrome/54.0.2816.0 Safari/537.36')
  99.     if headers:
  100.         for header_key, header_value in headers.items():
  101.             request.add_header(header_key, header_value)
  102.     if post:
  103.         #post = urllib.parse.urlencode(post)
  104.         post = post.encode('utf-8')  # Convert dictionary to POST parameter string.
  105.     # print("request.headers: " + str(request.headers) + " COOKIE_JAR: " + str(COOKIE_JAR))
  106.     # print("post: " + str(post) + "request: " + str(request))
  107.     response = OPENER.open((request), data=post)
  108.  
  109.     if response.getcode() == 204:
  110.         # For activities without GPS coordinates, there is no GPX download (204 = no content).
  111.         # Write an empty file to prevent redownloading it.
  112.         #print('Writing empty file since there was no GPX activity data...')
  113.         return ''
  114.     elif response.getcode() != 200:
  115.         raise Exception('Bad return code (' + str(response.getcode()) + ') for: ' + url)
  116.     # print(response.getcode())
  117.  
  118.     return response.read()
  119.  
  120. print('Welcome to the Garmin Connect "Delete Activity by Device ID" Tool!')
  121. print('')
  122. USERNAME=''
  123. PASSWORD=''
  124. DEVICEID=''
  125. while not USERNAME:
  126.     USERNAME = ARGS.username if ARGS.username else input('Username: ')
  127.     if not USERNAME:
  128.         print("Please enter a username.")
  129.         print("")
  130. while not PASSWORD:
  131.     PASSWORD = ARGS.password if ARGS.password else getpass()
  132.     if not PASSWORD:
  133.         print("Please enter a password.")
  134.         print("")
  135.  
  136. while not DEVICEID:
  137.     DEVICEID = ARGS.deviceid if ARGS.deviceid else input('Device ID: ')
  138.     if not DEVICEID:
  139.         print("Please enter a device ID.")
  140.         print("")
  141.  
  142. DEVICEID=int(DEVICEID)
  143.  
  144. # Maximum # of activities you can search for at once (URL_GC_LIST)
  145. LIMIT_ACTIVITY_LIST = 9999
  146.  
  147. print('Select Activities')
  148. print('  Up to ' + str(LIMIT_ACTIVITY_LIST) + ' activities can be processed at one time.')
  149. print('  Leave the start date blank to start at the beginning.')
  150. print('  Leave the end date blank to end at the latest activity.')
  151. print("")
  152.  
  153. def promptDate(prompt, defaultValue, errorStr):
  154.     while True:
  155.         DATE = defaultValue if defaultValue else input(prompt)
  156.         DATE = DATE.strip()
  157.         if not DATE:
  158.             break;
  159.         try:
  160.             datetime.strptime(DATE, '%Y-%m-%d')
  161.         except ValueError:
  162.             #raise ValueError("Incorrect data format, should be YYYY-MM-DD")
  163.             print(errorStr)
  164.             print("")
  165.             continue
  166.         break
  167.     return DATE
  168.  
  169. STARTDATE = promptDate('  Start Date (e.g. "2018-09-30" or blank): ', ARGS.startdate, "  Invalid date.")
  170. ENDDATE = promptDate('  End Date (e.g. "2018-10-30" or blank): ', ARGS.enddate, "  Invalid date.")
  171. print('')
  172.  
  173. print('')
  174. print('WARNING: GARMIN CONNECT ACTIVITIES FOR DEVICE ' + str(DEVICEID) + " IN THE GIVEN DATE RANGE WILL BE DELETED FOREVER!")
  175. RESPONSE = input('Type "YES" and press ENTER if you are absolutely sure: ')
  176. if RESPONSE != 'YES':
  177.     sys.exit(0)
  178.  
  179.  
  180. # Maximum number of activities you can request at once.  Set and enforced by Garmin.
  181. LIMIT_MAXIMUM = 1000
  182.  
  183. WEBHOST = "https://connect.garmin.com"
  184. REDIRECT = "https://connect.garmin.com/modern/"
  185. BASE_URL = "https://connect.garmin.com/en-US/signin"
  186. SSO = "https://sso.garmin.com/sso"
  187. CSS = "https://static.garmincdn.com/com.garmin.connect/ui/css/gauth-custom-v1.2-min.css"
  188.  
  189. DATA = {
  190.     'service': REDIRECT,
  191.     'webhost': WEBHOST,
  192.     'source': BASE_URL,
  193.     'redirectAfterAccountLoginUrl': REDIRECT,
  194.     'redirectAfterAccountCreationUrl': REDIRECT,
  195.     'gauthHost': SSO,
  196.     'locale': 'en_US',
  197.     'id': 'gauth-widget',
  198.     'cssUrl': CSS,
  199.     'clientId': 'GarminConnect',
  200.     'rememberMeShown': 'true',
  201.     'rememberMeChecked': 'false',
  202.     'createAccountShown': 'true',
  203.     'openCreateAccount': 'false',
  204.     'usernameShown': 'false',
  205.     'displayNameShown': 'false',
  206.     'consumeServiceTicket': 'false',
  207.     'initialFocus': 'true',
  208.     'embedWidget': 'false',
  209.     'generateExtraServiceTicket': 'true',
  210.     'generateTwoExtraServiceTickets': 'false',
  211.     'generateNoServiceTicket': 'false',
  212.     'globalOptInShown': 'true',
  213.     'globalOptInChecked': 'false',
  214.     'mobile': 'false',
  215.     'connectLegalTerms': 'true',
  216.     'locationPromptShown': 'true',
  217.     'showPassword': 'true'    
  218. }
  219.  
  220. #print(urllib.parse.urlencode(DATA))
  221.  
  222. # URLs for various services.
  223. URL_GC_LOGIN = 'https://sso.garmin.com/sso/signin?' + urllib.parse.urlencode(DATA)
  224. URL_GC_POST_AUTH = 'https://connect.garmin.com/modern/activities?'
  225. URL_GC_SEARCH = 'https://connect.garmin.com/proxy/activity-search-service-1.2/json/activities?'
  226. URL_GC_LIST = \
  227.     'https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?'
  228. URL_GC_ACTIVITY = 'https://connect.garmin.com/modern/proxy/activity-service/activity/'
  229. URL_GC_ACTIVITY_DETAIL = \
  230.     'https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/'
  231. URL_GC_GPX_ACTIVITY = \
  232.     'https://connect.garmin.com/modern/proxy/download-service/export/gpx/activity/'
  233. URL_GC_TCX_ACTIVITY = \
  234.     'https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/'
  235. URL_GC_ORIGINAL_ACTIVITY = 'http://connect.garmin.com/proxy/download-service/files/activity/'
  236.  
  237. URL_GC_ACTIVITY_PAGE = 'https://connect.garmin.com/modern/activity/'
  238.  
  239. print("Logging in...")
  240.  
  241. # Initially, we need to get a valid session cookie, so we pull the login page.
  242. #print('Request login page')
  243. http_req(URL_GC_LOGIN)
  244. #print('Finish login page')
  245.  
  246. # Now we'll actually login.
  247. # Fields that are passed in a typical Garmin login.
  248. POST_DATA = {
  249.     'username': USERNAME,
  250.     'password': PASSWORD,
  251.     'embed': 'false',
  252.     'rememberme': 'on'
  253.     }
  254.    
  255. headers = {
  256.     'referer': URL_GC_LOGIN
  257. }    
  258.  
  259. #print('Post login data')
  260. LOGIN_RESPONSE = http_req(URL_GC_LOGIN + '#', urllib.parse.urlencode(POST_DATA), headers).decode()
  261. #print('Finish login post')
  262.  
  263. # extract the ticket from the login response
  264. PATTERN = re.compile(r".*\?ticket=([-\w]+)\";.*", re.MULTILINE|re.DOTALL)
  265. MATCH = PATTERN.match(LOGIN_RESPONSE)
  266. if not MATCH:
  267.     raise Exception('Did not get a ticket in the login response. Cannot log in. Did \
  268. you enter the correct username and password?')
  269. LOGIN_TICKET = MATCH.group(1)
  270. #print('login ticket=' + LOGIN_TICKET)
  271.  
  272. #print("Request authentication URL: " + URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET)
  273. #print("Request authentication")
  274. LOGIN_AUTH_REP = http_req(URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET).decode()
  275. #print(LOGIN_AUTH_REP)
  276. #print('Finished authentication')
  277.  
  278.  
  279. SEARCH_PARAMS = {'start': 0, 'limit': LIMIT_ACTIVITY_LIST}
  280.  
  281. if STARTDATE:
  282.     SEARCH_PARAMS['startDate'] = STARTDATE
  283. if ENDDATE:
  284.     SEARCH_PARAMS['endDate'] = ENDDATE
  285.  
  286. ACLISTURL = URL_GC_LIST + urllib.parse.urlencode(SEARCH_PARAMS)
  287. #print("Activity list URL: " + ACLISTURL)
  288.  
  289. print('')
  290. print('Searching for activities (this might take a while)...')
  291. print('')
  292. #print('Loading activity list')
  293. ACTIVITY_LIST = http_req(ACLISTURL, None,  { 'nk': 'NT' })
  294.  
  295. #print('Processing activity list')
  296. #print('')
  297.  
  298. JSON_LIST = json.loads(ACTIVITY_LIST)
  299.  
  300. if len(JSON_LIST) == 0:
  301.     print("No activities found for the given date range.")
  302. else:
  303.     print("Found " + str(len(JSON_LIST)) + " activities.")
  304.  
  305. for a in JSON_LIST:
  306.     print('Activity: ' + a['startTimeLocal'] + (' | ' + a['activityName'] if a['activityName'] else ''))
  307.  
  308.     print('  Activity device ID = ' + str(a['deviceId']))
  309.     if a['deviceId'] == DEVICEID:
  310.         print('  Deleting activity!')  
  311.         ACTIVITY_URL = URL_GC_ACTIVITY + str(a['activityId'])
  312.         ACTIVITY_PAGE_URL = URL_GC_ACTIVITY_PAGE + str(a['activityId'])
  313.         ACTIVITY_POST_HEADERS = {
  314.             'referer': ACTIVITY_PAGE_URL,
  315.             'authority': 'connect.garmin.com',
  316.             'origin': 'https://connect.garmin.com',
  317.             'Content-Type':'application/json',
  318.             'X-HTTP-Method-Override': 'DELETE',
  319.             'X-Requested-With': 'XMLHttpRequest',
  320.             'nk': 'NT'
  321.         }
  322.         resp = http_req(ACTIVITY_URL, {}, ACTIVITY_POST_HEADERS)
  323.     else:
  324.         print('  Device ID does not match. Not deleting activity.')
  325.  
  326. print('')
  327. print('Done!')
  328.  
  329. input('Press ENTER to quit');
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement