├── src ├── __init__.py ├── ryanair.py ├── utils.py ├── seats.py └── RyanairApi.py ├── .gitignore ├── seats.png ├── screenshot.png ├── ryanair-seats.py └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | setup.py 3 | -------------------------------------------------------------------------------- /seats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santoru/ryanair-seats/HEAD/seats.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santoru/ryanair-seats/HEAD/screenshot.png -------------------------------------------------------------------------------- /ryanair-seats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from src import ryanair 4 | 5 | 6 | if __name__ == '__main__': 7 | ryanair.main() 8 | -------------------------------------------------------------------------------- /src/ryanair.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import getpass 4 | 5 | from RyanairApi import RyanairApi 6 | 7 | 8 | def main(): 9 | try: 10 | print "Welcome!" 11 | 12 | username = raw_input("Please enter username: ") 13 | password = getpass.getpass("Please enter password:") 14 | 15 | ryanairApi = RyanairApi(username, password) 16 | ryanairApi.getAllSeats() 17 | exit(0) 18 | except KeyboardInterrupt: 19 | print "\nQuitting.." 20 | exit(0) 21 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | # Headers 2 | CLIENT_VERSION = '3.27.1' 3 | CLIENT_OS = 'ios' 4 | CLIENT_V_OS = '10.1.1' 5 | USER_AGENT = 'RyanairApp/3.27.1 (iPhone; iOS 10.1.1; Scale/2.00)' 6 | 7 | # Max passengers supported at the moment 8 | MAXPASSENGERS = 1 9 | 10 | 11 | def getHeaders(): 12 | """ Basic headers to set-up a request""" 13 | headers = { 14 | 'Accept': '*/*', 15 | 'Connection': 'close', 16 | 'client-version': CLIENT_VERSION, 17 | 'client': CLIENT_OS, 18 | 'Accept-Language': 'en-GB;q=1', 19 | 'User-Agent': USER_AGENT, 20 | 'client-os': CLIENT_V_OS 21 | } 22 | return headers 23 | 24 | 25 | class bclr: 26 | HEADER = '\033[95m' 27 | OKBLUE = '\033[94m' 28 | OKGREEN = '\033[92m' 29 | WARNING = '\033[93m' 30 | FAIL = '\033[91m' 31 | ENDC = '\033[0m' 32 | BOLD = '\033[1m' 33 | UNDERLINE = '\033[4m' 34 | 35 | 36 | def parseDate(string): 37 | """Parse that string.""" 38 | # TODO: Give some format based on timezone, please :D 39 | date = string.split('T')[0] 40 | time = string.split('T')[1] 41 | datef = date.split('-') 42 | datep = datef[2] + '/' + datef[1] + '/' + datef[0] 43 | timef = time.split(':') 44 | timep = timef[0] + ':' + timef[1] 45 | return bclr.WARNING + \ 46 | datep + bclr.ENDC + ' - ' + bclr.WARNING + timep + bclr.ENDC 47 | -------------------------------------------------------------------------------- /src/seats.py: -------------------------------------------------------------------------------- 1 | # Seats are allocated with this order.. 2 | rows = [ 3 | '33', 4 | '20', 5 | '19', 6 | '22', 7 | '15', 8 | '24', 9 | '12', 10 | '26', 11 | '10', 12 | '28', 13 | '08', 14 | '30', 15 | '18', 16 | '21', 17 | '14', 18 | '23', 19 | '11', 20 | '25', 21 | '09', 22 | '27', 23 | '29', 24 | '07', 25 | '06', 26 | '05', 27 | '04', 28 | '03', 29 | '02', 30 | '01', 31 | '17', 32 | '16', 33 | '31', 34 | '32' 35 | ] 36 | 37 | # ...from A to F 38 | order = [ 39 | 'A', 40 | 'B', 41 | 'C', 42 | 'D', 43 | 'E', 44 | 'F' 45 | ] 46 | 47 | # First row only has 3 seats 48 | nonexistent = [ 49 | '01D', 50 | '01E', 51 | '01F', 52 | ] 53 | 54 | # Larger seats 55 | morespace = [ 56 | '01A', 57 | '01B', 58 | '01C', 59 | '02D', 60 | '02E', 61 | '02F', 62 | ] 63 | 64 | 65 | def getFirstFree(unavailable): 66 | """ Return the seat that will be allocated next 67 | according to the seats already allocated (unavailable). """ 68 | 69 | # FIXME: The row are separated in two groups (ABC + DEF) 70 | # If the first group is empty but the second is not, the single place 71 | # is allocated on the second group, instead of on the first. 72 | 73 | for row in rows: 74 | for place in order: 75 | seat = str(row) + place 76 | if seat not in unavailable and seat not in nonexistent: 77 | return seat 78 | return 'N/D' 79 | 80 | 81 | def seatInfo(seat): 82 | """Give some informations about a seat.""" 83 | 84 | info = '' 85 | if 'A' in seat: 86 | info += 'Close to the left window' 87 | if 'F' in seat: 88 | info += 'Close to the right window' 89 | if 'C' in seat or 'D' in seat: 90 | info += 'Middle seat' 91 | if seat in morespace: 92 | if info == '': 93 | info += 'More space for legs' 94 | else: 95 | info += ' and more space for legs' 96 | if '16' in seat or '17' in seat: 97 | if info == '': 98 | info += 'More space for legs, emergency exit in the middle' 99 | else: 100 | info += ' and more space for legs, emergency exit in the middle' 101 | if info != '': 102 | return '(' + info + ')' 103 | return info 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ryanair seats prediction 2 | ### A tool to predict which will be your free seat 3 | ##### And a trick to get that damn window-close seat! :P 4 | 5 | This tool will try to predict which seat you will get for free if you will do the check-in just after running the tool (If nobody else will do check-in after you run the script but before you do the check-in). 6 | 7 | ### UPDATE 24/05/2017 8 | I just realized that Ryanair seems to have changed the allocation algorithm. The tool is no more reliable then.. 9 | If somebody wants to update the algorithm, just send a pull requests or write to me :) We can work together on it. 10 | 11 | ### USAGE 12 | It's not that hard :P 13 | You will just need to install *requests* 14 | ``` 15 | pip install requests 16 | ``` 17 | ...and then 18 | ``` 19 | ./ryanair-seats.py 20 | ``` 21 | 22 | And then log-in. 23 | 24 |

