Initial commit: Vuelato - buscador de vuelos
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Nuxt 4 + Supabase + Flightics API. Incluye búsqueda de vuelos, inspiraciones, watchlist, tracking de precios y mapa interactivo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
300
server/utils/flightics.ts
Normal file
300
server/utils/flightics.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
const BASE_URL = 'https://www.flightics.com/api/v1'
|
||||
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36'
|
||||
|
||||
const HEADERS = {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'User-Agent': USER_AGENT,
|
||||
'sec-ch-ua': '"Chromium";v="147", "Not.A/Brand";v="8"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'Referer': 'https://www.flightics.com/'
|
||||
}
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export interface PassengersCount {
|
||||
adult: number
|
||||
child: number
|
||||
infant: number
|
||||
}
|
||||
|
||||
export interface StopConfig {
|
||||
locations: string[]
|
||||
stayRange: { min: number, max: number }
|
||||
stayDateRange: { begin: string, end: string }
|
||||
continueFromAny: boolean
|
||||
}
|
||||
|
||||
export interface SearchParams {
|
||||
departures: string[]
|
||||
local: string
|
||||
departureDateInterval: { begin: string, end: string }
|
||||
stops: StopConfig[]
|
||||
endInSameLocation: boolean
|
||||
maxStops: number | null
|
||||
fixStopsOrder: boolean
|
||||
stopLength: { min: number, max: number, isSet: boolean }
|
||||
maxResults: number
|
||||
passengersCount: PassengersCount
|
||||
}
|
||||
|
||||
export interface Company {
|
||||
name: string | null
|
||||
code: string
|
||||
icao: string
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
id: string
|
||||
departureCode: string
|
||||
departureCity: string
|
||||
departureDate: string
|
||||
departureTimestamp: number
|
||||
departureDateUtc: string
|
||||
departureUtcTimestamp: number
|
||||
arrivalCode: string
|
||||
arrivalCity: string
|
||||
arrivalDate: string
|
||||
arrivalTimestamp: number
|
||||
arrivalDateUtc: string
|
||||
arrivalUtcTimestamp: number
|
||||
number: number
|
||||
company: Company
|
||||
isHidden: boolean
|
||||
}
|
||||
|
||||
export interface Leg {
|
||||
segments: Segment[]
|
||||
}
|
||||
|
||||
export interface Trip {
|
||||
legs: Leg[]
|
||||
totalCost: number
|
||||
bookingToken: string
|
||||
currency: string
|
||||
deepLink: string | null
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
trips: Trip[]
|
||||
notComplete: boolean
|
||||
contractVersion: number
|
||||
responseId: string
|
||||
}
|
||||
|
||||
export interface DetailResponse {
|
||||
trip: Trip
|
||||
}
|
||||
|
||||
export interface Country {
|
||||
isoCode2: string
|
||||
isoCode3: string
|
||||
nameEng: string
|
||||
nameNative: string
|
||||
phonePreselection: string
|
||||
}
|
||||
|
||||
export interface InspirationItem {
|
||||
from: string
|
||||
to: string[]
|
||||
minPrice: number
|
||||
minStops: number
|
||||
}
|
||||
|
||||
export interface InspirationsResponse {
|
||||
items: InspirationItem[]
|
||||
}
|
||||
|
||||
// --- API: Search ---
|
||||
|
||||
export async function searchTrips(params: SearchParams): Promise<SearchResponse> {
|
||||
return $fetch(`${BASE_URL}/trips/search`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll search until notComplete is false or maxPolls reached.
|
||||
* The API returns partial results with notComplete: true.
|
||||
* Re-calling with the same params fetches more results.
|
||||
*/
|
||||
export async function searchTripsComplete(params: SearchParams, maxPolls: number = 3): Promise<SearchResponse> {
|
||||
let allTrips: Trip[] = []
|
||||
let lastResponse: SearchResponse | null = null
|
||||
|
||||
for (let i = 0; i < maxPolls; i++) {
|
||||
const res = await searchTrips(params)
|
||||
allTrips = [...allTrips, ...res.trips]
|
||||
lastResponse = res
|
||||
if (!res.notComplete) break
|
||||
// Small delay between polls
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
}
|
||||
|
||||
return {
|
||||
trips: allTrips,
|
||||
notComplete: lastResponse?.notComplete ?? false,
|
||||
contractVersion: lastResponse?.contractVersion ?? 0,
|
||||
responseId: lastResponse?.responseId ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
// --- API: Trip Detail & Check ---
|
||||
|
||||
export async function getTripDetail(bookingToken: string, local: string, passengersCount: PassengersCount): Promise<DetailResponse> {
|
||||
return $fetch(`${BASE_URL}/trip/detail`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: { bookingToken, local, passengersCount }
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkTrip(bookingToken: string, local: string, passengersCount: PassengersCount): Promise<DetailResponse> {
|
||||
return $fetch(`${BASE_URL}/trip/check`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: { bookingToken, local, passengersCount }
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Inspirations ---
|
||||
|
||||
export async function getInspirations(from: string, take: number = 100, locale: string = 'en'): Promise<InspirationsResponse> {
|
||||
return $fetch(`${BASE_URL}/inspirations/search`, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': USER_AGENT },
|
||||
query: { from, take, locale }
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Weekend Search ---
|
||||
|
||||
export async function searchWeekendTrips(params: SearchParams): Promise<SearchResponse> {
|
||||
return $fetch(`${BASE_URL}/trips/weekend/search`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: params
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Route Flights ---
|
||||
|
||||
export interface RouteFlightsResponse {
|
||||
contractVersion: number
|
||||
returnResults: {
|
||||
trips: Trip[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export async function getRouteFlights(from: string, to: string, locale: string = 'en', passengersCount?: PassengersCount): Promise<RouteFlightsResponse> {
|
||||
return $fetch(`${BASE_URL}/route-flights`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: { from, to, locale, passengersCount }
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Multi-City Inspirations ---
|
||||
|
||||
export interface MultiCityInspirationItem {
|
||||
from: string
|
||||
stops: string[]
|
||||
minPrice: number
|
||||
}
|
||||
|
||||
export interface MultiCityInspirationsResponse {
|
||||
currency: { name: string, code: string, symbol: string }
|
||||
items: MultiCityInspirationItem[]
|
||||
}
|
||||
|
||||
export async function getMultiCityInspirations(startLocationsCodes: string[], locale: string = 'en', take: number = 20): Promise<MultiCityInspirationsResponse> {
|
||||
return $fetch(`${BASE_URL}/inspirations/search-multi-city`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: { startLocationsCodes, locale, take }
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Locations ---
|
||||
|
||||
export interface Airport {
|
||||
id: string
|
||||
iata: string
|
||||
icao: string
|
||||
nameEng: string
|
||||
lat: number
|
||||
lon: number
|
||||
cityId: string
|
||||
timeZone: string
|
||||
timeZoneOffset: string
|
||||
}
|
||||
|
||||
export interface City {
|
||||
id: string
|
||||
name: string
|
||||
nameEng: string
|
||||
countryId: string
|
||||
lat: number
|
||||
lon: number
|
||||
}
|
||||
|
||||
export interface LocationCountry {
|
||||
id: string
|
||||
name: string
|
||||
nameEng: string
|
||||
isoCode2: string
|
||||
}
|
||||
|
||||
export interface LocationsResponse {
|
||||
contractVersion: number
|
||||
locale: string
|
||||
airports: Airport[]
|
||||
cities: City[]
|
||||
countries: LocationCountry[]
|
||||
}
|
||||
|
||||
export async function getLocations(): Promise<LocationsResponse> {
|
||||
return $fetch(`${BASE_URL}/locations`, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': USER_AGENT }
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Route Data ---
|
||||
|
||||
export async function getRouteData(departureCode: string, arrivalCode: string): Promise<any> {
|
||||
return $fetch(`${BASE_URL}/route-data`, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': USER_AGENT },
|
||||
query: { departureCode, arrivalCode }
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Countries ---
|
||||
|
||||
export async function getCountries(): Promise<{ countries: Country[] }> {
|
||||
return $fetch(`${BASE_URL}/countries`, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': USER_AGENT }
|
||||
})
|
||||
}
|
||||
|
||||
// --- API: Booking ---
|
||||
|
||||
export async function getBookingInfo(bookingToken: string, passengersCount: PassengersCount): Promise<any> {
|
||||
return $fetch(`${BASE_URL}/booking/info`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: { bookingToken, passengersCount }
|
||||
})
|
||||
}
|
||||
|
||||
export async function initBooking(bookingToken: string, passengersCount: PassengersCount): Promise<any> {
|
||||
return $fetch(`${BASE_URL}/booking/init`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: { bookingToken, passengersCount }
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user