package transcoder import ( "fmt" "log/slog" "os" "path/filepath" "strings" "time" "github.com/SayaAndy/saya-today-article-metadata-add/config" "github.com/SayaAndy/saya-today-article-metadata-add/internal/draft" "github.com/SayaAndy/saya-today-article-metadata-add/internal/storage" ) type SayauzTranscoder struct { category string storage storage.StorageClient } func NewSayauzTranscoder(cfg any) (Transcoder, error) { sayauzCfg, ok := cfg.(*config.SayauzConfig) if !ok { return nil, fmt.Errorf("invalid config for sayauz transcoder") } newClient, ok := storage.NewStorageClientMap[sayauzCfg.Storage.Type] if !ok { return nil, fmt.Errorf("unsupported storage type %q for sayauz transcoder", sayauzCfg.Storage.Type) } storageClient, err := newClient(&sayauzCfg.Storage) if err != nil { return nil, fmt.Errorf("init storage client: %w", err) } return &SayauzTranscoder{ category: sayauzCfg.Category, storage: storageClient, }, nil } func (t *SayauzTranscoder) Name() string { return "sayauz" } func (t *SayauzTranscoder) Transcode(doc *draft.Document) error { if doc.Metadata == nil { return fmt.Errorf("draft %q has no frontmatter metadata", doc.SourcePath) } prodKey := t.category + "/" + doc.Codename + ".md" // publishedTime is the first-publish time: reuse an existing value (from the // draft frontmatter, else from the already-published object), otherwise stamp now. inject := doc.Metadata.PublishedTime.IsZero() if inject { published := time.Time{} if meta, err := t.storage.GetMetadata(prodKey); err == nil { if v := meta["published-time"]; v != "" { if parsed, err := time.Parse(time.RFC3339, v); err == nil { published = parsed } } } else { slog.Warn("could not read existing metadata for publishedTime, stamping now", slog.String("key", prodKey), slog.String("error", err.Error())) } if published.IsZero() { published = time.Now() } doc.Metadata.PublishedTime = published } formatted := formatContent(doc.RawContent, doc.Metadata.PublishedTime, inject) localPath := filepath.Join(filepath.Dir(doc.SourcePath), doc.Codename+".md") if err := os.WriteFile(localPath, formatted, 0o644); err != nil { return fmt.Errorf("write formatted file %q: %w", localPath, err) } if err := t.storage.Put(prodKey, formatted, doc.Metadata); err != nil { return fmt.Errorf("upload %q: %w", prodKey, err) } slog.Info("sayauz published page", slog.String("key", prodKey), slog.String("local", localPath)) return nil } func (t *SayauzTranscoder) Finalize() error { return t.storage.BuildIndex() } // formatContent renders a draft into its published form: standalone "..." // separator lines become blank lines (trailing ones are dropped), and when // inject is true a "publishedTime:" line is added after "actionDate:" in the // frontmatter. Frontmatter, galleries and "---" rules are otherwise untouched. func formatContent(raw []byte, published time.Time, inject bool) []byte { lines := strings.Split(string(raw), "\n") out := make([]string, 0, len(lines)+1) inFrontmatter := false frontmatterDone := false for i, line := range lines { trimmed := strings.TrimSpace(line) if !frontmatterDone { if i == 0 && trimmed == "---" { inFrontmatter = true out = append(out, line) continue } if inFrontmatter { out = append(out, line) switch { case trimmed == "---": inFrontmatter = false frontmatterDone = true case inject && strings.HasPrefix(trimmed, "actionDate:"): out = append(out, "publishedTime: "+published.Format(time.RFC3339)) } continue } } if trimmed == "..." { out = append(out, "") continue } out = append(out, line) } for len(out) > 0 && out[len(out)-1] == "" { out = out[:len(out)-1] } return []byte(strings.Join(out, "\n")) }