From 2c28de68b924cbf708a370be6dcc48381e77dd40 Mon Sep 17 00:00:00 2001 From: Alexandru Date: Sat, 28 Sep 2024 16:25:44 +0300 Subject: [PATCH 1/3] Improve code --- background.js | 39 ---------------- content.js | 35 +++++++++++++++ manifest.json | 3 -- popup.html | 6 ++- popup.js | 120 ++++++++++++++++++++++++++++++++------------------ 5 files changed, 116 insertions(+), 87 deletions(-) delete mode 100644 background.js diff --git a/background.js b/background.js deleted file mode 100644 index a4fe60d..0000000 --- a/background.js +++ /dev/null @@ -1,39 +0,0 @@ -let headers = null; - -function getDynamicUrl() { - return new Promise((resolve, reject) => { - chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { - const currentTab = tabs[0]; - if (currentTab.url.includes('multipass.wizzair.com')) { - const urlParts = currentTab.url.split('/'); - const uuid = urlParts[urlParts.length - 1]; - resolve(`https://multipass.wizzair.com/w6/subscriptions/json/availability/${uuid}`); - } else { - reject(new Error("Not on the Wizzair Multipass page")); - } - }); - }); -} - -chrome.webRequest.onBeforeSendHeaders.addListener( - function(details) { - if (details.url === getDynamicUrl()) { - headers = details.requestHeaders; - } - }, - {urls: [""]}, - ["requestHeaders"] -); - -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === "getHeaders") { - if (headers) { - sendResponse({ headers: headers }); - } else { - sendResponse({ headers: { - 'Content-Type': 'application/json', - }}); - } - } - return true; -}); \ No newline at end of file diff --git a/content.js b/content.js index 3bebf50..9d2a416 100644 --- a/content.js +++ b/content.js @@ -62,3 +62,38 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { return true; } }); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "getDynamicUrl") { + setTimeout(() => { + const pageContent = document.head.innerHTML; + const match = pageContent.match(/"searchFlight":"https:\/\/multipass\.wizzair\.com[^"]+\/([^"]+)"/); + if (match && match[1]) { + const uuid = match[1]; + const dynamicUrl = `https://multipass.wizzair.com/w6/subscriptions/json/availability/${uuid}`; + sendResponse({dynamicUrl: dynamicUrl}); + } else { + console.log('Dynamic ID not found in page content'); + sendResponse({error: "Dynamic ID not found"}); + } + }, 1000); + return true; + } +}); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "getHeaders") { + const headers = {}; + performance.getEntriesByType("resource").forEach(entry => { + if (entry.name.includes("multipass.wizzair.com")) { + entry.serverTiming.forEach(timing => { + if (timing.name.startsWith("request_header_")) { + const headerName = timing.name.replace("request_header_", ""); + headers[headerName] = timing.description; + } + }); + } + }); + sendResponse({headers: headers}); + } +}); diff --git a/manifest.json b/manifest.json index f67ff6e..505feb7 100644 --- a/manifest.json +++ b/manifest.json @@ -14,9 +14,6 @@ "default_popup": "popup.html", "default_icon": "icon.png" }, - "background": { - "service_worker": "background.js" - }, "content_scripts": [ { "matches": ["https://multipass.wizzair.com/*"], diff --git a/popup.html b/popup.html index 2075bff..88b67a8 100644 --- a/popup.html +++ b/popup.html @@ -22,7 +22,8 @@

Wizz AYCF Route Finder

GitHub | - Feedback (Discord) + Feedback (Discord) | + ☕️ Tips

@@ -47,7 +48,8 @@

Wizz AYCF Route Finder

-

Note: First go to the Wizzair Multipass page, enter a random route and press Search. Then you can use this extension.

+

Disclaimer: This extension is not affiliated with Wizz Air or its partners in any way. It is a personal project to help Multipass users like me find available routes.

+

Please use responsibly and at your own risk. Doing too many searches in a short period of time will result in temporary rate limiting.

diff --git a/popup.js b/popup.js index d8c383d..e86942d 100644 --- a/popup.js +++ b/popup.js @@ -28,41 +28,63 @@ function getDynamicUrl() { return new Promise((resolve, reject) => { chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { const currentTab = tabs[0]; - if (currentTab.url.includes('multipass.wizzair.com')) { - const urlParts = currentTab.url.split('/'); - const uuid = urlParts[urlParts.length - 1]; - resolve(`https://multipass.wizzair.com/w6/subscriptions/json/availability/${uuid}`); - } else { - reject(new Error("Not on the Wizzair Multipass page")); - } + chrome.tabs.sendMessage(currentTab.id, {action: "getDynamicUrl"}, function(response) { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else if (response && response.dynamicUrl) { + resolve(response.dynamicUrl); + } else if (response && response.error) { + reject(new Error(response.error)); + } else { + reject(new Error("Failed to get dynamic URL")); + } + }); }); }); } async function checkRoute(origin, destination, date) { - const dynamicUrl = await getDynamicUrl(); - - const headers = await new Promise((resolve) => { - chrome.runtime.sendMessage({action: "getHeaders"}, (response) => { - resolve(response.headers); + try { + const dynamicUrl = await getDynamicUrl(); + + const data = { + flightType: 'OW', + origin: origin, + destination: destination, + departure: date, + arrival: '', + intervalSubtype: null + }; + + const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); + + const response = await new Promise((resolve) => { + chrome.tabs.sendMessage(tab.id, {action: "getHeaders"}, resolve); }); - }); - const data = { - flightType: 'OW', - origin: origin, - destination: destination, - departure: date, - arrival: '', - intervalSubtype: null - }; - - const response = await fetch(dynamicUrl, { - method: 'POST', - headers: headers, - body: JSON.stringify(data) - }); - return response.json(); + if (!response || !response.headers) { + throw new Error("Failed to get headers from the page"); + } + + const headers = response.headers; + + headers['Content-Type'] = 'application/json'; + + const fetchResponse = await fetch(dynamicUrl, { + method: 'POST', + headers: headers, + body: JSON.stringify(data) + }); + + if (!fetchResponse.ok) { + throw new Error(`HTTP error! status: ${fetchResponse.status}`); + } + + return fetchResponse.json(); + } catch (error) { + console.error('Error in checkRoute:', error); + throw error; + } } async function checkAllRoutes() { @@ -86,7 +108,8 @@ async function checkAllRoutes() { } try { - const destinations = await fetchDestinations(origin); + // const destinations = await fetchDestinations(origin); + const destinations = ['DXB', 'ATH']; console.log('Fetched destinations:', destinations); const progressElement = document.createElement('div'); @@ -94,38 +117,49 @@ async function checkAllRoutes() { progressElement.style.marginBottom = '10px'; routeListElement.insertBefore(progressElement, routeListElement.firstChild); - const results = await Promise.all(destinations.map(async (destination, index) => { + const results = []; + let allRoutesErrored = true; + + for (let i = 0; i < destinations.length; i++) { + const destination = destinations[i]; try { - progressElement.textContent = `Checking ${destinations.length} routes, please wait...`; + progressElement.textContent = `Checking route ${i + 1} of ${destinations.length}, please wait...`; const result = await checkRoute(origin, destination, selectedDate); if (result && result.flightsOutbound && result.flightsOutbound.length > 0) { const flight = result.flightsOutbound[0]; - return { + results.push({ route: `${origin} (${flight.departureStationText}) to ${destination} (${flight.arrivalStationText})`, date: flight.departureDate, departure: `${flight.departure} (${flight.departureOffsetText})`, arrival: `${flight.arrival} (${flight.arrivalOffsetText})`, duration: flight.duration - }; + }); + allRoutesErrored = false; } - return null; } catch (error) { console.error(`Error processing ${origin} to ${destination} on ${selectedDate}:`, error.message); - return null; } - })); + + if (i < destinations.length - 1) { + await new Promise(resolve => setTimeout(resolve, 800)); + } + } progressElement.remove(); - results.filter(result => result !== null).forEach(flightInfo => { - if (!flightsByDate[selectedDate]) { - flightsByDate[selectedDate] = []; - } - flightsByDate[selectedDate].push(flightInfo); - }); + if (allRoutesErrored) { + routeListElement.innerHTML = `

No flights available for ${selectedDate}.

`; + } else { + results.filter(result => result !== null).forEach(flightInfo => { + if (!flightsByDate[selectedDate]) { + flightsByDate[selectedDate] = []; + } + flightsByDate[selectedDate].push(flightInfo); + }); - displayResults(flightsByDate); + displayResults(flightsByDate); + } } catch (error) { console.error("An error occurred:", error.message); routeListElement.innerHTML = `

Error: ${error.message}

`; From 8f55e3db5fef94cd5b3af0bf579143598dffde9a Mon Sep 17 00:00:00 2001 From: Alexandru Date: Sat, 28 Sep 2024 18:39:59 +0300 Subject: [PATCH 2/3] Improve destinations find --- content.js | 77 +++++++++++++++--------------------------------- popup.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 66 deletions(-) diff --git a/content.js b/content.js index 9d2a416..396e9a4 100644 --- a/content.js +++ b/content.js @@ -1,64 +1,33 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.action === "getDestinations") { - const origin = request.origin; - - // Find the input fields. - const firstInputField = document.querySelector('input[aria-owns="autocomplete-result-list-1"]'); - const secondInputField = document.querySelector('input[aria-owns="autocomplete-result-list-2"]'); + setTimeout(() => { + const routePattern = /"routes":\[(.*?)\].*?"isOneWayFlightsOnly"/gms; + const pageContent = document.head.innerHTML; + const match = pageContent.match(routePattern); + if (match && match[0]) { + try { + const routesJson = `{"routes":${match[0].split('"routes":')[1].split(',"isOneWayFlightsOnly"')[0]}}`; + const routesData = JSON.parse(routesJson); - const firstList = document.querySelector('ul#autocomplete-result-list-1'); - const secondList = document.querySelector('ul#autocomplete-result-list-2'); + const originAirport = request.origin; + const routesFromOrigin = routesData.routes.find(route => route.departureStation.id === originAirport); - // Clear both input fields. - [firstInputField, secondInputField].forEach((inputField, index) => { - if (inputField) { - const clearButton = inputField.parentElement.querySelector('button.CvoClose'); - if (clearButton) { - clearButton.click(); - } else { - console.error(`Clear button not found for input field ${index + 1}`); + if (routesFromOrigin && routesFromOrigin.arrivalStations) { + const destinationIds = routesFromOrigin.arrivalStations.map(station => station.id); + console.log(`Routes from ${originAirport}:`, destinationIds); + sendResponse({ success: true, destinations: destinationIds }); + } else { + console.log(`No routes found from ${originAirport}`); + sendResponse({ success: false, error: `No routes found from ${originAirport}` }); + } + } catch (error) { + console.error("Error parsing routes data:", error); + sendResponse({ success: false, error: "Failed to parse routes data" }); } } else { - console.error(`Input field ${index + 1} not found`); + sendResponse({ success: false, error: "No routes data found" }); } - }); - - if (firstInputField && secondInputField) { - firstInputField.focus(); - firstInputField.value = origin; - firstInputField.dispatchEvent(new Event('input', { bubbles: true })); - - setTimeout(() => { - firstInputField.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); - setTimeout(() => { - firstInputField.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); - - - setTimeout(() => { - secondInputField.focus(); - secondInputField.click(); - secondInputField.dispatchEvent(new Event('input', { bubbles: true })); - - setTimeout(() => { - if (secondList) { - const destinations = Array.from(secondList.querySelectorAll('li')) - .map(li => { - const text = li.textContent.trim(); - const match = text.match(/\(([A-Z]{3})\)/); - return match ? match[1] : text; - }); - sendResponse({ destinations: destinations }); - } else { - sendResponse({ error: 'Destination list not found' }); - } - }, 500); - }, 500); - }, 100); - }, 500); - } else { - sendResponse({ error: 'Input fields not found' }); - } - + }, 1000); return true; } }); diff --git a/popup.js b/popup.js index e86942d..3b12a8d 100644 --- a/popup.js +++ b/popup.js @@ -5,10 +5,7 @@ async function fetchDestinations(origin) { chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { const currentTab = tabs[0]; if (currentTab.url.includes('multipass.wizzair.com')) { - chrome.tabs.sendMessage(currentTab.id, {action: "getDestinations", origin: origin}, function(response) { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } else if (response && response.destinations) { + chrome.tabs.sendMessage(currentTab.id, {action: "getDestinations", origin: origin}, function(response) {if (response && response.destinations) { resolve(response.destinations); } else if (response && response.error) { reject(new Error(response.error)); @@ -87,6 +84,34 @@ async function checkRoute(origin, destination, date) { } } +function cacheKey(origin, date) { + return `${origin}-${date}`; +} + +function setCachedResults(key, results) { + const cacheData = { + results: results, + timestamp: Date.now() + }; + localStorage.setItem(key, JSON.stringify(cacheData)); +} + +function getCachedResults(key) { + const cachedData = localStorage.getItem(key); + if (cachedData) { + const { results, timestamp } = JSON.parse(cachedData); + const thirtyMinutesInMs = 30 * 60 * 1000; + if (Date.now() - timestamp < thirtyMinutesInMs) { + return results; + } + } + return null; +} + +function clearCache(key) { + localStorage.removeItem(key); +} + async function checkAllRoutes() { console.log('checkAllRoutes started'); const originInput = document.getElementById('airport-input'); @@ -99,6 +124,24 @@ async function checkAllRoutes() { return; } + const cacheKey = `${origin}-${selectedDate}`; + const cachedResults = getCachedResults(cacheKey); + + if (cachedResults) { + console.log('Using cached results'); + displayResults({ [selectedDate]: cachedResults }); + const routeListElement = document.querySelector('.route-list'); + const cacheNotification = document.createElement('div'); + cacheNotification.textContent = 'Using cached results. Click the "Refresh Cache" button to fetch new data.'; + cacheNotification.style.backgroundColor = '#e6f7ff'; + cacheNotification.style.border = '1px solid #91d5ff'; + cacheNotification.style.borderRadius = '4px'; + cacheNotification.style.padding = '10px'; + cacheNotification.style.marginBottom = '15px'; + routeListElement.insertBefore(cacheNotification, routeListElement.firstChild); + return; + } + const flightsByDate = {}; const routeListElement = document.querySelector('.route-list'); @@ -108,8 +151,7 @@ async function checkAllRoutes() { } try { - // const destinations = await fetchDestinations(origin); - const destinations = ['DXB', 'ATH']; + const destinations = await fetchDestinations(origin); console.log('Fetched destinations:', destinations); const progressElement = document.createElement('div'); @@ -140,10 +182,6 @@ async function checkAllRoutes() { } catch (error) { console.error(`Error processing ${origin} to ${destination} on ${selectedDate}:`, error.message); } - - if (i < destinations.length - 1) { - await new Promise(resolve => setTimeout(resolve, 800)); - } } progressElement.remove(); @@ -158,6 +196,7 @@ async function checkAllRoutes() { flightsByDate[selectedDate].push(flightInfo); }); + setCachedResults(cacheKey, flightsByDate[selectedDate]); displayResults(flightsByDate); } } catch (error) { @@ -197,10 +236,33 @@ function displayResults(flightsByDate) { year: 'numeric' }); - dateHeader.textContent = formattedDate; + dateHeader.style.display = 'flex'; + dateHeader.style.justifyContent = 'space-between'; + dateHeader.style.alignItems = 'center'; dateHeader.style.backgroundColor = '#f0f0f0'; dateHeader.style.padding = '10px'; dateHeader.style.borderRadius = '5px'; + + const dateText = document.createElement('span'); + dateText.textContent = formattedDate; + dateHeader.appendChild(dateText); + + const clearCacheButton = document.createElement('button'); + clearCacheButton.textContent = '♻️ Refresh Cache'; + clearCacheButton.style.padding = '5px 10px'; + + clearCacheButton.style.fontSize = '12px'; + clearCacheButton.style.backgroundColor = '#f0f0f0'; + clearCacheButton.style.border = '1px solid #ccc'; + clearCacheButton.style.borderRadius = '3px'; + clearCacheButton.style.cursor = 'pointer'; + clearCacheButton.addEventListener('click', () => { + const origin = document.getElementById('airport-input').value.toUpperCase(); + const cacheKey = `${origin}-${date}`; + clearCache(cacheKey); + }); + + dateHeader.appendChild(clearCacheButton); resultsDiv.appendChild(dateHeader); const flightList = document.createElement('ul'); @@ -294,4 +356,4 @@ document.addEventListener('DOMContentLoaded', function() { dateSelect.appendChild(option); } -}); +}); \ No newline at end of file From bc3ba98c19d5fbfc00e340d1d08a1a8feb6cb7c1 Mon Sep 17 00:00:00 2001 From: Alexandru Date: Sat, 28 Sep 2024 22:13:49 +0300 Subject: [PATCH 3/3] Add back concurrency --- popup.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/popup.js b/popup.js index 3b12a8d..ed1fee7 100644 --- a/popup.js +++ b/popup.js @@ -42,6 +42,8 @@ function getDynamicUrl() { async function checkRoute(origin, destination, date) { try { + await new Promise(resolve => setTimeout(resolve, 200)); + const dynamicUrl = await getDynamicUrl(); const data = { @@ -160,13 +162,14 @@ async function checkAllRoutes() { routeListElement.insertBefore(progressElement, routeListElement.firstChild); const results = []; - let allRoutesErrored = true; + let completedRoutes = 0; - for (let i = 0; i < destinations.length; i++) { - const destination = destinations[i]; - try { - progressElement.textContent = `Checking route ${i + 1} of ${destinations.length}, please wait...`; + const updateProgress = () => { + progressElement.textContent = `Checked ${completedRoutes} of ${destinations.length} routes...`; + }; + const routePromises = destinations.map(async (destination) => { + try { const result = await checkRoute(origin, destination, selectedDate); if (result && result.flightsOutbound && result.flightsOutbound.length > 0) { const flight = result.flightsOutbound[0]; @@ -177,16 +180,20 @@ async function checkAllRoutes() { arrival: `${flight.arrival} (${flight.arrivalOffsetText})`, duration: flight.duration }); - allRoutesErrored = false; } } catch (error) { console.error(`Error processing ${origin} to ${destination} on ${selectedDate}:`, error.message); + } finally { + completedRoutes++; + updateProgress(); } - } + }); + + await Promise.all(routePromises); progressElement.remove(); - if (allRoutesErrored) { + if (results.length === 0) { routeListElement.innerHTML = `

No flights available for ${selectedDate}.

`; } else { results.filter(result => result !== null).forEach(flightInfo => {