Files
flashscore/daemon/daemon.go

274 lines
6.4 KiB
Go
Raw Normal View History

2026-07-21 03:49:47 +03:00
package daemon
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"sync"
"time"
"gordenko.dev/dima/flashscore"
"gordenko.dev/dima/flashscore/model"
"gordenko.dev/dima/qx"
"gordenko.dev/dima/web"
)
const (
defaultCheckInterval = 1 * time.Hour
)
type Options struct {
BaseDir string
CheckInterval int
Logger *log.Logger
Db *qx.Db
SportIDs []int
}
type Daemon struct {
mutex sync.Mutex
checkInterval time.Duration
fs *flashscore.Flashscore
baseDir string
dumpFilepath string
logger *log.Logger
port int
model *model.Model
// map[sportID]map[matchID]
reports map[int]map[string]flashscore.UpcomingMatchReport
}
func New(opt Options) (*Daemon, error) {
if opt.Logger == nil {
return nil, errors.New("Logger option is required")
}
if opt.Db == nil {
return nil, errors.New("Db option is required")
}
if opt.BaseDir == "" {
return nil, errors.New("BaseDir option is required")
}
s := &Daemon{
logger: opt.Logger,
baseDir: opt.BaseDir,
dumpFilepath: filepath.Join(opt.BaseDir, "reports.dump"),
fs: flashscore.New(opt.Db, opt.Logger),
model: model.New(opt.Db),
reports: make(map[int]map[string]flashscore.UpcomingMatchReport),
}
if opt.CheckInterval <= 0 {
s.checkInterval = defaultCheckInterval
} else {
s.checkInterval = time.Duration(opt.CheckInterval) * time.Second
}
for _, sportID := range opt.SportIDs {
s.reports[sportID] = make(map[string]flashscore.UpcomingMatchReport)
}
err := s.tryRestoreReportsFromDump()
if err != nil {
// только логгируем, ибо если файла нет или он поврежден - это не проблема
s.logger.Printf("tryRestoreReportsFromDump: %s\n", err)
}
return s, nil
}
func (s *Daemon) Close() {
s.mutex.Lock()
defer s.mutex.Unlock()
buf, err := json.MarshalIndent(s.reports, "", " ")
if err != nil {
return
}
err = os.WriteFile(s.dumpFilepath, buf, 0666)
if err != nil {
return
}
}
func (s *Daemon) tryRestoreReportsFromDump() (err error) {
buf, err := os.ReadFile(s.dumpFilepath)
if err != nil {
return
}
return json.Unmarshal(buf, &s.reports)
}
func (s *Daemon) ShowReports(state *web.State, reply *web.Reply) (err error) {
buf, _ := json.MarshalIndent(s.reports, "", " ")
str := fmt.Sprintf("%s", buf)
reply.WriteString(str)
return nil
}
func (s *Daemon) Run() {
ticker := time.NewTicker(s.checkInterval)
s.logger.Println("tick")
for sportID := range s.reports {
s.checkUpcomingMatches(sportID)
}
s.logger.Println("tick processed")
for {
select {
case <-ticker.C:
s.logger.Println("tick")
for sportID := range s.reports {
s.checkUpcomingMatches(sportID)
}
s.logger.Println("tick processed")
}
}
}
func (s *Daemon) ListUpcomingMatchReports(sportID int) (list []flashscore.UpcomingMatchReport, err error) {
s.mutex.Lock()
defer s.mutex.Unlock()
now := time.Now().Unix() - 3600
reports, ok := s.reports[sportID]
if !ok {
return
}
fmt.Printf("sportID: %d, reports size: %d\n", sportID, len(reports))
var started []string
for matchID, report := range reports {
if report.StartTime < now {
// Матч уже начался
//fmt.Printf("matchStarted: startTime %d < now %d\n", report.StartTime, now)
started = append(started, matchID)
} else {
if report.IsTeamsLoaded && report.IsHistoryLoaded {
list = append(list, report)
}
}
}
// Удаляем матчи, которые начались
for _, matchID := range started {
delete(reports, matchID)
}
// Сортируем матчи
sort.Slice(list, func(i, j int) bool {
a := list[i]
b := list[j]
if a.StartTime == b.StartTime {
// Время начала равно - сравниваем MatchID
return a.MatchID < b.MatchID
}
return a.StartTime < b.StartTime
})
return
}
func (s *Daemon) checkUpcomingMatches(sportID int) {
scheduledMatches, err := s.fs.ListOfScheduledMatches(sportID)
if err != nil {
s.logger.Printf("ListOfScheduledMatches: %s\n", err)
return
}
s.logger.Printf("Sport: %d, scheduled matches found: %d\n", sportID, len(scheduledMatches))
reports := s.reports[sportID]
for idx, match := range scheduledMatches {
if sportID == flashscore.Tennis {
// Пары игнорим
if flashscore.IsTennisDoublesChamp(match.FullChampName) {
continue
}
}
// Здесь лок не нужен, ибо нет race conditions, пишем в мапу в этом же потоке
report, ok := reports[match.MatchID]
if !ok {
report = flashscore.UpcomingMatchReport{
MatchID: match.MatchID,
SportID: match.SportID,
StartTime: match.StartTime.Unix(),
Zone: model.TeamZone{
ZoneID: match.Zone.ZoneID,
Name: match.Zone.Name,
SportID: match.SportID,
},
Champ: model.TeamChamp{
ChampID: match.ChampID,
},
FullChampName: match.FullChampName,
}
// Ниже по тексту report будет перезаписан в коллекции reports
}
// Даже если отчет полный - синкаем, чтобы обновить odds
err = s.fs.SyncUpcomingMatchReport(&report)
if err != nil {
s.logger.Printf("SyncUpcomingMatchReport: %s; matchID=%s\n", err, report.MatchID)
} else {
report.ParseTime = time.Now()
//buf, _ := json.MarshalIndent(report, "", " ")
//fmt.Printf("REPORT:\n\n%s\n\n", buf)
s.mutex.Lock()
reports[report.MatchID] = report
s.mutex.Unlock()
}
//fmt.Printf("REPORT:\n\n%s\n\n", buf)
if (idx % 10) == 0 {
s.logger.Printf("%d matches scanned\n", idx+1)
}
}
s.logger.Printf("Sport: %d, all matches scanned\n", sportID)
}
/*
func (s *Flashscore) ListUpcomingMatchReports(sportID int, reportCh chan *UpcomingMatchReport) (err error) {
matches, err := s.listOfScheduledMatches(sportID)
if err != nil {
err = fmt.Errorf("listOfScheduledMatches: %s", err)
return
}
//matches = DiscardScheduledMatchesFurtherThan(matches, 24*time.Hour)
reports := s.reports
for _, match := range matches {
s.Li
report = &UpcomingMatchReport{
IsComplete: true,
MatchID: match.MatchID,
SportID: match.SportID,
StartTime: match.StartTime.Unix(),
}
report, reportErr := s.getUpcomingMatchReport(match)
if reportErr != nil {
s.logger.Printf("getUpcomingMatchReport: %s; matchID=%s", reportErr, match.MatchID)
} else {
reportCh <- &report
}
}
close(reportCh)
return
}
*/