feat(clients/weather): yandex weather integration
This commit is contained in:
parent
1499e46a0f
commit
9ade79ffa9
3 changed files with 238 additions and 0 deletions
|
|
@ -33,5 +33,8 @@
|
||||||
"typescript-eslint": "^8.47.0",
|
"typescript-eslint": "^8.47.0",
|
||||||
"vite": "^7.2.2",
|
"vite": "^7.2.2",
|
||||||
"vite-plugin-devtools-json": "^1.0.0"
|
"vite-plugin-devtools-json": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-cache": "^5.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -7,6 +7,10 @@ settings:
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
node-cache:
|
||||||
|
specifier: ^5.1.2
|
||||||
|
version: 5.1.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/compat':
|
'@eslint/compat':
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
|
|
@ -679,6 +683,10 @@ packages:
|
||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
clone@2.1.2:
|
||||||
|
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
|
||||||
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
clsx@2.1.1:
|
clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -1034,6 +1042,10 @@ packages:
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
node-cache@5.1.2:
|
||||||
|
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==}
|
||||||
|
engines: {node: '>= 8.0.0'}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
@ -1801,6 +1813,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
clone@2.1.2: {}
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
|
|
@ -2133,6 +2147,10 @@ snapshots:
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
node-cache@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
clone: 2.1.2
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
|
|
|
||||||
217
src/lib/clients/weather.ts
Normal file
217
src/lib/clients/weather.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
import NodeCache from "node-cache";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User agent. Used to request the API.
|
||||||
|
*/
|
||||||
|
const USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secret key for a API token.
|
||||||
|
*/
|
||||||
|
const TOKEN_SECRET = "gravisfrontem";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response cache.
|
||||||
|
*/
|
||||||
|
const cache = new NodeCache({
|
||||||
|
// NOTE: 10 minutes
|
||||||
|
stdTTL: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather response.
|
||||||
|
*/
|
||||||
|
export interface WeatherResponse {
|
||||||
|
/**
|
||||||
|
* Current Unix timestamp (seconds).
|
||||||
|
*/
|
||||||
|
now: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current time in ISO 8601 format.
|
||||||
|
*/
|
||||||
|
now_dt: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the requested location.
|
||||||
|
*/
|
||||||
|
info: {
|
||||||
|
/**
|
||||||
|
* Latitude of the location.
|
||||||
|
*/
|
||||||
|
lat: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longitude of the location.
|
||||||
|
*/
|
||||||
|
lon: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public URL to the Yandex.Weather page.
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current factual weather information.
|
||||||
|
*/
|
||||||
|
fact: {
|
||||||
|
/**
|
||||||
|
* Air temperature in °C.
|
||||||
|
*/
|
||||||
|
temp: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Feels like" temperature in °C.
|
||||||
|
*/
|
||||||
|
feels_like: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather condition string (e.g., "clear", "rain").
|
||||||
|
*/
|
||||||
|
condition: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon code used to build an icon URL.
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wind speed in m/s.
|
||||||
|
*/
|
||||||
|
wind_speed?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative humidity (%).
|
||||||
|
*/
|
||||||
|
humidity?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atmospheric pressure in mmHg.
|
||||||
|
*/
|
||||||
|
pressure_mm?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forecast data for the current date.
|
||||||
|
*/
|
||||||
|
forecast: {
|
||||||
|
/**
|
||||||
|
* Date of the forecast (YYYY-MM-DD).
|
||||||
|
*/
|
||||||
|
date: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unix timestamp for the forecast date.
|
||||||
|
*/
|
||||||
|
date_ts: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local sunrise time (HH:MM), if available.
|
||||||
|
*/
|
||||||
|
sunrise?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local sunset time (HH:MM), if available.
|
||||||
|
*/
|
||||||
|
sunset?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather forecast split by parts of the day.
|
||||||
|
*/
|
||||||
|
parts: Array<{
|
||||||
|
/**
|
||||||
|
* Name of the day part ("night", "morning", "day", "evening").
|
||||||
|
*/
|
||||||
|
part_name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum temperature for this part of day (°C).
|
||||||
|
*/
|
||||||
|
temp_min?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum temperature for this part of day (°C).
|
||||||
|
*/
|
||||||
|
temp_max?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather condition string for this period.
|
||||||
|
*/
|
||||||
|
condition?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon code for this part of the day.
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wind speed in m/s.
|
||||||
|
*/
|
||||||
|
wind_speed?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative humidity (%).
|
||||||
|
*/
|
||||||
|
humidity?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precipitation amount in mm.
|
||||||
|
*/
|
||||||
|
prec_mm?: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAuthorizationHeaders() {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
return {
|
||||||
|
// NOTE: no one will pay ~$7k/year to have a weather on their personal website
|
||||||
|
'x-yandex-weather-client': 'YandexWeatherFront2',
|
||||||
|
'x-yandex-weather-token':
|
||||||
|
crypto.createHash('md5').update(`${TOKEN_SECRET}${timestamp}`).digest('hex'),
|
||||||
|
'x-yandex-weather-timestamp': timestamp.toString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the weather information.
|
||||||
|
* @param latitude Latitude.
|
||||||
|
* @param longitude Longitude.
|
||||||
|
* @returns Weather data.
|
||||||
|
*/
|
||||||
|
export default async function fetchWeather(latitude: number, longitude: number) {
|
||||||
|
const key = `${latitude};${longitude}`;
|
||||||
|
|
||||||
|
// NOTE: Get the cached response first
|
||||||
|
let data = cache.get<WeatherResponse>(key);
|
||||||
|
if (data !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Request the weather from Yandex
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.weather.yandex.net/v2/informers?lat=${latitude}&lon=${longitude}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'user-agent': USER_AGENT,
|
||||||
|
...generateAuthorizationHeaders(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// NOTE: Handle the error if it did happen
|
||||||
|
if (!response.ok) {
|
||||||
|
const { message }: { message: string } = await response.json();
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Cache the response and return it
|
||||||
|
data = await response.json();
|
||||||
|
cache.set<WeatherResponse>(key, data!);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue