274 lines
6.4 KiB
Go
274 lines
6.4 KiB
Go
|
|
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
|
||
|
|
}
|
||
|
|
*/
|