generated from VLADIMIR/template
add parser
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
proto "pinned_message/proto"
|
||||
"context"
|
||||
proto "pinned_message/proto"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
proto.UnimplementedpinnedMessageServer
|
||||
proto.UnimplementedPinnedMessageServer
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
ClientPort = ":8100"
|
||||
FilePort = ":8120"
|
||||
)
|
||||
|
||||
func GetScheduleFilepath() string {
|
||||
return getFilepath("SCHEDULE_FILENAME", "data/schedule.json")
|
||||
}
|
||||
|
||||
func getFilepath(env string, defaultFilepath string) string {
|
||||
filepath := selectFilepath(env, defaultFilepath)
|
||||
ensureDirExists(filepath)
|
||||
return filepath
|
||||
}
|
||||
|
||||
func selectFilepath(env string, defaultFilepath string) string {
|
||||
filepath := os.Getenv(env)
|
||||
if filepath != "" {
|
||||
return filepath
|
||||
}
|
||||
return defaultFilepath
|
||||
}
|
||||
|
||||
func ensureDirExists(filePath string) error {
|
||||
dir := filepath.Dir(filePath)
|
||||
if dir == "" || dir == "." || dir == "/" {
|
||||
return nil
|
||||
}
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// День с событиями
|
||||
type Day struct {
|
||||
Date time.Time `json:"date"`
|
||||
Performances []*DayPerformance `json:"performances"`
|
||||
}
|
||||
|
||||
// Событие (мероприятия, праздник, поездка, событие)
|
||||
type DayPerformance struct {
|
||||
// Время сбора
|
||||
TimeCollection string `json:"time_collection"`
|
||||
// Время начала мероприятия
|
||||
TimeStart string `json:"time_start"`
|
||||
// Место
|
||||
Place string `json:"place"`
|
||||
|
||||
// Наименование мероприятия
|
||||
Name string `json:"name"`
|
||||
// Номера
|
||||
Numbers []*Number `json:"numbers"`
|
||||
// Костюмы
|
||||
Costumes string `json:"costumes"`
|
||||
}
|
||||
|
||||
// Номер
|
||||
type Number struct {
|
||||
// Название
|
||||
Name string `json:"name"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package data_parser
|
||||
|
||||
import "context"
|
||||
|
||||
type IDataParser interface {
|
||||
Parse(ctx context.Context, url string, v interface{}) error
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package data_parser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
)
|
||||
|
||||
type parser struct{}
|
||||
|
||||
func NewGoogleTableScheduleParser() IDataParser {
|
||||
return &parser{}
|
||||
}
|
||||
|
||||
func (p *parser) Parse(_ context.Context, url string, v interface{}) error {
|
||||
re := regexp.MustCompile(`/d/([a-zA-Z0-9-_]+)`)
|
||||
matches := re.FindStringSubmatch(url)
|
||||
if len(matches) < 2 {
|
||||
return fmt.Errorf("Не удалось найти ID таблицы в ссылке")
|
||||
}
|
||||
sheetID := matches[1]
|
||||
|
||||
csvURL := fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/export?format=csv", sheetID)
|
||||
|
||||
resp, err := http.Get(csvURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Ошибка при скачивании таблицы: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Ошибка: статус код %d (убедитесь, что таблица публичная)", resp.StatusCode)
|
||||
}
|
||||
|
||||
if err := gocsv.Unmarshal(resp.Body, v); err != nil {
|
||||
return fmt.Errorf("Ошибка при парсинге CSV в структуру: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package date_parser
|
||||
|
||||
import "time"
|
||||
|
||||
type IDateParser interface {
|
||||
Parse(date string) (time.Time, error)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package date_parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ruMonths = map[string]time.Month{
|
||||
"января": time.January,
|
||||
"февраля": time.February,
|
||||
"марта": time.March,
|
||||
"апреля": time.April,
|
||||
"мая": time.May,
|
||||
"июня": time.June,
|
||||
"июля": time.July,
|
||||
"августа": time.August,
|
||||
"сентября": time.September,
|
||||
"октября": time.October,
|
||||
"ноября": time.November,
|
||||
"декабря": time.December,
|
||||
}
|
||||
)
|
||||
|
||||
type parser struct{}
|
||||
|
||||
func NewDateParser() IDateParser {
|
||||
return &parser{}
|
||||
}
|
||||
|
||||
func (p *parser) Parse(date string) (time.Time, error) {
|
||||
parts := strings.Fields(date)
|
||||
if len(parts) < 2 {
|
||||
return time.Time{}, fmt.Errorf("Неверный формат даты, ожидалось 'День Месяц', получено: '%s'", date)
|
||||
}
|
||||
|
||||
day, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("Не удалось получить день: %v", err)
|
||||
}
|
||||
|
||||
monthStr := strings.ToLower(parts[1])
|
||||
month, ok := ruMonths[monthStr]
|
||||
if !ok {
|
||||
return time.Time{}, fmt.Errorf("Неизвестный месяц: %s", monthStr)
|
||||
}
|
||||
|
||||
year := time.Now().Year()
|
||||
|
||||
return time.Date(year, month, day, 0, 0, 0, 0, time.Local), nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package schedule_parser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"pinned_message/internal/models"
|
||||
"pinned_message/internal/modules/data_parser"
|
||||
"pinned_message/internal/modules/date_parser"
|
||||
"pinned_message/internal/services/schedule_storage"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type performance struct {
|
||||
Date string `csv:"ДАТА"`
|
||||
Day string `csv:"день недели"`
|
||||
Name string `csv:"НАЗВАНИЕ"`
|
||||
Place string `csv:"МЕСТО"`
|
||||
TimeCollection string `csv:"время СБОРА (для концерта)"`
|
||||
TimeStart string `csv:"время НАЧАЛА"`
|
||||
Numbers string `csv:"ЧТО ТАНЦУЕМ"`
|
||||
Costumes string `csv:"КОСТЮМЫ"`
|
||||
}
|
||||
|
||||
type ScheduleParser struct {
|
||||
dataParser data_parser.IDataParser
|
||||
dateParser date_parser.IDateParser
|
||||
scheduleStorage schedule_storage.ScheduleStorage
|
||||
}
|
||||
|
||||
func NewScheduleParser(
|
||||
dataParser data_parser.IDataParser,
|
||||
dateParser date_parser.IDateParser,
|
||||
scheduleStorage schedule_storage.ScheduleStorage,
|
||||
) *ScheduleParser {
|
||||
return &ScheduleParser{
|
||||
dataParser: dataParser,
|
||||
dateParser: dateParser,
|
||||
scheduleStorage: scheduleStorage,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ScheduleParser) Run(ctx context.Context) {
|
||||
ticker := time.NewTicker(15 * time.Second) // TODO: set 1h
|
||||
defer ticker.Stop()
|
||||
|
||||
sheetURL := "https://docs.google.com/spreadsheets/d/1v57bCAG764j1ULXDMb3amNFMzkkLmObKWsl5oE0Xq00/edit?gid=57461713#gid=57461713"
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
days, err := p.parseSchedule(ctx, sheetURL)
|
||||
if err != nil {
|
||||
log.Printf("Error parse schedule: %s\n", sheetURL)
|
||||
break
|
||||
}
|
||||
if err := p.scheduleStorage.SaveSchedule(days); err != nil {
|
||||
log.Printf("Error save err: %s schedule: %s\n", err, sheetURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ScheduleParser) parseSchedule(ctx context.Context, sheetURL string) ([]*models.Day, error) {
|
||||
var performances []performance
|
||||
if err := p.dataParser.Parse(ctx, sheetURL, &performances); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.mapSchedule(performances), nil
|
||||
}
|
||||
|
||||
func (p *ScheduleParser) mapSchedule(performances []performance) []*models.Day {
|
||||
days := []*models.Day{}
|
||||
currentDay := &models.Day{}
|
||||
for i, performance := range performances {
|
||||
if performance.Name == "" || performance.Name == "-" {
|
||||
continue
|
||||
}
|
||||
if performance.Date != "" {
|
||||
if i > 0 {
|
||||
days = append(days, currentDay)
|
||||
currentDay = &models.Day{}
|
||||
}
|
||||
date, err := p.mapDate(performance.Date)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
currentDay.Date = date
|
||||
}
|
||||
currentDay.Performances = append(
|
||||
currentDay.Performances,
|
||||
&models.DayPerformance{
|
||||
TimeCollection: performance.TimeCollection,
|
||||
TimeStart: performance.TimeStart,
|
||||
Place: performance.Place,
|
||||
Name: performance.Name,
|
||||
Numbers: p.mapNumbers(performance.Numbers),
|
||||
Costumes: performance.Costumes,
|
||||
},
|
||||
)
|
||||
}
|
||||
days = append(days, currentDay)
|
||||
return days
|
||||
}
|
||||
|
||||
func (p *ScheduleParser) mapDate(date string) (time.Time, error) {
|
||||
if date == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return p.dateParser.Parse(date)
|
||||
}
|
||||
|
||||
func (p *ScheduleParser) mapNumbers(numbers string) []*models.Number {
|
||||
names := strings.Split(numbers, ",")
|
||||
res := make([]*models.Number, 0, len(names))
|
||||
for _, name := range names {
|
||||
res = append(
|
||||
res,
|
||||
&models.Number{
|
||||
Name: name,
|
||||
},
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package schedule_storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"pinned_message/internal/models"
|
||||
)
|
||||
|
||||
type ScheduleStorage struct {
|
||||
filepath string
|
||||
}
|
||||
|
||||
func NewScheduleStorage(
|
||||
filepath string,
|
||||
) *ScheduleStorage {
|
||||
return &ScheduleStorage{
|
||||
filepath: filepath,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScheduleStorage) SaveSchedule(days []*models.Day) error {
|
||||
data, err := json.Marshal(days)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(s.filepath, data, 0x777); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("save story to: %s", s.filepath)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user