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 } */