feat(tournament): add sourceUrl field and implement repair logic for tournaments
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 16m19s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 16m19s
This commit is contained in:
@@ -3,6 +3,7 @@ const require = createRequire(import.meta.url);
|
||||
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
|
||||
|
||||
import { Op } from 'sequelize';
|
||||
import axios from 'axios';
|
||||
import OfficialTournament from '../models/OfficialTournament.js';
|
||||
import OfficialCompetition from '../models/OfficialCompetition.js';
|
||||
import OfficialCompetitionMember from '../models/OfficialCompetitionMember.js';
|
||||
@@ -12,6 +13,55 @@ import Member from '../models/Member.js';
|
||||
import OfficialTournamentParserService from './officialTournamentParserService.js';
|
||||
|
||||
class OfficialTournamentService {
|
||||
extractJsonArray(html, propertyName) {
|
||||
const content = String(html);
|
||||
const propertyIndex = content.indexOf(`"${propertyName}":[`);
|
||||
if (propertyIndex < 0) return [];
|
||||
const start = content.indexOf('[', propertyIndex);
|
||||
let depth = 0; let quoted = false; let escaped = false;
|
||||
for (let index = start; index < content.length; index += 1) {
|
||||
const character = content[index];
|
||||
if (quoted) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === '\\') escaped = true;
|
||||
else if (character === '"') quoted = false;
|
||||
} else if (character === '"') quoted = true;
|
||||
else if (character === '[') depth += 1;
|
||||
else if (character === ']' && --depth === 0) {
|
||||
try { return JSON.parse(content.slice(start, index + 1)); }
|
||||
catch (_error) { return []; }
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async repairTournamentFromSource(clubId, tournament) {
|
||||
let sourceUrl = tournament.sourceUrl;
|
||||
if (!sourceUrl) {
|
||||
const suggestion = await TournamentSuggestion.findOne({ where: { clubId, title: tournament.title, eventDate: tournament.eventDate } });
|
||||
sourceUrl = suggestion?.sourceUrl || null;
|
||||
if (sourceUrl) await tournament.update({ sourceUrl });
|
||||
}
|
||||
if (!sourceUrl) return 0;
|
||||
|
||||
const source = new URL(sourceUrl);
|
||||
if (!['www.mytischtennis.de', 'mytischtennis.de'].includes(source.hostname)) return 0;
|
||||
const response = await axios.get(sourceUrl, { timeout: 30000, headers: { 'User-Agent': 'Trainingstagebuch/1.0 (+turnierdetails)' } });
|
||||
const competitions = this.extractJsonArray(response.data, 'competitions')
|
||||
.filter((competition) => competition?.name)
|
||||
.map((competition) => ({
|
||||
ageClassCompetition: String(competition.name).slice(0, 255),
|
||||
cutoffDate: competition.fed_rank_remarks ? String(competition.fed_rank_remarks).slice(0, 255) : null,
|
||||
}));
|
||||
if (!competitions.length) return 0;
|
||||
|
||||
const existing = await OfficialCompetition.findAll({ where: { tournamentId: tournament.id }, attributes: ['ageClassCompetition'] });
|
||||
const existingNames = new Set(existing.map((competition) => competition.ageClassCompetition));
|
||||
const missing = competitions.filter((competition) => !existingNames.has(competition.ageClassCompetition));
|
||||
if (missing.length) await OfficialCompetition.bulkCreate(missing.map((competition) => ({ tournamentId: tournament.id, ...competition })));
|
||||
return missing.length;
|
||||
}
|
||||
|
||||
async saveTournamentSuggestion(clubId, suggestionId) {
|
||||
const suggestion = await TournamentSuggestion.findOne({ where: { id: suggestionId, clubId } });
|
||||
if (!suggestion) return null;
|
||||
@@ -31,6 +81,7 @@ class OfficialTournamentService {
|
||||
competitionTypes: JSON.stringify([]),
|
||||
registrationDeadlines: JSON.stringify([]),
|
||||
entryFees: JSON.stringify({}),
|
||||
sourceUrl: suggestion.sourceUrl,
|
||||
});
|
||||
}
|
||||
await suggestion.update({ status: 'reviewed' });
|
||||
@@ -121,7 +172,15 @@ class OfficialTournamentService {
|
||||
const t = await OfficialTournament.findOne({ where: { id, clubId } });
|
||||
if (!t) return null;
|
||||
|
||||
const comps = await OfficialCompetition.findAll({ where: { tournamentId: id } });
|
||||
let comps = await OfficialCompetition.findAll({ where: { tournamentId: id } });
|
||||
if (!comps.length) {
|
||||
try {
|
||||
await this.repairTournamentFromSource(clubId, t);
|
||||
comps = await OfficialCompetition.findAll({ where: { tournamentId: id } });
|
||||
} catch (error) {
|
||||
console.warn(`[officialTournament] Automatic repair failed for ${id}:`, error.message);
|
||||
}
|
||||
}
|
||||
const entries = await OfficialCompetitionMember.findAll({ where: { tournamentId: id } });
|
||||
const competitions = comps.map((c) => {
|
||||
const j = c.toJSON();
|
||||
|
||||
Reference in New Issue
Block a user