Files
2026-10-27 16:44:55 +08:00

311 lines
7.8 KiB
Go

package rss
import (
"context"
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/common/utils"
"videoweb/pub/models/cfg"
"videoweb/pub/pubdef"
"videoweb/web/service/srv_cfg"
)
const (
maxRSSItems = 200 // RSS 条目上限
)
var generateMu sync.Mutex
// RSS 2.0 结构定义
type rssFeed struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Atom string `xml:"xmlns:atom,attr"`
Channel rssChannel `xml:"channel"`
}
type rssChannel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Language string `xml:"language"`
LastBuildDate string `xml:"lastBuildDate"`
AtomLink atomLink `xml:"atom:link"`
Items []rssItem `xml:"item"`
}
type atomLink struct {
Href string `xml:"href,attr"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
}
type rssItem struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
PubDate string `xml:"pubDate"`
GUID string `xml:"guid"`
Enclosure *rssEnclosure `xml:"enclosure,omitempty"`
}
type rssEnclosure struct {
URL string `xml:"url,attr"`
Type string `xml:"type,attr"`
Length string `xml:"length,attr"`
}
// rssEntry 内部排序用,合并文章与工具后统一输出
type rssEntry struct {
Id int64
Title string
Link string
Description string
PubAt time.Time
Cover string
}
func GenerateRSS() {
generateMu.Lock()
defer generateMu.Unlock()
ctx := context.Background()
config, serr := srv_cfg.GetSystemConfig(ctx)
if serr != nil {
log.Error("GenerateRSS GetSystemConfig", log.Any("err", serr))
return
}
if config == nil {
log.Error("GenerateRSS GetSystemConfig nil config")
return
}
//
//entries := loadRSSEntries(ctx, config.WebAddr, maxRSSItems)
//items := buildRSSItems(config, entries)
//feedURL := utils.JoinUrlPath(config.WebAddr, "/rss.xml")
//feed := rssFeed{
// Version: "2.0",
// Atom: "http://www.w3.org/2005/Atom",
// Channel: rssChannel{
// Title: config.WebTitle,
// Link: config.WebAddr,
// Description: channelDescription(config),
// Language: "zh-cn",
// LastBuildDate: time.Now().Format(time.RFC1123Z),
// AtomLink: atomLink{
// Href: feedURL,
// Rel: "self",
// Type: "application/rss+xml",
// },
// Items: items,
// },
//}
//const filename = "rss.xml"
//if err := writeRSSFile(filename, feed); err != nil {
// log.Error("GenerateRSS write", log.Any("err", err))
// return
//}
//
//src := filepath.Join(pubdef.OutputDir, filename+".tmp")
//dst := filepath.Join(pubdef.OutputDir, filename)
//if err := os.Rename(src, dst); err != nil {
// log.Error("GenerateRSS rename", log.Any("err", err))
// os.Remove(src)
// return
//}
//log.Info("GenerateRSS done", log.Any("items", len(items)))
}
func channelDescription(config *cfg.SysCfg) string {
if config.WebDescription != "" {
return config.WebDescription
}
return "AI 工具导航与资讯更新"
}
//
//func loadRSSEntries(ctx context.Context, baseURL string, limit int) []rssEntry {
// baseURL = strings.TrimSuffix(baseURL, "/")
// articleFilter := bson.M{
// articlemod.FN.Article.ReviewStatus: 1,
// articlemod.FN.Article.Slug: bson.M{"$nin": []interface{}{"", nil}},
// }
// articleOpt := options.Find().
// SetSort(bson.D{{Key: articlemod.FN.Article.CreatedAt, Value: -1}}).
// SetLimit(int64(limit))
// articles, _ := articlemod.FindMany(ctx, articleFilter, articleOpt)
//
// toolFilter := bson.M{
// toolmod.FN.Tool.ReviewStatus: 1,
// toolmod.FN.Tool.Slug: bson.M{"$nin": []interface{}{"", nil}},
// }
// toolOpt := options.Find().
// SetSort(bson.D{{Key: toolmod.FN.Tool.CreatedAt, Value: -1}}).
// SetLimit(int64(limit))
// tools, _ := toolmod.FindMany(ctx, toolFilter, toolOpt)
//
// entries := make([]rssEntry, 0, len(articles)+len(tools))
// for _, a := range articles {
// if a == nil || a.Slug == "" {
// continue
// }
// desc := a.Intro
// if desc == "" {
// desc = a.Title
// }
// entries = append(entries, rssEntry{
// Id: a.Id,
// Title: a.Title,
// Link: articleDetailURL(baseURL, a.Slug),
// Description: desc,
// PubAt: a.CreatedAt,
// Cover: a.Cover,
// })
// }
// for _, t := range tools {
// if t == nil || t.Slug == "" {
// continue
// }
// desc := t.Intro
// if desc == "" {
// desc = t.Desc
// }
// if desc == "" {
// desc = t.Title
// }
// cover := t.Logo
// if cover == "" {
// cover = t.Snapshot
// }
// entries = append(entries, rssEntry{
// Id: t.Id,
// Title: t.Title,
// Link: toolDetailURL(baseURL, t.Slug),
// Description: desc,
// PubAt: t.CreatedAt,
// Cover: cover,
// })
// }
//
// sort.Slice(entries, func(i, j int) bool {
// return entries[i].PubAt.After(entries[j].PubAt)
// })
// if len(entries) > limit {
// entries = entries[:limit]
// }
// return entries
//}
//
//func buildRSSItems(config *cfg.SysCfg, entries []rssEntry) []rssItem {
// logoURL := utils.JoinUrlPath(strings.TrimSuffix(config.WebAddr, "/"), "/static/images/logo.png")
// imgCdn := imageCdn(config)
//
// items := make([]rssItem, 0, len(entries))
// for _, e := range entries {
// if e.Link == "" {
// continue
// }
//
// pubDate := ""
// if !e.PubAt.IsZero() {
// pubDate = e.PubAt.Format(time.RFC1123Z)
// }
//
// item := rssItem{
// Title: e.Title,
// Link: e.Link,
// Description: e.Description,
// PubDate: pubDate,
// GUID: e.Link,
// }
//
// encURL, encType := resolveEnclosureURL(config, imgCdn, logoURL, e.Cover, e.Id)
// item.Enclosure = &rssEnclosure{
// URL: encURL,
// Type: encType,
// Length: "0",
// }
// items = append(items, item)
// }
// return items
//}
func imageCdn(config *cfg.SysCfg) string {
if config.ImgWebCdn != "" {
return config.ImgWebCdn
}
return config.UnencryptedCdn
}
func resolveEnclosureURL(config *cfg.SysCfg, imgCdn, logoURL, cover string, id int64) (url, mime string) {
mime = "image/jpeg"
if cover != "" && imgCdn != "" {
url = netutil.MergeURL(imgCdn, cover)
} else if len(config.SitemapDefaultImages) > 0 && imgCdn != "" {
img := config.SitemapDefaultImages[absModIndex(id, len(config.SitemapDefaultImages))]
url = netutil.MergeURL(imgCdn, img)
} else {
url = logoURL
mime = "image/png"
}
if strings.HasSuffix(strings.ToLower(url), ".png") {
mime = "image/png"
} else if strings.HasSuffix(strings.ToLower(url), ".webp") {
mime = "image/webp"
}
return url, mime
}
func toolDetailURL(baseURL, slug string) string {
return utils.JoinUrlPath(baseURL, fmt.Sprintf("/tool/details/%s", slug))
}
func articleDetailURL(baseURL, slug string) string {
return utils.JoinUrlPath(baseURL, fmt.Sprintf("/article/details/%s", slug))
}
func absModIndex(id int64, length int) int {
if length <= 0 {
return 0
}
idx := id % int64(length)
if idx < 0 {
idx = -idx
}
return int(idx)
}
func writeRSSFile(filename string, data interface{}) error {
if err := os.MkdirAll(pubdef.OutputDir, 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", pubdef.OutputDir, err)
}
path := filepath.Join(pubdef.OutputDir, filename+".tmp")
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("create %s: %w", filename, err)
}
defer file.Close()
if _, err := file.WriteString(xml.Header); err != nil {
return fmt.Errorf("write header %s: %w", filename, err)
}
encoder := xml.NewEncoder(file)
encoder.Indent("", " ")
if err := encoder.Encode(data); err != nil {
return fmt.Errorf("encode %s: %w", filename, err)
}
if err := encoder.Flush(); err != nil {
return fmt.Errorf("flush %s: %w", filename, err)
}
return file.Sync()
}