package plugins import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "sync" "time" "trbot/utils" "trbot/utils/configs" "trbot/utils/plugin_utils" "trbot/utils/task" "trbot/utils/yaml" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" "github.com/reugn/go-quartz/job" "github.com/reugn/go-quartz/quartz" "github.com/rs/zerolog" ) var msrConfigPath string = filepath.Join(configs.YAMLDatabaseDir, "msr_update/", configs.YAMLFileName) var msrConfig MSRUpdateConfig var msrCachedDir string = filepath.Join(configs.CacheDir, "msr_conver/") type MSRUpdateConfig struct { lock sync.Mutex botIns *bot.Bot ChannelID int64 `yaml:"ChannelID"` SilentPost bool `yaml:"SilentPost"` APIBaseURL string `yaml:"APIBaseURL"` PostedAlbumID string `yaml:"PostedAlbumID"` TaskCron string `yaml:"TaskCron"` PostIntervalInMinute int `yaml:"PostIntervalInMinute"` } func DoAPIRequest(ctx context.Context, URI string, data any) error { resp, err := http.Get(msrConfig.APIBaseURL + URI) if err != nil { return err } defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return err } return nil } type AlbumsResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data []Album `json:"data"` } type Album struct { CID string `json:"cid"` Name string `json:"name"` ConverURL string `json:"coverUrl"` Artistes []string`json:"artistes"` } func GetAlbums(ctx context.Context) ([]Album, error) { var albumsResp AlbumsResponse err := DoAPIRequest(ctx, "albums", &albumsResp) if err != nil { return nil, fmt.Errorf("failed to get albums: %w", err) } if albumsResp.Code != 0 { return nil, fmt.Errorf("response code is not 0, msg: %s", albumsResp.Msg) } return albumsResp.Data, nil } func FindNewAlbums(ctx context.Context) ([]Album, error) { albums, err := GetAlbums(ctx) if err != nil { return nil, err } var needPost []Album for i := len(albums) - 1; i >= 0; i-- { if msrConfig.PostedAlbumID == albums[i].CID { needPost = albums[:i] break } } for i, j := 0, len(needPost)-1; i < j; i, j = i+1, j-1 { needPost[i], needPost[j] = needPost[j], needPost[i] } return needPost, nil } type AlbumDetailResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data AlbumDetail `json:"data"` } type AlbumDetail struct { CID string `json:"cid"` Name string `json:"name"` Intro string `json:"intro"` Belong string `json:"belong"` ConverURL string `json:"coverUrl"` CoverDeURL string `json:"coverDeUrl"` // 暂时不需要 Songs []Song `json:"songs"` } func (ad *AlbumDetail) Escape() { ad.Name = utils.IgnoreHTMLTags(strings.TrimSpace(ad.Name)) ad.Intro = utils.IgnoreHTMLTags(strings.Trim(ad.Intro, "\n")) } func (ad AlbumDetail) GetLandscapeCoverImage(ctx context.Context) (io.Reader, error) { coverPath := filepath.Join(msrCachedDir, ad.CID + ".jpg") _, err := os.Stat(coverPath) if err != nil { if os.IsNotExist(err) { err = os.MkdirAll(msrCachedDir, 0755) if err != nil { return nil, fmt.Errorf("failed to create directory [%s] to cache cover image: %w", msrCachedDir, err) } resp, err := http.Get(ad.CoverDeURL) if err != nil { return nil, fmt.Errorf("failed to download cover image: %w", err) } defer resp.Body.Close() stickerfile, err := os.Create(coverPath) if err != nil { return nil, fmt.Errorf("failed to create cover image file [%s]: %w", coverPath, err) } defer stickerfile.Close() _, err = io.Copy(stickerfile, resp.Body) if err != nil { return nil, fmt.Errorf("failed to writing data to cover image file [%s]: %w", coverPath, err) } } else { return nil, fmt.Errorf("failed to check cover image file [%s]: %w", coverPath, err) } } data, err := os.Open(coverPath) if err != nil { return nil, fmt.Errorf("failed to open cover image file [%s]: %w", coverPath, err) } return data, nil } type Song struct { CID string `json:"cid"` Name string `json:"name"` Artistes []string `json:"artistes"` } func GetAlbumDetail(ctx context.Context, albumID string) (AlbumDetail, error) { var albumDetailResp AlbumDetailResponse err := DoAPIRequest(ctx, fmt.Sprintf("album/%s/detail", albumID), &albumDetailResp) if err != nil { return AlbumDetail{}, fmt.Errorf("failed to get album detail: %w", err) } if albumDetailResp.Code != 0 { return AlbumDetail{}, fmt.Errorf("failed to get album detail: %s", albumDetailResp.Msg) } return albumDetailResp.Data, nil } func init() { plugin_utils.AddInitializer(plugin_utils.Initializer{ Name: "msr_update", Func: initMSRUpdate, }) plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{ Name: "msr_update", Loader: readMSRConfig, Saver: saveMSRConfig, }) } func initMSRUpdate(ctx context.Context, thebot *bot.Bot) error { msrConfig.botIns = thebot logger := zerolog.Ctx(ctx). With(). Str("pluginName", "msr_update"). Str(utils.GetCurrentFuncName()). Logger() err := readMSRConfig(ctx) if err != nil { logger.Error(). Err(err). Msg("Failed to read msr update config") return fmt.Errorf("failed to read msr update config: %w", err) } trigger, err := quartz.NewCronTriggerWithLoc(msrConfig.TaskCron, time.FixedZone("CST", 8*3600)) if err != nil { logger.Error(). Err(err). Msg("Failed to create cron trigger") return fmt.Errorf("failed to create cron trigger: %w", err) } err = task.ScheduleTask(ctx, task.Task{ Name: "check_new_msr_album", Group: "msr_update", Job: job.NewFunctionJobWithDesc( MSRUpdateTask, "check new msr album and post", ), Trigger: trigger, }) if err != nil { logger.Error(). Err(err). Msg("Failed to schedule task") return fmt.Errorf("failed to schedule task: %w", err) } return nil } func readMSRConfig(ctx context.Context) error { logger := zerolog.Ctx(ctx). With(). Str("pluginName", "msr_update"). Str(utils.GetCurrentFuncName()). Logger() err := yaml.LoadYAML(msrConfigPath, &msrConfig) if err != nil { if os.IsNotExist(err) { logger.Warn(). Err(err). Str("path", msrConfigPath). Msg("Not found msr update config file. Created new one") err = yaml.SaveYAML(msrConfigPath, &MSRUpdateConfig{ APIBaseURL: "https://monster-siren.hypergryph.com/api/", TaskCron: "0 0 11,17 * * ?", PostIntervalInMinute: 5, }) if err != nil { logger.Error(). Err(err). Str("path", msrConfigPath). Msg("Failed to create empty config") return fmt.Errorf("failed to create empty config: %w", err) } } else { logger.Error(). Err(err). Str("path", msrConfigPath). Msg("Failed to read config file") // 读取配置文件内容失败也不允许重新启动 return fmt.Errorf("failed to read config file: %w", err) } } return err } func saveMSRConfig(ctx context.Context) error { err := yaml.SaveYAML(msrConfigPath, &msrConfig) if err != nil { zerolog.Ctx(ctx).Error(). Str("pluginName", "msr_update"). Str(utils.GetCurrentFuncName()). Err(err). Str("path", msrConfigPath). Msg("Failed to save msr update config") return fmt.Errorf("failed to save msr update config: %w", err) } return nil } func MSRUpdateTask(ctx context.Context) (int, error) { logger := zerolog.Ctx(ctx). With(). Str("pluginName", "msr_update"). Str(utils.GetCurrentFuncName()). Logger() if msrConfig.lock.TryLock() { defer msrConfig.lock.Unlock() } else { logger.Info().Msg("Another task is running, skip this run") return 0, nil } newAlbums, err := FindNewAlbums(ctx) if err != nil { logger.Err(err). Str("PostedAlbumID", msrConfig.PostedAlbumID). Msg("Failed to get new albums") return 1, fmt.Errorf("failed to get new albums: %w", err) } if len(newAlbums) == 0 { logger.Info(). Str("PostedAlbumID", msrConfig.PostedAlbumID). Msg("No new albums found") return 0, nil } logger.Info(). Str("PostedAlbumID", msrConfig.PostedAlbumID). Int("count", len(newAlbums)). // Interface("newAlbums", newAlbums). Msg("New albums found") for _, album := range newAlbums { logger.Info(). Str("album", album.Name). Str("albumID", album.CID). Msg("New album found") detail, err := GetAlbumDetail(ctx, album.CID) if err != nil { logger.Err(err). Str("album", album.Name). Str("albumID", album.CID). Msg("failed to get album detail") return 1, fmt.Errorf(`failed to get new %s:"%s" album detail: %w`, album.CID, album.Name, err) } detail.Escape() logger.Info(). Str("Name", detail.Name). Str("Intro", detail.Intro). Str("CoverDeURL", detail.CoverDeURL). Msg("Album detail found") // 把图片下载到本地再发出去 data, err := detail.GetLandscapeCoverImage(ctx) if err != nil { logger.Err(err). Str("album", album.Name). Str("albumID", album.CID). Msg("failed to get album cover image") return 1, fmt.Errorf(`failed to get new %s:"%s" album cover image: %w`, album.CID, album.Name, err) } _, err = msrConfig.botIns.SendPhoto(ctx, &bot.SendPhotoParams{ ChatID: msrConfig.ChannelID, Photo: &models.InputFileUpload{ Filename: fmt.Sprintf("%s.jpg", album.CID), Data: data, }, ParseMode: models.ParseModeHTML, DisableNotification: msrConfig.SilentPost, Caption: fmt.Sprintf( "%s\n\n%s\n\nListen on\nMonster Siren Records", detail.Name, detail.Intro, detail.Songs[0].CID, ), }) if err != nil { logger.Err(err). Str("album", album.Name). Str("albumID", album.CID). Msg("Failed to send new album") return 1, fmt.Errorf(`failed to send new %s:"%s" album: %w`, album.CID, album.Name, err) } logger.Info(). Str("album", album.Name). Str("albumID", album.CID). Msg("New album sent") msrConfig.PostedAlbumID = album.CID time.Sleep(time.Duration(msrConfig.PostIntervalInMinute) * time.Minute) } err = saveMSRConfig(ctx) if err != nil { logger.Err(err). Msg("Failed to save msr config after post") return 1, fmt.Errorf("failed to save msr config after post: %w", err) } logger.Info(). Msg("MSR update task done") return 0, nil }