Files
vuelato/explore.mjs
Alejandro Martinez b8906efc80
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Initial commit: Vuelato - buscador de vuelos
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>
2026-04-10 23:37:06 +02:00

319 lines
12 KiB
JavaScript

import { chromium } from "playwright";
import { writeFileSync, appendFileSync } from "fs";
const LOG_FILE = "flightics_explore.log";
writeFileSync(LOG_FILE, `=== Flightics Full Exploration - ${new Date().toISOString()} ===\n\n`);
function log(text) {
console.log(text);
appendFileSync(LOG_FILE, text + "\n");
}
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
userAgent: "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 page = await context.newPage();
const allRequests = new Map();
page.on("request", (req) => {
const url = req.url();
const method = req.method();
const rt = req.resourceType();
if ((rt === "xhr" || rt === "fetch") && url.includes("flightics.com")) {
const key = `${method} ${url.split("?")[0]}`;
if (!allRequests.has(key)) {
allRequests.set(key, { method, url, headers: req.headers(), postData: req.postData() });
log(`\n>>> ${method} ${url}`);
if (req.postData()) log(` BODY: ${req.postData()}`);
}
}
});
page.on("response", async (res) => {
const req = res.request();
const url = res.url();
if ((req.resourceType() === "xhr" || req.resourceType() === "fetch") && url.includes("flightics.com")) {
try {
const ct = res.headers()["content-type"] || "";
if (ct.includes("json")) {
const body = await res.json();
log(`<<< ${res.status()} ${url}`);
log(` RESPONSE: ${JSON.stringify(body, null, 2).substring(0, 3000)}`);
} else if (ct.includes("grpc")) {
log(`<<< ${res.status()} ${url} [grpc-web binary]`);
const buf = await res.body();
log(` RESPONSE SIZE: ${buf.length} bytes`);
}
} catch {}
}
});
log("=== PHASE 1: Homepage ===");
await page.goto("https://www.flightics.com/en");
await page.waitForTimeout(3000);
log("\n=== PHASE 2: Inspiration pages ===");
// Try different inspiration endpoints
const airports = ["MAD", "BCN", "AGP", "GRX", "SVQ", "VLC", "PMI", "TFS", "LPA"];
for (const apt of airports.slice(0, 3)) {
try {
await page.goto(`https://www.flightics.com/en/from/${apt.toLowerCase()}`);
await page.waitForTimeout(2000);
} catch {}
}
log("\n=== PHASE 3: Search with different params ===");
// One-way search
await page.goto("https://www.flightics.com/en/trip/1-0-0/20-05-2026/mad/nte/0-0");
await page.waitForTimeout(5000);
// Multi-city / complex
await page.goto("https://www.flightics.com/en/trip/2-0-0/01-06-2026_15-06-2026/mad/nte/3-7");
await page.waitForTimeout(5000);
// Explore URL patterns
await page.goto("https://www.flightics.com/en/trip/1-0-0/20-05-2026/bcn/cdg/0-0?maxStops=0");
await page.waitForTimeout(5000);
log("\n=== PHASE 4: Probing API endpoints ===");
// Try common API patterns
const probeEndpoints = [
{ method: "GET", path: "/api/v1" },
{ method: "GET", path: "/api/v1/airports" },
{ method: "GET", path: "/api/v1/airlines" },
{ method: "GET", path: "/api/v1/routes" },
{ method: "GET", path: "/api/v1/countries" },
{ method: "GET", path: "/api/v1/cities" },
{ method: "GET", path: "/api/v1/cheapest" },
{ method: "GET", path: "/api/v1/calendar" },
{ method: "GET", path: "/api/v1/prices" },
{ method: "GET", path: "/api/v1/inspirations" },
{ method: "GET", path: "/api/v1/inspirations/search?from=MAD&take=100&locale=en" },
{ method: "GET", path: "/api/v1/inspirations/search?from=BCN&take=100&locale=en" },
{ method: "GET", path: "/api/v1/inspirations/search?from=MAD,BCN,AGP,GRX,SVQ,VLC&take=100&locale=en" },
{ method: "GET", path: "/api/v2" },
{ method: "GET", path: "/api/v2/trips/search" },
{ method: "GET", path: "/api" },
{ method: "GET", path: "/_configuration" },
{ method: "GET", path: "/_blazor" },
{ method: "POST", path: "/api/v1/trips/search/calendar" },
{ method: "POST", path: "/api/v1/trips/cheapest" },
{ method: "POST", path: "/api/v1/airports/search" },
{ method: "POST", path: "/api/v1/routes/search" },
{ method: "GET", path: "/api/v1/config" },
{ method: "GET", path: "/api/v1/settings" },
{ method: "GET", path: "/api/v1/meta" },
{ method: "GET", path: "/api/v1/health" },
{ method: "GET", path: "/api/v1/version" },
{ method: "GET", path: "/api/v1/status" },
{ method: "GET", path: "/api/v1/sitemap" },
];
// Also probe gRPC services
const grpcServices = [
"trips.v1.TripService/SearchTrips",
"trips.v1.TripService/GetTripDetail",
"trips.v1.TripService/CheckTrip",
"locations.v1.LocationsService/GetLocationsV1",
"locations.v1.LocationsService/SearchLocations",
"inspirations.v1.InspirationService/GetInspirationsV1",
"inspirations.v1.InspirationService/SearchInspirations",
"airports.v1.AirportService/GetAirports",
"airports.v1.AirportService/SearchAirports",
"airlines.v1.AirlineService/GetAirlines",
"routes.v1.RouteService/GetRoutes",
"prices.v1.PriceService/GetPrices",
"calendar.v1.CalendarService/GetCalendar",
];
for (const ep of probeEndpoints) {
const url = `https://www.flightics.com${ep.path}`;
try {
const opts = { method: ep.method, headers: { "Content-Type": "application/json" } };
if (ep.method === "POST") opts.body = JSON.stringify({});
const res = await page.evaluate(async ({ url, opts }) => {
try {
const r = await fetch(url, opts);
const text = await r.text();
return { status: r.status, body: text.substring(0, 2000), contentType: r.headers.get("content-type") };
} catch (e) {
return { status: -1, body: e.message, contentType: null };
}
}, { url, opts });
if (res.status !== 404 && res.status !== -1 && res.status !== 405) {
log(`\n[PROBE] ${ep.method} ${ep.path} => ${res.status} (${res.contentType})`);
log(` ${res.body.substring(0, 1500)}`);
} else {
log(`[PROBE] ${ep.method} ${ep.path} => ${res.status}`);
}
} catch (e) {
log(`[PROBE] ${ep.method} ${ep.path} => ERROR: ${e.message}`);
}
}
for (const svc of grpcServices) {
const url = `https://www.flightics.com/${svc}`;
try {
const res = await page.evaluate(async ({ url }) => {
try {
const r = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/grpc-web",
"x-user-agent": "grpc-dotnet/2.76.0",
"grpc-accept-encoding": "identity,gzip,deflate",
},
body: new Uint8Array([0, 0, 0, 0, 0]),
});
return { status: r.status, headers: Object.fromEntries(r.headers.entries()) };
} catch (e) {
return { status: -1, error: e.message };
}
}, { url });
const grpcStatus = res.headers?.["grpc-status"];
if (res.status === 200 || grpcStatus !== undefined) {
log(`\n[gRPC] ${svc} => HTTP ${res.status}, grpc-status: ${grpcStatus || "N/A"}`);
log(` Headers: ${JSON.stringify(res.headers)}`);
} else {
log(`[gRPC] ${svc} => ${res.status}`);
}
} catch (e) {
log(`[gRPC] ${svc} => ERROR: ${e.message}`);
}
}
log("\n=== PHASE 5: Try search with POST body variations ===");
const searchVariations = [
// One-way
{
departures: ["MAD"],
local: "en",
departureDateInterval: { begin: "2026-05-20T00:00:00+00:00", end: "2026-05-20T00:00:00+00:00" },
stops: [{ locations: ["NTE"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false }],
endInSameLocation: false, maxStops: null, fixStopsOrder: false,
stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10,
passengersCount: { adult: 1, child: 0, infant: 0 }
},
// Anywhere destination (empty locations)
{
departures: ["MAD"],
local: "en",
departureDateInterval: { begin: "2026-06-01T00:00:00+00:00", end: "2026-06-30T00:00:00+00:00" },
stops: [{ locations: [], stayRange: { min: 48, max: 168 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true }, { locations: ["MAD"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false }],
endInSameLocation: false, maxStops: null, fixStopsOrder: false,
stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10,
passengersCount: { adult: 1, child: 0, infant: 0 }
},
// Multi-stop (3 cities)
{
departures: ["MAD"],
local: "en",
departureDateInterval: { begin: "2026-06-01T00:00:00+00:00", end: "2026-06-30T00:00:00+00:00" },
stops: [
{ locations: ["PAR"], stayRange: { min: 48, max: 72 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true },
{ locations: ["ROM"], stayRange: { min: 48, max: 72 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true },
{ locations: ["MAD"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false },
],
endInSameLocation: false, maxStops: null, fixStopsOrder: false,
stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10,
passengersCount: { adult: 1, child: 0, infant: 0 }
},
// Direct flights only
{
departures: ["BCN"],
local: "en",
departureDateInterval: { begin: "2026-06-01T00:00:00+00:00", end: "2026-06-15T00:00:00+00:00" },
stops: [{ locations: ["LHR"], stayRange: { min: 48, max: 120 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true }, { locations: ["BCN"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false }],
endInSameLocation: false, maxStops: 0, fixStopsOrder: false,
stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10,
passengersCount: { adult: 1, child: 0, infant: 0 }
},
];
for (let i = 0; i < searchVariations.length; i++) {
const variant = searchVariations[i];
log(`\n--- Search variation ${i + 1} ---`);
try {
const res = await page.evaluate(async ({ body }) => {
const r = await fetch("https://www.flightics.com/api/v1/trips/search", {
method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(body),
});
const data = await r.json();
return { status: r.status, tripCount: data.trips?.length || 0, sample: JSON.stringify(data).substring(0, 2000) };
}, { body: variant });
log(`[SEARCH] Status: ${res.status}, Trips: ${res.tripCount}`);
log(` Body sent: ${JSON.stringify(variant).substring(0, 500)}`);
if (res.tripCount > 0) {
log(` Sample: ${res.sample.substring(0, 1000)}`);
}
} catch (e) {
log(`[SEARCH] ERROR: ${e.message}`);
}
}
log("\n=== PHASE 6: Explore sitemap and other pages ===");
const pagesToVisit = [
"/en/about",
"/en/privacy",
"/en/terms",
"/en/faq",
"/en/contact",
"/en/blog",
"/en/destinations",
"/en/airlines",
"/en/airports",
"/en/cheap-flights",
"/en/map",
"/en/calendar",
"/en/explore",
"/en/anywhere",
"/sitemap.xml",
"/robots.txt",
];
for (const p of pagesToVisit) {
try {
const res = await page.evaluate(async ({ url }) => {
const r = await fetch(url);
return { status: r.status, contentType: r.headers.get("content-type"), size: (await r.text()).length };
}, { url: `https://www.flightics.com${p}` });
if (res.status !== 404) {
log(`[PAGE] ${p} => ${res.status} (${res.contentType}, ${res.size} bytes)`);
}
} catch {}
}
// Try to get the blazor boot manifest to discover API surface
log("\n=== PHASE 7: Blazor manifest ===");
try {
const res = await page.evaluate(async () => {
const r = await fetch("https://www.flightics.com/_framework/blazor.boot.json");
if (r.ok) return await r.text();
return `Status: ${r.status}`;
});
if (res.length < 5000) {
log(`[BLAZOR] boot.json: ${res}`);
} else {
log(`[BLAZOR] boot.json: ${res.length} bytes (too large, checking for API hints...)`);
// Extract interesting parts
const matches = res.match(/[A-Za-z]+\.(Api|Service|Grpc|Client)[A-Za-z.]*/g);
if (matches) {
log(` API-related assemblies: ${[...new Set(matches)].join(", ")}`);
}
}
} catch (e) {
log(`[BLAZOR] ERROR: ${e.message}`);
}
log("\n\n=== SUMMARY OF UNIQUE ENDPOINTS ===");
for (const [key, val] of allRequests) {
log(` ${key}`);
}
log("\n=== DONE ===");
await browser.close();