This commit is contained in:
2026-04-16 17:21:48 +03:00
parent c8fa4c442d
commit c8e7e4e927
116 changed files with 3720 additions and 4197 deletions
@@ -1,24 +1,24 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../database/prisma.service';
import axios from 'axios';
import * as fs from 'fs';
import * as path from 'path';
import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { ConfigService } from "@nestjs/config";
import { PrismaService } from "../../database/prisma.service";
import axios from "axios";
import * as fs from "fs";
import * as path from "path";
import { ImageRendererService } from './image-renderer.service';
import { CaptionGeneratorService } from './caption-generator.service';
import { TwitterService } from './twitter.service';
import { MetaService } from './meta.service';
import { ImageRendererService } from "./image-renderer.service";
import { CaptionGeneratorService } from "./caption-generator.service";
import { TwitterService } from "./twitter.service";
import { MetaService } from "./meta.service";
import {
PredictionCardDto,
TopPick,
SocialPostResult,
} from './dto/prediction-card.dto';
} from "./dto/prediction-card.dto";
// Top leagues loaded once
const TOP_LEAGUES_PATH = path.join(process.cwd(), 'top_leagues.json');
const TOP_LEAGUES_PATH = path.join(process.cwd(), "top_leagues.json");
@Injectable()
export class SocialPosterService {
@@ -38,24 +38,24 @@ export class SocialPosterService {
private readonly metaService: MetaService,
) {
this.aiEngineUrl =
this.configService.get<string>('AI_ENGINE_URL') ||
'http://localhost:8000';
this.configService.get<string>("AI_ENGINE_URL") ||
"http://localhost:8000";
this.appBaseUrl =
this.configService.get<string>('APP_BASE_URL') || 'http://localhost:3000';
this.configService.get<string>("APP_BASE_URL") || "http://localhost:3000";
this.isEnabled =
this.configService.get<string>('SOCIAL_POSTER_ENABLED') === 'true';
this.configService.get<string>("SOCIAL_POSTER_ENABLED") === "true";
this.loadTopLeagues();
}
private loadTopLeagues() {
try {
const data = fs.readFileSync(TOP_LEAGUES_PATH, 'utf-8');
const data = fs.readFileSync(TOP_LEAGUES_PATH, "utf-8");
const ids = JSON.parse(data);
this.topLeagueIds = new Set(ids);
this.logger.log(`✅ Loaded ${this.topLeagueIds.size} top league IDs`);
} catch {
this.logger.warn('⚠️ Could not load top_leagues.json');
this.logger.warn("⚠️ Could not load top_leagues.json");
}
}
@@ -63,7 +63,7 @@ export class SocialPosterService {
* Cron: Every 10 minutes, check for upcoming matches.
* Posts predictions 30 minutes before kickoff.
*/
@Cron('*/10 * * * *')
@Cron("*/10 * * * *")
async checkAndPostUpcomingMatches() {
if (!this.isEnabled) return;
@@ -115,7 +115,7 @@ export class SocialPosterService {
const matches = await this.prisma.liveMatch.findMany({
where: {
sport: 'football',
sport: "football",
leagueId: { in: Array.from(this.topLeagueIds) },
mstUtc: {
gte: minTime,
@@ -144,7 +144,7 @@ export class SocialPosterService {
// Step 1: Get prediction from AI Engine
const prediction = await this.getPrediction(matchId);
if (!prediction) {
throw new Error('No prediction returned from AI Engine');
throw new Error("No prediction returned from AI Engine");
}
// Step 2: Build prediction card data
@@ -194,9 +194,9 @@ export class SocialPosterService {
this.logger.log(
`✅ Posted: ${match.homeTeam?.name} vs ${match.awayTeam?.name} ` +
`[TW: ${result.twitterPostId ? '✅' : '❌'}, ` +
`FB: ${result.facebookPostId ? '✅' : '❌'}, ` +
`IG: ${result.instagramPostId ? '✅' : '❌'}]`,
`[TW: ${result.twitterPostId ? "✅" : "❌"}, ` +
`FB: ${result.facebookPostId ? "✅" : "❌"}, ` +
`IG: ${result.instagramPostId ? "✅" : "❌"}]`,
);
return result;
@@ -229,8 +229,8 @@ export class SocialPosterService {
): PredictionCardDto {
// V20+ returns score_prediction.ft / .ht
const score = prediction.score_prediction || {};
const htScore = score.ht || '0-0';
const ftScore = score.ft || '1-1';
const htScore = score.ht || "0-0";
const ftScore = score.ft || "1-1";
// Extract best bets from bet_summary array
const topPicks = this.extractTopPicks(prediction);
@@ -247,18 +247,18 @@ export class SocialPosterService {
return {
matchId: match.id,
homeTeam:
match.homeTeam?.name || prediction.match_info?.home_team || 'Home',
match.homeTeam?.name || prediction.match_info?.home_team || "Home",
awayTeam:
match.awayTeam?.name || prediction.match_info?.away_team || 'Away',
homeLogo: this.resolveLogoUrl(match.homeTeam?.logoUrl || ''),
awayLogo: this.resolveLogoUrl(match.awayTeam?.logoUrl || ''),
leagueName: match.league?.name || prediction.match_info?.league || '',
match.awayTeam?.name || prediction.match_info?.away_team || "Away",
homeLogo: this.resolveLogoUrl(match.homeTeam?.logoUrl || ""),
awayLogo: this.resolveLogoUrl(match.awayTeam?.logoUrl || ""),
leagueName: match.league?.name || prediction.match_info?.league || "",
matchDate,
htScore,
ftScore,
scoreConfidence,
topPicks,
riskLevel: prediction.risk?.level || 'MEDIUM',
riskLevel: prediction.risk?.level || "MEDIUM",
rawPrediction: prediction,
};
}
@@ -271,16 +271,16 @@ export class SocialPosterService {
// Market code to Turkish/English label mapping
const marketLabels: Record<string, { tr: string; en: string }> = {
MS: { tr: 'Maç Sonucu', en: 'Match Result' },
OU15: { tr: 'Üst 1.5 Gol', en: 'Over 1.5' },
OU25: { tr: 'Üst 2.5 Gol', en: 'Over 2.5' },
OU35: { tr: 'Üst 3.5 Gol', en: 'Over 3.5' },
BTTS: { tr: 'Karşılıklı Gol', en: 'Both Teams Score' },
DC: { tr: 'Çifte Şans', en: 'Double Chance' },
HT: { tr: 'İlk Yarı Sonucu', en: 'Half Time Result' },
HT_OU05: { tr: 'İY 0.5 Üst/Alt', en: 'HT Over/Under 0.5' },
OE: { tr: 'Tek/Çift', en: 'Odd/Even' },
HTFT: { tr: 'İY/MS', en: 'HT/FT' },
MS: { tr: "Maç Sonucu", en: "Match Result" },
OU15: { tr: "Üst 1.5 Gol", en: "Over 1.5" },
OU25: { tr: "Üst 2.5 Gol", en: "Over 2.5" },
OU35: { tr: "Üst 3.5 Gol", en: "Over 3.5" },
BTTS: { tr: "Karşılıklı Gol", en: "Both Teams Score" },
DC: { tr: "Çifte Şans", en: "Double Chance" },
HT: { tr: "İlk Yarı Sonucu", en: "Half Time Result" },
HT_OU05: { tr: "İY 0.5 Üst/Alt", en: "HT Over/Under 0.5" },
OE: { tr: "Tek/Çift", en: "Odd/Even" },
HTFT: { tr: "İY/MS", en: "HT/FT" },
};
const candidates: TopPick[] = betSummary.map((bet) => {
@@ -308,11 +308,11 @@ export class SocialPosterService {
* Locally during dev, we fetch them from the deployed server via APP_BASE_URL.
*/
private resolveLogoUrl(logoUrl: string): string {
if (!logoUrl) return '';
if (!logoUrl) return "";
// Already a full URL
if (logoUrl.startsWith('http')) return logoUrl;
if (logoUrl.startsWith("http")) return logoUrl;
// Relative path → check local first, otherwise make full URL
const localPath = path.join(process.cwd(), 'public', logoUrl);
const localPath = path.join(process.cwd(), "public", logoUrl);
if (fs.existsSync(localPath)) return logoUrl; // Keep relative, renderer reads local
// Not local → prepend base URL for remote fetch
return `${this.appBaseUrl}${logoUrl}`;
@@ -321,24 +321,24 @@ export class SocialPosterService {
private formatMatchDate(mstUtc: number | bigint): string {
const d = new Date(Number(mstUtc));
const months = [
'Oca',
'Şub',
'Mar',
'Nis',
'May',
'Haz',
'Tem',
'Ağu',
'Eyl',
'Eki',
'Kas',
'Ara',
"Oca",
"Şub",
"Mar",
"Nis",
"May",
"Haz",
"Tem",
"Ağu",
"Eyl",
"Eki",
"Kas",
"Ara",
];
const day = String(d.getDate()).padStart(2, '0');
const day = String(d.getDate()).padStart(2, "0");
const month = months[d.getMonth()];
const year = d.getFullYear();
const hour = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
const hour = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
return `${day} ${month} ${year} - ${hour}:${min}`;
}
@@ -383,7 +383,7 @@ export class SocialPosterService {
const prediction = await this.getPrediction(matchId);
if (!prediction) {
throw new Error('No prediction returned from AI Engine');
throw new Error("No prediction returned from AI Engine");
}
const card = this.buildCardFromPrediction(match, prediction);