Advertisement
Guest User

Bungie.Net API Scraper for Last Played Time

a guest
Jan 4th, 2018
4,227
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.50 KB | None | 0 0
  1. #!/usr/bin/python
  2.  
  3. # IMPORTS
  4. import time
  5. import datetime
  6. from time import sleep
  7. import Queue
  8. import traceback
  9. import sys
  10. import os
  11. import json
  12. from threading import Thread, RLock, Lock, current_thread
  13. from optparse import OptionParser
  14. import requests
  15. from requests.exceptions import *
  16. import MySQLdb
  17. import random
  18. import urllib
  19. import requests.packages.urllib3
  20. requests.packages.urllib3.disable_warnings()
  21.  
  22. parser = OptionParser()
  23. parser.add_option('-s', '--start',   dest='start',   default=1,              help='Start index for work queue.', type='int')
  24. parser.add_option('-e', '--end',     dest='end',     default=20000000,       help='End index for work queue.',   type='int')
  25. parser.add_option('-t', '--threads', dest='threads', default=2,              help='Number of worker threads.',   type='int')
  26. parser.add_option('-S', '--syncfile',dest='syncfile',default="progress.json",help='File used to syncronize starting numbers.')
  27. parser.add_option('-v', '--verbose', dest='verbose', action="store_true",    default=False, help='Display verbose output.')
  28. parser.add_option('-H', '--host',    dest='host',    default=None,           help='Database server hostname or IP address')
  29. parser.add_option('-P', '--port',    dest='port',    default=None,           help='Database server port')
  30. parser.add_option('-N', '--name',    dest='name',    default=None,           help='Database name')
  31. parser.add_option('-u', '--username',dest='username',default=None,           help='Database username')
  32. parser.add_option('-p', '--password',dest='password',default=None,           help='Database password')
  33. parser.add_option('-a', '--apikey',  dest='apikey',  default=None,           help='Bungie.Net API Key')
  34. parser.add_option('-F', '--forcerestart', dest='forcerestart', action="store_true", default=False, help='Ignore sync file and force start and end numbers.')
  35. parser.add_option('-d', '--disableproxy', dest='disableproxy', action="store_true", default=False, help='Disable proxies.')
  36.  
  37. (options, args) = parser.parse_args()
  38.  
  39. if options.host is None or options.port is None or options.username is None or options.password is None or options.apikey is None:
  40.     parser.print_help()
  41.     sys.exit(1)
  42.  
  43. if options.end > options.start:
  44.     parser.print_help()
  45.     sys.exit(1)
  46.    
  47.  
  48. class WorkQueue:
  49.     def __init__(self):
  50.         self.q = Queue.Queue()
  51.     def add(self, item):
  52.         self.q.put(item)
  53.     def queueMax(self):
  54.         return self.qMax
  55.     def get(self):
  56.         return self.q.get()
  57.     def empty(self):
  58.         if self.q.empty(): return True
  59.         else: return False
  60.     def full(self):
  61.         if self.q.full(): return True
  62.         else: return False
  63.     def size(self):
  64.         return self.q.qsize()
  65.     def clear(self):
  66.         with self.q.mutex:
  67.             self.q.queue.clear()
  68.  
  69. EntryQueue = WorkQueue()
  70.  
  71. class ProxyDistributor:
  72.     def __init__(self, disableproxy):
  73.         self.lock = RLock()
  74.         self.badlist = []
  75.         if os.path.file_exists("proxies.json"):
  76.             with open("proxies.json", "r") as FILE:
  77.                 self.proxies = json.loads(FILE.readlines())
  78.         else:
  79.             if not disableproxy:
  80.                 self.sayLine("Missing Proxies list.")
  81.                 sys.exit()
  82.         if os.path.isfile('badproxies.txt'):
  83.             with open('badproxies.txt', 'rb') as FILE:
  84.                 LINES = FILE.readlines()
  85.                 for LINE in LINES:
  86.                     proxy = self.snl(LINE)
  87.                     self.sayLine("Adding Proxy [%s] to Bad Proxies List...", (proxy))
  88.                     self.badlist.append(proxy)
  89.         if len(self.badlist) == len(self.proxies):
  90.             self.out("No good proxies remain. Exiting...")
  91.             sys.exit()
  92.     def flagbad(self, proxy):
  93.         with self.lock:
  94.             self.badlist.append(proxy['https'])
  95.             with open('badproxies.txt', 'a') as FILE:
  96.                 FILE.write("%s\n" % proxy['https'])
  97.     def isgood(self, proxy):
  98.         with self.lock:
  99.             if proxy['https'] in self.badlist:
  100.                 return False
  101.             else:
  102.                 return True
  103.     def getProxy(self):
  104.         with self.lock:
  105.             GoodProxy = False
  106.             while not GoodProxy:
  107.                 proxy = random.choice(self.proxies)
  108.                 if self.isgood(proxy): GoodProxy = True
  109.             return proxy
  110.     def snl(self, s):
  111.         # QUICK FUNCTION TO STRIP NEW LINE AND RETURN CHARACTERS
  112.         s = s.rstrip('\n')
  113.         s = s.rstrip('\r')
  114.         return s
  115.     def say(self, format, args=()):
  116.         with self.lock:
  117.             sys.stdout.write('\r                                                                                                                                                                                           \r' + format % args)
  118.             sys.stdout.flush()
  119.     def sayLine(self, format, args=()):
  120.         format = '%s\n' % format
  121.         self.say(format, args)         
  122.     def out(self, string):
  123.         self.say(string)
  124.            
  125. MyProxy = ProxyDistributor(options.disableproxy)
  126.            
  127. class API:
  128.     def __init__(self, threads, start, end, verbose, disableproxy, syncfile, forcerestart, host, port, name, username, password, apikey):
  129.         try:
  130.             self.lock = RLock()
  131.             self.headers = {"X-API-Key": apikey}
  132.             self.endpoint = "https://www.bungie.net/Platform"
  133.             self.DBUSER = username
  134.             self.DBPASS = password
  135.             self.DBHOST = host
  136.             self.DBPORT = port
  137.             self.DBNAME = name
  138.             self.start = start
  139.             self.end = end
  140.             self.verbose = verbose
  141.             self.disableproxy = disableproxy
  142.             self.badproxies = []
  143.             self.syncfile = syncfile
  144.             self.forcerestart = forcerestart
  145.            
  146.            
  147.             self.out("Building Queue...")
  148.             if not self.forcerestart:
  149.                 if self.syncfile is not None and os.path.isfile(self.syncfile):
  150.                         sync = json.load(open(self.syncfile))
  151.                         for i in range(sync['start'], sync['end']):
  152.                             EntryQueue.add(i)
  153.                 else:
  154.                     if self.syncfile:
  155.                         with open(self.syncfile, "w") as FILE:
  156.                             sync = {"start": self.start, "end": self.end}
  157.                             FILE.write( json.dumps(sync) )
  158.                     for i in range(start, end):
  159.                         EntryQueue.add(i)
  160.             else:
  161.                 if self.syncfile:
  162.                     with open(self.syncfile, "w") as FILE:
  163.                         sync = {"start": self.start, "end": self.end}
  164.                         FILE.write( json.dumps(sync) )
  165.                 for i in range(start, end):
  166.                     EntryQueue.add(i)
  167.            
  168.             self.out("Starting workers...")
  169.             Workers = [Thread(target=self.WorkerThread) for i in range(threads)]
  170.             for Worker in Workers:
  171.                 Worker.daemon = True
  172.                 Worker.start()
  173.             self.out( "Done." )
  174.         except KeyboardInterrupt:
  175.             self.exit()
  176.        
  177.     def getProxy(self):
  178.         return MyProxy.getProxy()
  179.        
  180.     def WorkerThread(self):
  181.         self.out( "   - New thread started." )
  182.         while not EntryQueue.empty():
  183.             with self.lock:
  184.                 try:
  185.                     ID = EntryQueue.get()
  186.                     if self.syncfile is not None and os.path.isfile(self.syncfile):
  187.                         with open(self.syncfile, "w") as FILE:
  188.                             sync = {"start": ID, "end": self.end}
  189.                             FILE.write( json.dumps(sync) )
  190.    
  191.                     self.out("%s\tRequesting entry for membershipId %s..." % (current_thread(), ID) )
  192.                     endpoint = "/User/GetMembershipsById/%s/-1/" % ID
  193.                     url = "%s%s" % (self.endpoint, endpoint)
  194.                    
  195.                     # Initial request
  196.                     data = False
  197.                     count = 0
  198.                     while not data:
  199.                         count += 1
  200.                         if count > 1:
  201.                             self.out("%s\tReceived no data. Retrying..." % current_thread())
  202.                         if count == 5:
  203.                             break;
  204.                         data = self.request(url)
  205.                     #self.out(data)
  206.                     # Skip empty responses
  207.                     if data:
  208.                         if len(data) < 1:
  209.                             if self.verbose: self.out("%s\tReceived blank data." % current_thread())
  210.                             continue
  211.                     else:
  212.                         if self.verbose: self.out("%s\tReceived no data." % current_thread())
  213.                         continue
  214.                     if 'throttleSeconds' in data:
  215.                         if type(data['throttleSeconds']) is int or type(data['throttleSeconds']) is float:
  216.                             if data['throttleSeconds'] >= 1:
  217.                                 if self.verbose: self.out("%s\tSleeping for %s seconds..." % (current_thread(), response['throttleSeconds']))
  218.                                 sleep(response['throttleSeconds'])
  219.                                 if self.verbose: self.out("%s\tRequesting new data..." % current_thread())
  220.                                 data = self.request(url)
  221.                     #if self.verbose: print data
  222.                     # Success
  223.                     if 'ErrorCode' in data and data['ErrorCode'] == 1:
  224.                         response = data['Response']
  225.                         # Get User ID
  226.                         userId = response['bungieNetUser']['membershipId']
  227.                        
  228.                         querystring = {"components": 100}
  229.                         if 'destinyMemberships' in response:
  230.                             for entry in response['destinyMemberships']:
  231.                                 url = "%s/Destiny2/%s/Profile/%s/" % (self.endpoint, entry['membershipType'], entry['membershipId'])
  232.                                 membership = False
  233.                                 count = 0
  234.                                 while not membership:
  235.                                     count += 1
  236.                                     if count > 1:
  237.                                         self.out("%s\tReceived no membership data. Retrying..." % current_thread())
  238.                                     if count == 5: break
  239.                                     membership = self.request(url, querystring)
  240.                                
  241.                                 if membership:
  242.                                     if len(membership) < 1:
  243.                                         if self.verbose: self.out("%s\tReceived blank membership data." % current_thread() )
  244.                                         continue
  245.                                 else:
  246.                                     if self.verbose: self.out("%s\tReceived no membership data." % current_thread() )
  247.                                     continue
  248.                                
  249.                                 if 'throttleSeconds' in membership:
  250.                                     if type(membership['throttleSeconds']) is int or type(membership['throttleSeconds']) is float:
  251.                                         if membership['ThrottleSeconds'] >= 1:
  252.                                             if self.verbose: self.out("%s\tSleeping for %s seconds..." % (current_thread(), membership['throttleSeconds']))
  253.                                             sleep(membership['ThrottleSeconds']);
  254.                                             if self.verbose: self.out("%s\tRequesting new membership data..." % current_thread() )
  255.                                             membership = self.request(url, querystring)
  256.                                
  257.                                 if 'ErrorCode' in membership and membership['ErrorCode'] == 1:
  258.                                     timeLastPlayed = time.mktime(datetime.datetime.strptime(membership['Response']['profile']['data']['dateLastPlayed'], "%Y-%m-%dT%H:%M:%SZ").timetuple())
  259.                                     record = {"membershipId": userId,
  260.                                                 "destinyMembershipId": membership['Response']['profile']['data']['userInfo']['membershipId'],
  261.                                                 "membershipType": membership['Response']['profile']['data']['userInfo']['membershipType'],
  262.                                                 "dateLastPlayed": membership['Response']['profile']['data']['dateLastPlayed'],
  263.                                                 "timestampLastPlayed": int(timeLastPlayed)}
  264.                                    
  265.                                     update = {"dateLastPlayed": membership['Response']['profile']['data']['dateLastPlayed'],
  266.                                                 "timestampLastPlayed": int(timeLastPlayed)}
  267.                                     self.insert("entries", record, update)     
  268.                                 else:
  269.                                     self.out("%s\t[%s] -- %s: %s (%s)" % (current_thread(), ID, membership['ErrorCode'], membership['ErrorStatus'], membership['Message']))
  270.                     else:
  271.                         self.out("%s\t[%s] -- %s: %s (%s)" % (current_thread(), ID, data['ErrorCode'], data['ErrorStatus'], data['Message']))
  272.                 except KeyboardInterrupt:
  273.                     self.exit()
  274.  
  275.     def e(self, string):
  276.         ue = "%s" % string
  277.         return urllib.quote_plus(ue)
  278.                    
  279.     def insert(self, table, insert, update):
  280.         with self.lock:
  281.             try:
  282.                 db = MySQLdb.connect(host=self.DBHOST, port=self.DBPORT, user=self.DBUSER, passwd=self.DBPASS, db=self.DBNAME, autocommit=True)
  283.                 cursor = db.cursor()
  284.                 q = "INSERT INTO `%s` " % table
  285.                 k = ''
  286.                 v = ''
  287.                 for key,value in insert.iteritems():
  288.                     k = "%s`%s`, " % (k, key)
  289.                     v = "%s'%s', " % (v, self.e(value) )
  290.                
  291.                 k = k.rstrip(", ")
  292.                 v = v.rstrip(", ")
  293.                 q = "%s(%s) VALUES (%s) ON DUPLICATE KEY UPDATE " % (q, k, v)
  294.                 for key,value in update.iteritems():
  295.                     q = "%s`%s` = '%s', " % (q, key, self.e(value) )
  296.                
  297.                 q = q.rstrip(", ")
  298.                 q = "%s;" % q
  299.                
  300.                
  301.                 if self.verbose: self.out("SQL STATEMENT:\n %s\n" % q)
  302.                 self.sayLine('%s\nInserted Record:', (current_thread()))
  303.                 for key, value in insert.iteritems():
  304.                     self.sayLine( "\t\t%s: %s", (key, self.e(value)) )
  305.                
  306.                 cursor.execute(q)
  307.                 db.commit()
  308.                 cursor.close() 
  309.                 db.close()
  310.             except KeyboardInterrupt:
  311.                 self.exit()        
  312.            
  313.     def request(self, url, querystring=None):
  314.         try:
  315.             if not self.disableproxy:
  316.                 proxy = self.getProxy()
  317.                 self.out("%s\tUsing Proxy: %s" % (current_thread(), proxy['https']))
  318.             if querystring is not None:
  319.                 if self.disableproxy:
  320.                     r = requests.get(url, headers=self.headers, params=querystring, timeout=10)
  321.                 else:
  322.                     r = requests.get(url, headers=self.headers, params=querystring, proxies=proxy, timeout=10)
  323.             else:
  324.                 if self.disableproxy:
  325.                     r = requests.get(url, headers=self.headers, timeout=10)
  326.                 else:  
  327.                     r = requests.get(url, headers=self.headers, proxies=proxy, timeout=10)
  328.             response = r.json()
  329.             return response
  330.         except ValueError:
  331.            
  332.             return False
  333.         except KeyboardInterrupt:
  334.             self.exit()
  335.         except ProxyError:
  336.             if not self.disableproxy: MyProxy.flagbad(proxy)
  337.             if not self.disableproxy: self.out("%s\tProxyError: Retrying with new proxy..." % current_thread() )
  338.             self.request(url, querystring)
  339.         except ConnectionError:
  340.             if not self.disableproxy: MyProxy.flagbad(proxy)
  341.             if not self.disableproxy: self.out("%s\tConnectionError: Retrying with new proxy..." % current_thread() )
  342.             self.request(url, querystring)
  343.         except SSLError:
  344.             if not self.disableproxy: MyProxy.flagbad(proxy)
  345.             if not self.disableproxy: self.out("%s\tSSLError: Retrying with new proxy..." % current_thread() )
  346.             self.request(url, querystring)
  347.         except Timeout:
  348.             if not self.disableproxy: MyProxy.flagbad(proxy)
  349.             if not self.disableproxy: self.out("%s\tTimeout: Retrying with new proxy..." % current_thread() )
  350.             self.request(url, querystring)
  351.         except:
  352.             traceback.print_exc()
  353.             return False
  354.    
  355.     def say(self, format, args=()):
  356.         sys.stdout.write('\r                                                                                                                                                                                           \r' + format % args)
  357.         sys.stdout.flush()
  358.     def sayLine(self, format, args=()):
  359.         format = '%s\n' % format
  360.         self.say(format, args)         
  361.     def out(self, string):
  362.         self.say(string)
  363.    
  364.     def Start(self):
  365.         # ENDLESS LOOP TO ALLOW BACKGROUND THREADS
  366.         while True:
  367.             try:
  368.                 sleep(0.1)
  369.             except KeyboardInterrupt:
  370.                 self.exit()
  371.             except:
  372.                 #with self.lock:
  373.                 #   traceback.print_exc()
  374.                 self.exit()
  375.             if EntryQueue.empty():
  376.                 print "\n\nAll threads finished."
  377.                 sys.exit()
  378.     def exit(self):
  379.         # JUST A CLEAN EXIT FUNCTION
  380.         self.out( "Use the '-h' flag for command line options." )
  381.         sys.exit()
  382.                        
  383.     def snl(self, s):
  384.         # QUICK FUNCTION TO STRIP NEW LINE AND RETURN CHARACTERS
  385.         s = s.rstrip('\n')
  386.         s = s.rstrip('\r')
  387.         return s
  388.  
  389. try:
  390.     Scraper = API(options.threads, options.start, options.end, options.verbose, options.disableproxy, options.syncfile, options.forcerestart, options.host, options.port, options.name, options.username, options.password, options.apikey)
  391.     Scraper.Start()
  392. except KeyboardInterrupt:
  393.     sys.exit()
  394. except:
  395.     traceback.print_exc()
  396.     sys.exit()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement