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:
318
explore.mjs
Normal file
318
explore.mjs
Normal file
@@ -0,0 +1,318 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user