Files
web_hghl_server/app/skd/job/rss/rss.go
T
2026-10-26 16:28:14 +08:00

218 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package rss
import (
"context"
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"videoweb/app/service/configser"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/common/utils"
"videoweb/pub/models/cfg"
"videoweb/pub/models/videoinformationmod"
"videoweb/pub/pubdef"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
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"`
}
// GenerateRSS 生成 rss.xml,输出到 outDir/rss.xml(Web 侧通过 /rss.xml 静态暴露)
func GenerateRSS(ctx context.Context, outDir string) {
generateMu.Lock()
defer generateMu.Unlock()
scfg, srvErr := configser.GetSystemConfig(ctx)
if srvErr != nil || scfg == nil {
log.Error("GenerateRSS GetSystemConfig", log.Any("err", srvErr))
return
}
// 查询最新的已上架视频
filter := bson.M{
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
}
opt := options.Find().
SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1}).
SetLimit(int64(maxRSSItems))
videoList, vsErr := videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
if vsErr != nil {
log.Error("GenerateRSS FindManyVideoInformations", log.Any("err", vsErr))
return
}
items := buildRSSItems(scfg, videoList)
feedURL := utils.JoinUrlPath(scfg.WebAddr, "/rss.xml")
feed := rssFeed{
Version: "2.0",
Atom: "http://www.w3.org/2005/Atom",
Channel: rssChannel{
Title: scfg.WebTitle,
Link: scfg.WebAddr,
Description: scfg.WebDescription,
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(outDir, filename, feed); err != nil {
log.Error("GenerateRSS write", log.Any("err", err))
return
}
// 写入成功,rename .tmp → 正式文件
src := filepath.Join(outDir, filename+".tmp")
dst := filepath.Join(outDir, 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)))
}
// absModIndex 根据 id 确定性选取切片索引,保证结果非负
func absModIndex(id int64, length int) int {
idx := id % int64(length)
if idx < 0 {
idx = -idx
}
return int(idx)
}
func buildRSSItems(scfg *cfg.SysCfg, videoList videoinformationmod.VideoInformationList) []rssItem {
logoURL := utils.JoinUrlPath(scfg.WebAddr, "/static/images/logo.png")
items := make([]rssItem, 0, len(videoList))
for _, v := range videoList {
desc := v.Desc
if desc == "" {
desc = v.Title
}
pageURL := utils.JoinUrlPathKeepSlash(scfg.WebAddr, fmt.Sprintf("/video/detail?id=%d", v.Id))
pubDate := ""
if !v.AddedTime.IsZero() {
pubDate = v.AddedTime.Format(time.RFC1123Z)
}
item := rssItem{
Title: v.Title,
Link: pageURL,
Description: desc,
PubDate: pubDate,
GUID: pageURL,
}
// 封面图优先级:UnencryptedImage > SitemapDefaultImages 确定性选取 > logo
encURL := ""
encType := "image/jpeg"
if v.UnencryptedImage != "" && scfg.UnencryptedCdn != "" {
encURL = netutil.MergeURL(scfg.UnencryptedCdn, v.UnencryptedImage)
} else if len(scfg.SitemapDefaultImages) > 0 && scfg.UnencryptedCdn != "" {
img := scfg.SitemapDefaultImages[absModIndex(v.Id, len(scfg.SitemapDefaultImages))]
encURL = netutil.MergeURL(scfg.UnencryptedCdn, img)
} else {
encURL = logoURL
encType = "image/png"
}
if strings.HasSuffix(encURL, ".png") {
encType = "image/png"
} else if strings.HasSuffix(encURL, ".webp") {
encType = "image/webp"
}
item.Enclosure = &rssEnclosure{
URL: encURL,
Type: encType,
Length: "0",
}
items = append(items, item)
}
return items
}
// writeRSSFile 将数据写入 rssDir/filename.tmp,由调用方负责 rename
func writeRSSFile(rssDir, filename string, data interface{}) error {
if err := os.MkdirAll(rssDir, 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", rssDir, err)
}
path := filepath.Join(rssDir, 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()
}