From 269277770734694c9bee7ea8e194803348f391b8 Mon Sep 17 00:00:00 2001 From: lingdianshiren <88759938+lingdiansr@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:42:01 +0800 Subject: [PATCH] fix(weather): robust location resolution with parallel fetch and multi-tier fallback (#2638) Decouple weather data fetching from reverse geocoding so that weather loads as soon as coordinates are available, even when Nominatim is unreachable (e.g. mainland China). - Fetch Open-Meteo weather immediately once lat/lon are known. - Resolve city name in parallel via Nominatim -> Photon -> BigDataCloud. - If all reverse geocoding fails, keep displaying weather with a placeholder city name. - Skip reverse geocoding entirely when the user has configured a city name. - Fall back to ip-api.com when GeoClue2 is unavailable or returns zero coordinates. - Add request-generation tracking to discard stale geocoding responses after location changes. - Hold explicit WeatherService refs in bar widget and dashboard tab. Co-authored-by: lingdiansr <2077258365@qq.com> --- .../Modules/DankBar/Widgets/Weather.qml | 5 +- quickshell/Modules/DankDash/WeatherTab.qml | 3 + quickshell/Services/WeatherService.qml | 282 ++++++++++++++++-- 3 files changed, 261 insertions(+), 29 deletions(-) diff --git a/quickshell/Modules/DankBar/Widgets/Weather.qml b/quickshell/Modules/DankBar/Widgets/Weather.qml index efd8893e..a64a7d8d 100644 --- a/quickshell/Modules/DankBar/Widgets/Weather.qml +++ b/quickshell/Modules/DankBar/Widgets/Weather.qml @@ -9,9 +9,8 @@ BasePill { visible: SettingsData.weatherEnabled - Ref { - service: WeatherService - } + Component.onCompleted: WeatherService.addRef() + Component.onDestruction: WeatherService.removeRef() content: Component { Item { diff --git a/quickshell/Modules/DankDash/WeatherTab.qml b/quickshell/Modules/DankDash/WeatherTab.qml index 6d09c4f7..9fd64a73 100644 --- a/quickshell/Modules/DankDash/WeatherTab.qml +++ b/quickshell/Modules/DankDash/WeatherTab.qml @@ -18,6 +18,9 @@ Item { property bool showHourly: false property bool available: WeatherService.weather.available + Component.onCompleted: WeatherService.addRef() + Component.onDestruction: WeatherService.removeRef() + function syncFrom(type) { if (!dailyLoader.item || !hourlyLoader.item) return; diff --git a/quickshell/Services/WeatherService.qml b/quickshell/Services/WeatherService.qml index 134789ea..d3753d8f 100644 --- a/quickshell/Services/WeatherService.qml +++ b/quickshell/Services/WeatherService.qml @@ -43,6 +43,8 @@ Singleton { property int lastFetchTime: 0 property int minFetchInterval: 30000 property int persistentRetryCount: 0 + property int _geocodeReqId: 0 + property var _pendingCoords: null readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"] readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"] @@ -452,16 +454,54 @@ Singleton { if (!location) { return null; } - - const params = ["latitude=" + location.latitude, "longitude=" + location.longitude, "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m", "daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max", "hourly=temperature_2m,weather_code,precipitation_probability,wind_speed_10m,apparent_temperature,relative_humidity_2m,surface_pressure,visibility,cloud_cover", "timezone=auto", "forecast_days=7"]; - - return "https://api.open-meteo.com/v1/forecast?" + params.join('&'); + return getWeatherApiUrlForCoords(location.latitude, location.longitude); } function getGeocodingUrl(query) { return "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(query) + "&count=1&language=en&format=json"; } + function getConfiguredLocationName() { + return SessionData.isGreeterMode ? GreetdSettings.weatherLocation : SettingsData.weatherLocation; + } + + function setLocation(lat, lon, city, country) { + root.location = { + city: city || I18n.tr("Local Weather"), + country: country || "", + latitude: lat, + longitude: lon + }; + } + + function updateLocationCity(city, country) { + if (!root.location) + return; + + root.location = { + latitude: root.location.latitude, + longitude: root.location.longitude, + city: city || root.location.city, + country: country || root.location.country + }; + + if (root.weather.available) { + root.weather = Object.assign({}, root.weather, { + city: city || root.weather.city, + country: country || root.weather.country + }); + } + } + + function getWeatherApiUrlForCoords(lat, lon) { + if (lat == null || lon == null) + return null; + + const params = ["latitude=" + lat, "longitude=" + lon, "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m", "daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max", "hourly=temperature_2m,weather_code,precipitation_probability,wind_speed_10m,apparent_temperature,relative_humidity_2m,surface_pressure,visibility,cloud_cover", "timezone=auto", "forecast_days=7"]; + + return "https://api.open-meteo.com/v1/forecast?" + params.join('&'); + } + function addRef() { refCount++; @@ -490,20 +530,30 @@ Singleton { const lat = parseFloat(parts[0]); const lon = parseFloat(parts[1]); if (!isNaN(lat) && !isNaN(lon)) { - getLocationFromCoords(lat, lon); + if (cityName) { + // User provided both: trust the configured name and coordinates, skip geocoding + setLocation(lat, lon, cityName, ""); + fetchWeather(lat, lon); + } else { + getLocationFromCoords(lat, lon); + } return; } } } - if (cityName) + if (cityName) { getLocationFromCity(cityName); + } else { + root.handleWeatherFailure(); + } } function getLocationFromCoords(lat, lon) { - const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en"; - reverseGeocodeFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat(["-H", "User-Agent: DankMaterialShell Weather Widget", url]); - reverseGeocodeFetcher.running = true; + // Use coordinates immediately for weather; resolve city name in parallel with fallbacks + setLocation(lat, lon, I18n.tr("Local Weather"), ""); + fetchWeather(lat, lon); + resolveCityName(lat, lon); } function getLocationFromCity(city) { @@ -512,19 +562,78 @@ Singleton { } function getLocationFromService() { - if (!LocationService.valid) + if (!LocationService.valid) { + getLocationFromIP(); return; - getLocationFromCoords(LocationService.latitude, LocationService.longitude); + } + + const lat = LocationService.latitude; + const lon = LocationService.longitude; + + if (lat === 0 && lon === 0) { + getLocationFromIP(); + return; + } + + getLocationFromCoords(lat, lon); } - function fetchWeather() { + function getLocationFromIP() { + ipLocationFetcher.running = true; + } + + function resolveCityName(lat, lon) { + // Cancel any in-flight city resolution to avoid stale updates + if (nominatimFetcher.running) + nominatimFetcher.running = false; + if (photonFetcher.running) + photonFetcher.running = false; + if (bigDataCloudFetcher.running) + bigDataCloudFetcher.running = false; + + root._geocodeReqId++; + root._pendingCoords = { + latitude: lat, + longitude: lon, + reqId: root._geocodeReqId + }; + + tryNominatim(lat, lon, root._geocodeReqId); + } + + function tryNominatim(lat, lon, reqId) { + const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en"; + nominatimFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat(["-H", "User-Agent: DankMaterialShell Weather Widget", url]); + nominatimFetcher.reqId = reqId; + nominatimFetcher.running = true; + } + + function tryPhoton(lat, lon, reqId) { + const url = "https://photon.komoot.io/reverse?lat=" + lat + "&lon=" + lon + "&lang=en"; + photonFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([url]); + photonFetcher.reqId = reqId; + photonFetcher.running = true; + } + + function tryBigDataCloud(lat, lon, reqId) { + const url = "https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=" + lat + "&longitude=" + lon + "&localityLanguage=zh"; + bigDataCloudFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([url]); + bigDataCloudFetcher.reqId = reqId; + bigDataCloudFetcher.running = true; + } + + function fetchWeather(lat, lon) { if (root.refCount === 0 || !SettingsData.weatherEnabled) { return; } - if (!location) { - updateLocation(); - return; + if (lat == null || lon == null) { + if (!location) { + updateLocation(); + return; + } + lat = location.latitude; + lon = location.longitude; } if (weatherFetcher.running) { @@ -536,7 +645,7 @@ Singleton { return; } - const apiUrl = getWeatherApiUrl(); + const apiUrl = getWeatherApiUrlForCoords(lat, lon); if (!apiUrl) { return; } @@ -586,9 +695,123 @@ Singleton { } Process { - id: reverseGeocodeFetcher + id: nominatimFetcher + property int reqId: 0 running: false + stdout: StdioCollector { + onStreamFinished: { + if (nominatimFetcher.reqId !== root._geocodeReqId) + return; + + const raw = text.trim(); + if (!raw || raw[0] !== "{") { + root.tryPhoton(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId); + return; + } + + try { + const data = JSON.parse(raw); + const address = data.address || {}; + const city = address.hamlet || address.city || address.town || address.village || I18n.tr("Unknown"); + const country = address.country || I18n.tr("Unknown"); + root.updateLocationCity(city, country); + } catch (e) { + root.tryPhoton(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId); + } + } + } + + onExited: exitCode => { + if (nominatimFetcher.reqId !== root._geocodeReqId) + return; + if (exitCode !== 0) { + root.tryPhoton(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId); + } + } + } + + Process { + id: photonFetcher + property int reqId: 0 + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (photonFetcher.reqId !== root._geocodeReqId) + return; + + const raw = text.trim(); + if (!raw || raw[0] !== "{") { + root.tryBigDataCloud(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId); + return; + } + + try { + const data = JSON.parse(raw); + const features = data.features; + if (!features || features.length === 0) { + throw new Error("No Photon results"); + } + + const props = features[0].properties || {}; + const city = props.city || props.town || props.village || props.locality || props.name || I18n.tr("Unknown"); + const country = props.country || I18n.tr("Unknown"); + root.updateLocationCity(city, country); + } catch (e) { + root.tryBigDataCloud(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId); + } + } + } + + onExited: exitCode => { + if (photonFetcher.reqId !== root._geocodeReqId) + return; + if (exitCode !== 0) { + root.tryBigDataCloud(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId); + } + } + } + + Process { + id: bigDataCloudFetcher + property int reqId: 0 + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (bigDataCloudFetcher.reqId !== root._geocodeReqId) + return; + + const raw = text.trim(); + if (!raw || raw[0] !== "{") { + // All city resolution fallbacks failed; weather is already displayed + return; + } + + try { + const data = JSON.parse(raw); + const city = data.city || data.locality || I18n.tr("Unknown"); + const country = data.countryName || I18n.tr("Unknown"); + root.updateLocationCity(city, country); + } catch (e) { + // All fallbacks failed; keep placeholder city name + } + } + } + + onExited: exitCode => { + if (bigDataCloudFetcher.reqId !== root._geocodeReqId) + return; + // Final fallback; no further action needed + } + } + + Process { + id: ipLocationFetcher + running: false + command: lowPriorityCmd.concat(curlBaseCmd).concat(["http://ip-api.com/json/"]) + stdout: StdioCollector { onStreamFinished: { const raw = text.trim(); @@ -599,16 +822,21 @@ Singleton { try { const data = JSON.parse(raw); - const address = data.address || {}; - root.location = { - city: address.hamlet || address.city || address.town || address.village || I18n.tr("Unknown"), - country: address.country || I18n.tr("Unknown"), - latitude: parseFloat(data.lat), - longitude: parseFloat(data.lon) - }; + if (data.status === "fail") { + throw new Error("IP location lookup failed"); + } - fetchWeather(); + const lat = parseFloat(data.lat); + const lon = parseFloat(data.lon); + const city = data.city; + + if (!city || isNaN(lat) || isNaN(lon)) { + throw new Error("Missing or invalid location data"); + } + + setLocation(lat, lon, city, data.countryName || ""); + fetchWeather(lat, lon); } catch (e) { root.handleWeatherFailure(); } @@ -833,8 +1061,10 @@ Singleton { function onLocationChanged(data) { if (!SettingsData.useAutoLocation) return; - if (data.latitude === 0 && data.longitude === 0) + if (data.latitude === 0 && data.longitude === 0) { + root.getLocationFromIP(); return; + } root.getLocationFromCoords(data.latitude, data.longitude); } }