aemet-api
Version:
Cliente TypeScript para la API de AEMET (Agencia Estatal de Meteorología)
193 lines (192 loc) • 9.84 kB
JavaScript
/**
* Utilidades para la librería de AEMET
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchAemetData = fetchAemetData;
exports.getSkyStateDescription = getSkyStateDescription;
exports.formatDate = formatDate;
exports.getDayForecast = getDayForecast;
const axios_1 = __importDefault(require("axios"));
const constants_1 = require("./constants");
/**
* Realizar una petición GET a la API de AEMET y descargar los datos
* @param url - URL del endpoint de la API
* @param apiKey - Clave de API AEMET
* @param timeout - Timeout para la petición en milisegundos
* @returns - Datos procesados de la respuesta
*/
async function fetchAemetData(url, apiKey, timeout = 10000) {
try {
console.log(`Realizando petición a: ${url}`);
// Realizamos la petición inicial para obtener los URLs de datos y metadatos
const apiResponse = await axios_1.default.get(url, {
headers: {
'api_key': apiKey,
'accept': 'application/json'
},
timeout,
responseType: 'text', // Para manejar tanto JSON como texto plano
});
console.log('Respuesta recibida. Estado:', apiResponse.status);
console.log('Tipo de contenido:', apiResponse.headers['content-type']);
// Manejamos el caso especial de AEMET que puede devolver 200 con cuerpo vacío
if (apiResponse.status === 200 && (!apiResponse.data || (typeof apiResponse.data === 'string' && apiResponse.data.trim() === ""))) {
console.log('Respuesta vacía recibida con estado 200.');
// En lugar de simular una respuesta, lanzamos un error para manejar este caso
throw new Error('La API devolvió una respuesta vacía. Verifique que la URL y la API key sean correctas.');
}
// Si la respuesta es texto plano pero parece contener JSON, intentamos parsearlo
let responseData = apiResponse.data;
if (typeof apiResponse.data === 'string' &&
apiResponse.headers['content-type']?.includes('text/plain') &&
apiResponse.data.trim().startsWith('{')) {
try {
console.log('Intentando parsear respuesta de texto plano como JSON...');
responseData = JSON.parse(apiResponse.data);
console.log('Parseo exitoso, respuesta convertida a objeto JSON');
}
catch (parseError) {
console.error('Error al parsear respuesta como JSON:', parseError);
console.log('Contenido de la respuesta:', apiResponse.data);
}
}
responseData = JSON.parse(responseData);
// Verificar si es un objeto y mostrarlo para depuración
console.log('Tipo de respuesta:', typeof responseData);
console.log('Contenido de la respuesta:', responseData);
console.log('Contenido de la respuesta:', JSON.stringify(responseData));
// Valores climatológicos: AEMET devuelve estado = 0 como indicador de éxito
// El formato es: { "descripcion": "exito", "estado": 200, "datos": URL, "metadatos": URL }
if (responseData) {
// Para el caso específico donde la descripción sea "Éxito" y estado sea 0
// esto corresponde a los valores climatológicos con periodicidad diaria
if (responseData.estado === 200 && responseData.descripcion === "exito") {
console.log("Respuesta exitosa de valores climatológicos con estado 0");
}
const dataUrl = responseData.datos;
console.log('URL de datos:', dataUrl);
if (!dataUrl) {
throw new Error('URL de datos no disponible en la respuesta de la API');
}
// Si datos es una URL, la descargamos
if (typeof dataUrl === 'string' && (dataUrl.startsWith('http://') || dataUrl.startsWith('https://'))) {
console.log(`Descargando datos desde: ${dataUrl}`);
try {
const response = await axios_1.default.get(dataUrl, {
timeout,
headers: {
'accept': 'application/json'
},
responseType: 'text' // Para manejar tanto JSON como texto plano
});
console.log('Datos obtenidos correctamente. Tipo de contenido:', response.headers['content-type']);
// Intentar parsear si es texto plano pero parece ser JSON
let data = response.data;
if (typeof response.data === 'string' &&
response.headers['content-type']?.includes('text/plain') &&
(response.data.trim().startsWith('[') || response.data.trim().startsWith('{'))) {
try {
console.log('Intentando parsear datos de texto plano como JSON...');
data = JSON.parse(response.data);
console.log('Parseo exitoso, datos convertidos a objeto JSON');
}
catch (parseError) {
console.error('Error al parsear datos como JSON:', parseError);
}
}
console.log('Datos obtenidos:', data);
return data;
}
catch (dataError) {
console.error('Error al obtener los datos:', dataError);
if (axios_1.default.isAxiosError(dataError) && dataError.response) {
console.error('Detalles de error en datos:', dataError.response.status, JSON.stringify(dataError.response.data));
}
throw new Error(`Error al acceder a la URL de datos: ${dataError instanceof Error ? dataError.message : 'Error desconocido'}`);
}
}
else {
// Si datos no es una URL sino directamente la información
console.log('El campo "datos" contiene directamente la información, no una URL');
return responseData; // Devolver el objeto completo con { "descripcion": "Éxito", "estado": 0, "datos": ..., "metadatos": ... }
}
}
else {
console.error('Error en la respuesta:', JSON.stringify(responseData));
throw new Error(`Error en la respuesta de la API: ${responseData?.descripcion || 'Sin descripción'} (Estado: ${responseData?.estado || apiResponse.status})`);
}
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
console.error('Error de Axios:', error.message);
if (error.response) {
console.error('Respuesta de error:', error.response.status, JSON.stringify(error.response.data));
// Verificar si la respuesta es un HTML (error de Tomcat)
const contentType = error.response.headers['content-type'];
if (contentType && contentType.includes('text/html')) {
// En caso de respuesta HTML, proporcionar un mensaje más claro
if (error.response.status === 404) {
throw new Error(`Recurso no encontrado (404). Verifique que los parámetros sean correctos y que no esté solicitando datos futuros.`);
}
else {
throw new Error(`Error del servidor (${error.response.status}). Intente más tarde.`);
}
}
else {
throw new Error(`Error en la petición a la API: ${error.response.status} - ${JSON.stringify(error.response.data) || 'Sin datos'}`);
}
}
else if (error.request) {
console.error('No se recibió respuesta');
throw new Error(`No se recibió respuesta de la API: ${error.message}`);
}
else {
console.error('Error al configurar la petición');
throw new Error(`Error al configurar la petición a la API: ${error.message}`);
}
}
console.error('Error desconocido:', error);
throw error instanceof Error ? error : new Error('Error desconocido en la petición a la API');
}
}
/**
* Obtener descripción del estado del cielo a partir de su código
* @param code - Código del estado del cielo
* @returns - Descripción del estado del cielo
*/
function getSkyStateDescription(code) {
return constants_1.SKY_STATES[code] || 'Desconocido';
}
/**
* Formatea una fecha como YYYY-MM-DD
* @param date - Fecha a formatear
* @returns - Fecha formateada
*/
function formatDate(date) {
return date.toISOString().split('T')[0];
}
/**
* Extrae la predicción para un día específico
* @param forecast - Datos completos de la predicción
* @param date - Fecha para la que se quiere la predicción (o 0 para hoy, 1 para mañana, 2 para pasado mañana)
* @returns - Predicción para el día especificado
*/
function getDayForecast(forecast, date) {
if (typeof date === 'number') {
const today = new Date();
today.setDate(today.getDate() + date);
date = today;
}
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new Error('Fecha inválida');
}
const dateStr = formatDate(date);
if (!forecast || !Array.isArray(forecast)) {
throw new Error('El forecast no es un array válido');
}
return forecast.find((day) => day && day.fecha === dateStr);
}
;