From 684d0b6a57d2eb79730ade63baccdc6e59bebc29 Mon Sep 17 00:00:00 2001 From: Saya Andy Date: Sat, 1 Aug 2026 19:58:57 +0700 Subject: feat: repurpose metadata parser as article transcoder for saya.uz and telegram --- internal/transcoder/sayauz.go | 138 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 internal/transcoder/sayauz.go (limited to 'internal/transcoder/sayauz.go') diff --git a/internal/transcoder/sayauz.go b/internal/transcoder/sayauz.go new file mode 100644 index 0000000..ee888e5 --- /dev/null +++ b/internal/transcoder/sayauz.go @@ -0,0 +1,138 @@ +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")) +} -- cgit v1.3.1+13