UNPKG

aemet-api

Version:

Cliente TypeScript para la API de AEMET (Agencia Estatal de Meteorología)

193 lines (192 loc) 9.84 kB
"use strict"; /** * 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); }