2022-08-29 06:44:58 +02:00
|
|
|
require('dotenv').config();
|
|
|
|
const debug = require('debug')('soundbirder:api:quizz');
|
|
|
|
const debugResponses = require('debug')('soundbirder:api:responses');
|
|
|
|
const cache = require('./cache');
|
|
|
|
const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY);
|
|
|
|
const XenoCanto = require('@unclesamulus/xeno-canto-api');
|
|
|
|
const { choices, choice } = require('../utils/choices');
|
2024-01-08 19:41:57 +01:00
|
|
|
|
2024-02-12 09:29:34 +01:00
|
|
|
function questionKey(region, locale, size) {
|
2024-01-08 19:41:57 +01:00
|
|
|
return `${region}:${locale}#${size}`;
|
|
|
|
}
|
|
|
|
|
2024-02-12 09:29:34 +01:00
|
|
|
async function getQuestionCached(region, locale, size) {
|
|
|
|
let key = questionKey(region, locale, size);
|
2024-01-08 19:41:57 +01:00
|
|
|
let quizz = await cache.pop(key);
|
|
|
|
if (!quizz) {
|
2024-02-13 19:01:36 +01:00
|
|
|
quizz = await generateQuestion(region, locale, size);
|
2024-01-08 19:41:57 +01:00
|
|
|
cache.push(key, quizz);
|
|
|
|
}
|
|
|
|
return quizz;
|
|
|
|
}
|
|
|
|
|
2024-02-12 09:29:34 +01:00
|
|
|
async function cacheQuestion(region, locale, size) {
|
|
|
|
let quizz = await generateQuestion(region, locale, size);
|
|
|
|
let key = questionKey(region, locale, size);
|
2024-01-08 19:41:57 +01:00
|
|
|
cache.push(key, quizz);
|
2024-01-08 20:06:42 +01:00
|
|
|
return quizz;
|
2024-01-08 19:41:57 +01:00
|
|
|
}
|
2022-08-29 06:44:58 +02:00
|
|
|
|
2024-02-12 09:29:34 +01:00
|
|
|
async function generateQuestion(region, locale, nb_propositions) {
|
2022-08-29 06:44:58 +02:00
|
|
|
const quizz = {}
|
|
|
|
try {
|
2024-01-08 19:41:57 +01:00
|
|
|
let speciesSelection = [];
|
|
|
|
do {
|
2024-02-12 09:29:34 +01:00
|
|
|
speciesSelection = await getSpeciesSelection(region, nb_propositions);
|
2024-01-08 19:41:57 +01:00
|
|
|
} while (speciesSelection == []);
|
2022-08-29 06:44:58 +02:00
|
|
|
debugResponses(`Species selection: ${speciesSelection}`);
|
|
|
|
const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale);
|
2024-01-08 19:41:57 +01:00
|
|
|
if (!speciesSelectionLocalized) {
|
|
|
|
throw 'Not localized';
|
|
|
|
}
|
2022-08-29 06:44:58 +02:00
|
|
|
debugResponses('Localized species selection:', speciesSelectionLocalized);
|
|
|
|
quizz.species = speciesSelectionLocalized;
|
|
|
|
debug("Got species selection", quizz.species);
|
2022-08-29 10:38:42 +02:00
|
|
|
let answer;
|
|
|
|
do {
|
|
|
|
answer = choice(speciesSelectionLocalized);
|
|
|
|
quizz.answer = answer;
|
|
|
|
quizz.audio = await getAudio(answer.sciName);
|
2022-08-29 14:05:34 +02:00
|
|
|
if (quizz.audio === undefined) {
|
2022-08-29 10:38:42 +02:00
|
|
|
debug("No audio found for species", answer.sciName);
|
|
|
|
debug("Trying again...");
|
|
|
|
}
|
2022-08-29 14:05:34 +02:00
|
|
|
} while (quizz.audio === undefined);
|
2022-08-29 06:44:58 +02:00
|
|
|
debug("Got answer", answer);
|
|
|
|
debug("Got audio", quizz.audio);
|
|
|
|
} catch (error) {
|
|
|
|
debug("Error raised while generating quizz");
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
return quizz;
|
|
|
|
}
|
|
|
|
|
2024-02-12 09:29:34 +01:00
|
|
|
async function generateQuizz(region, locale, nb_questions) {
|
|
|
|
let questions = [];
|
|
|
|
let answers = [];
|
2024-02-13 19:01:36 +01:00
|
|
|
let start = Date.now();
|
|
|
|
let species_in_region_localized = await getSpeciesNamesInRegion(region, locale);
|
2024-02-12 09:29:34 +01:00
|
|
|
for (let i=0; i < nb_questions; i++) {
|
|
|
|
try {
|
2024-02-13 19:01:36 +01:00
|
|
|
let question_species = choice(species_in_region_localized);
|
2024-02-12 09:29:34 +01:00
|
|
|
let question_audio = await getAudio(question_species.sciName);
|
|
|
|
questions.push(question_audio);
|
|
|
|
answers.push(question_species);
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
2024-02-13 19:01:36 +01:00
|
|
|
const millis = Date.now() - start;
|
|
|
|
console.log(`seconds elapsed = ${Math.floor(millis / 1000)}`);
|
2024-02-12 09:29:34 +01:00
|
|
|
return {questions: questions, answers: answers, propositions: species_in_region_localized};
|
|
|
|
}
|
|
|
|
|
2024-02-13 19:01:36 +01:00
|
|
|
async function getSpeciesNamesInRegion(region, locale) {
|
|
|
|
const cacheKey = `region-sppnames-${locale}-${region}`;
|
|
|
|
let cached = await cache.getCached(cacheKey);
|
|
|
|
if (cached && cached != []) {
|
|
|
|
return cached;
|
|
|
|
} else {
|
|
|
|
const species_in_region = await getSpeciesList(region);
|
|
|
|
const species_in_region_localized = await getLocalizedNames(species_in_region, locale);
|
|
|
|
cache.cacheResponse(cacheKey, species_in_region_localized);
|
|
|
|
return species_in_region_localized;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-25 23:15:26 +01:00
|
|
|
async function getSpeciesSelection(region, number) {
|
|
|
|
const regionCode = region;
|
2022-08-29 06:44:58 +02:00
|
|
|
const speciesList = await getSpeciesList(regionCode);
|
|
|
|
const speciesSelection = choices(speciesList, number);
|
|
|
|
return speciesSelection;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getLocalizedNames(speciesCodes, locale) {
|
2024-02-04 07:07:37 +01:00
|
|
|
const localizedNames = [];
|
|
|
|
for (const speciesCode of speciesCodes) {
|
|
|
|
const localized = await getLocalizedName(speciesCode, locale);
|
2024-02-12 09:29:34 +01:00
|
|
|
if (!localized.sciName.includes(" x "))
|
|
|
|
localizedNames.push(localized);
|
2022-08-29 06:44:58 +02:00
|
|
|
}
|
2024-02-04 07:07:37 +01:00
|
|
|
return localizedNames;
|
2022-08-29 06:44:58 +02:00
|
|
|
}
|
|
|
|
|
2024-02-13 19:01:36 +01:00
|
|
|
async function getLocalizedName(speciesCode, locale) {
|
|
|
|
const cacheKey = `sppnames-${locale}-${speciesCode}`;
|
|
|
|
const cached = await cache.getCached(cacheKey);
|
|
|
|
if (cached) {
|
|
|
|
return cached;
|
|
|
|
} else {
|
|
|
|
return eBird.ref.taxonomy.ebird({
|
|
|
|
fmt: 'json',
|
|
|
|
locale: locale,
|
|
|
|
species: speciesCode
|
|
|
|
}).then(
|
|
|
|
response => {
|
|
|
|
const names = response[0];
|
|
|
|
cache.cacheResponse(cacheKey, names);
|
|
|
|
debug("Got localized species names");
|
|
|
|
debugResponses(names);
|
|
|
|
return names;
|
|
|
|
}
|
|
|
|
).catch(error => {
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
}
|
2022-08-29 06:44:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getSpeciesList(regionCode) {
|
|
|
|
const cached = await cache.getCached(`spplist-${regionCode}`);
|
|
|
|
if (cached) {
|
2024-01-08 23:02:04 +01:00
|
|
|
if (cached != []) {
|
|
|
|
return cached;
|
|
|
|
} else {
|
|
|
|
console.log("Empty cached species list, retrieving again");
|
|
|
|
}
|
2022-08-29 06:44:58 +02:00
|
|
|
} else {
|
|
|
|
return eBird.product.spplist.in(regionCode)()
|
|
|
|
.then(species => {
|
|
|
|
cache.cacheResponse(`spplist-${regionCode}`, species);
|
|
|
|
return species;
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getAudio(speciesScientificName) {
|
|
|
|
return XenoCanto.search({
|
|
|
|
name: speciesScientificName,
|
|
|
|
quality: 'A'
|
|
|
|
}).then(response => {
|
|
|
|
const { recordings } = response;
|
2024-02-13 19:01:36 +01:00
|
|
|
let randomRecord;
|
|
|
|
let audio;
|
|
|
|
for (let i=0; i < 3; i++) {
|
|
|
|
randomRecord = choice(recordings);
|
|
|
|
if ('file' in randomRecord) {
|
|
|
|
audio = randomRecord.file;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-08-29 06:44:58 +02:00
|
|
|
return audio;
|
|
|
|
}).catch(error => {
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
2024-02-12 09:29:34 +01:00
|
|
|
generateQuestion,
|
|
|
|
getQuestionCached,
|
|
|
|
cacheQuestion,
|
|
|
|
generateQuizz
|
2022-08-29 06:44:58 +02:00
|
|
|
}
|