25 | Screenshot 26 |

27 | 28 | ### TO DO 29 | - Currently the tool works only for one person, I need more investigation to understand how seats are allocated for more than one person. 30 | - Even when predicting one seat, the tool can't be 100% reliable: there are "hard" situations that are currently not checked by the tool. 31 | 32 | 33 | ### FAQ 34 | 35 | #### How it works? 36 | Ryanair let customer buy a seat anytime for the flight they booked. Thanks to this, it's possible to get the list of the seats already reserved. With some analysis on the "Allocation Algorithm" it was possible to understand how the allocation works: Seats are allocated from the middle of the plane, to the ends (both top and down together). Seats are allocated (generally) from A to F. If there is enough space to allocate all passengers of a booking, then the current row is filled. If passenger exceeded the current row space, then the system decide to put them in a new row. 37 | Emergency exit are "filled" when the other seats are allocated. 38 | 39 |

40 | Seats 41 |

42 | 43 | 44 | #### What can I do with this tool? 45 | This tool will try to predict which seat you will get for free if you will do the check-in just after running the tool :P (If nobody else will do check-in after you run the script but before you do the check-in). 46 | Or at least it will try, it's pretty accurate but not perfect yet :D 47 | 48 | #### So, can I choose my seat? 49 | Yes, you can! You just have to pay few euro and get your seat :) 50 | This tool doesn't want to provide a way to get free seats, but just to show how Ryanair seats are allocated. 51 | 52 | --- 53 | -------------------------------------------------------------------------------- /src/RyanairApi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import requests 4 | import json 5 | import seats 6 | import utils 7 | import logging 8 | 9 | 10 | class RyanairApi(object): 11 | logging.basicConfig(filename='/dev/null', level=logging.NOTSET) 12 | 13 | basicApiUrl = 'https://api.ryanair.com/' 14 | bookingApiUrl = 'https://nativeapps.ryanair.com/' 15 | 16 | customerId = "" 17 | authToken = "" 18 | sessionToken = "" 19 | 20 | def __init__(self, username, password): 21 | """Get the auth token and the customer ID""" 22 | url = 'userprofile/rest/api/v1/login' 23 | headers = utils.getHeaders() 24 | headers.update({ 25 | 'Content-Type': 'application/x-www-form-urlencoded', 26 | 'Content-Lenght': '53' 27 | }) 28 | params = { 29 | 'password': password, 30 | 'username': username 31 | } 32 | response = requests.post( 33 | self.basicApiUrl + url, 34 | headers=headers, 35 | params=params) 36 | 37 | if response.status_code != 200: 38 | logging.error('Impossible to login') 39 | print('Login error.') 40 | exit(1) 41 | data = response.json() 42 | 43 | self.customerId = data['customerId'] 44 | self.authToken = data['token'] 45 | logging.debug('User logged in') 46 | 47 | def getProfile(self): 48 | """Return user profile, printing a description""" 49 | url = 'userprofile/rest/api/v1/secure/users/' + \ 50 | self.customerId + \ 51 | '/profile/full' 52 | headers = utils.getHeaders() 53 | headers.update({ 54 | 'X-Auth-Token': self.authToken 55 | }) 56 | response = requests.get( 57 | self.basicApiUrl + url, 58 | headers=headers) 59 | if response.status_code != 200: 60 | logging.error('Impossible to fetch profile') 61 | print('Impossible to fetch profile!') 62 | exit(1) 63 | user = response.json() 64 | print 'So....' 65 | print 'You\'re ' + user['firstName'] + ' ' + user['lastName'] 66 | print 'Your nationality is ' + user['nationality'] 67 | print 'Phone = ' + user['countryCallingCode'] + user['phoneNumber'] 68 | print '--------------------------' 69 | return user 70 | 71 | def getUpcomingBookings(self): 72 | """Get the list of all upcoming bookings""" 73 | url = 'userprofile/rest/api/v1/secure/users/' + \ 74 | self.customerId + \ 75 | '/bookings/upcoming/all' 76 | headers = utils.getHeaders() 77 | headers.update({ 78 | 'X-Auth-Token': self.authToken 79 | }) 80 | response = requests.get(self.basicApiUrl + url, headers=headers) 81 | if response.status_code != 200: 82 | logging.error('Impossible to fetch upcoming bookings') 83 | print('Impossible to fetch upcoming bookings!') 84 | exit(1) 85 | return response.json() 86 | 87 | def getAllSeats(self): 88 | bookings = self.getUpcomingBookings() 89 | print 'You have %s travel booked!' % bookings['count'] 90 | # Display every booking 91 | for travel in bookings['Bookings']: 92 | self.infoBooking(travel['BookingId']) 93 | return 94 | 95 | def infoBooking(self, bookingId): 96 | """ Get and display information about one booking: 97 | - Print informations about the flights on the booking 98 | - Print informations about the passengers of the flights 99 | - Print a guess of the seat it will be allocated to the passengers 100 | """ 101 | url = 'v4/Booking' 102 | headers = utils.getHeaders() 103 | headers.update({ 104 | 'Content-Type': 'application/json', 105 | 'Content-Length': '52', 106 | 'X-Auth-Token': self.authToken 107 | }) 108 | data = { 109 | 'surrogateId': self.customerId, 110 | 'bookingId': bookingId 111 | } 112 | response = requests.post( 113 | self.bookingApiUrl + url, 114 | headers=headers, 115 | data=json.dumps(data)) 116 | 117 | if response.status_code != 200: 118 | logging.error('Impossible to fetch bookings') 119 | print('Impossible to fetch bookings!') 120 | exit(1) 121 | result = response.json() 122 | 123 | # Save the received sessionToken 124 | self.sessionToken = response.headers['X-Session-Token'] 125 | # Get informations about seats for this booking 126 | seatsList = self.infoSeats() 127 | if seatsList is False: 128 | logging.error('Impossible to fetch seats') 129 | print('Impossible to fetch seats!') 130 | exit(1) 131 | 132 | # Flight info 133 | print 'Prenotation number: %s (Status of the flight: %s)' % ( 134 | utils.bclr.OKBLUE + result['info']['pnr'] + utils.bclr.ENDC, 135 | utils.bclr.OKGREEN + result['info']['status'] + utils.bclr.ENDC) 136 | numberofSeats = len(result['passengers']) 137 | print 'Number of passengers: %s' % numberofSeats 138 | c = 0 139 | # Print information about passengers 140 | for passenger in result['passengers']: 141 | c += 1 142 | print ' %i: %s %s' % ( 143 | c, 144 | utils.bclr.HEADER + passenger['name']['first'], 145 | passenger['name']['last'] + utils.bclr.ENDC) 146 | c = 0 147 | for journey in result['journeys']: 148 | 149 | print ' [%s] %s -> %s (%s)' % ( 150 | utils.bclr.OKBLUE + journey['flt'] + utils.bclr.ENDC, 151 | utils.bclr.WARNING + journey['orig'] + utils.bclr.ENDC, 152 | utils.bclr.WARNING + journey['dest'] + utils.bclr.ENDC, 153 | utils.parseDate(journey['depart'])) 154 | 155 | if 'reasonCode' in journey['changeInfo'] and \ 156 | journey['changeInfo']['reasonCode'] == 'PassengerCheckedIn': 157 | print ' Already checked-in' 158 | else: 159 | allocation = self.getSeat( 160 | seatsList[c]['unavailableSeats'], 161 | numberofSeats) 162 | if (allocation['status'] == 'error'): 163 | print allocation['message'] 164 | else: 165 | print ' If you do check-in now, you will have \ 166 | seat %s %s' % ( 167 | utils.bclr.OKGREEN + 168 | allocation['seat'] + 169 | utils.bclr.ENDC, 170 | seats.seatInfo(allocation['seat'])) 171 | c += 1 172 | return 173 | 174 | def infoSeats(self): 175 | """ Show informations about seats for the booking 176 | - Return also the list of unavailable (already allocated) seats 177 | """ 178 | url = 'v4/Seat' 179 | headers = utils.getHeaders() 180 | headers.update({ 181 | 'X-Session-Token': self.sessionToken 182 | }) 183 | response = requests.get(self.bookingApiUrl + url, headers=headers) 184 | if response.status_code == 200: 185 | return response.json() 186 | return False 187 | 188 | def getSeat(self, unavailable, numberofSeats): 189 | """ Return the seat that will be allocated during check-in""" 190 | response = dict() 191 | if utils.MAXPASSENGERS > numberofSeats: 192 | response['status'] = 'error' 193 | response['message'] = ' We are sorry but we currently \ 194 | don\'t support seat prediction for flights \ 195 | with %s passengers.' % numberofSeats 196 | else: 197 | response['status'] = 'ok' 198 | response['seat'] = seats.getFirstFree(unavailable) 199 | 200 | return response 201 | --------------------------------------------------------------------------------