Compare commits

..

No commits in common. "master" and "v1.5" have entirely different histories.
master ... v1.5

20 changed files with 162 additions and 467 deletions

View File

@ -1,30 +1,12 @@
# 简述 # 简述
实时展示rss订阅最新消息。 RSS将信息聚合曾寻找过一些RSS客户端但觉得都太过于复杂会需要登陆、保存历史消息、
使用缓存加快响应速度但我想要看到的是打开页面看到关注网站的即时消息即可一般通过RSS订阅获取到的数据即是热点
## 特性 看到有感兴趣的信息,可以跳转过去再详细的了解。
- 打包后镜像大小仅有约20MB通过docker实现一键部署
- 支持自定义配置页面数据自动刷新
- 响应式布局,能够兼容不同的屏幕大小
- 良好的SEO首次加载使用模版引擎快速展示页面内容
- 支持添加多个RSS订阅链接
- 简洁的页面布局,可以查看每个订阅链接最后更新时间
- 支持夜间模式
- config.json配置文件支持热更新
2023年7月28日进行了界面改版和升级 2023年7月28日进行了界面改版和升级
![](pc.png) ![](demo.png)
![](mobile.png)
# 配置文件 # 配置文件
@ -46,9 +28,7 @@
"https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg" "https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg"
], ],
"refresh": 6, "refresh": 6,
"autoUpdatePush": 7, "autoUpdatePush": 7
"nightStartTime": "06:30:00",
"nightEndTime": "19:30:00"
} }
``` ```
@ -57,8 +37,6 @@
values | rss订阅链接必填 values | rss订阅链接必填
refresh | rss订阅更新时间间隔单位分钟必填 refresh | rss订阅更新时间间隔单位分钟必填
autoUpdatePush | 自动刷新间隔默认为0不开启。效果为前端每autoUpdatePush分钟自动更新页面信息单位分钟非必填 autoUpdatePush | 自动刷新间隔默认为0不开启。效果为前端每autoUpdatePush分钟自动更新页面信息单位分钟非必填
nightStartTime | 日间开始时间 ,如 06:30:00
nightEndTime | 日间结束时间,如 19:30:00
# 使用方式 # 使用方式

5
config.json Executable file → Normal file
View File

@ -5,6 +5,7 @@
"http://www.ruanyifeng.com/blog/atom.xml", "http://www.ruanyifeng.com/blog/atom.xml",
"https://feeds.appinn.com/appinns/", "https://feeds.appinn.com/appinns/",
"https://v2ex.com/feed/tab/tech.xml", "https://v2ex.com/feed/tab/tech.xml",
"https://www.cmooc.com/feed",
"http://www.sciencenet.cn/xml/blog.aspx?di=30", "http://www.sciencenet.cn/xml/blog.aspx?di=30",
"https://www.douban.com/feed/review/book", "https://www.douban.com/feed/review/book",
"https://www.douban.com/feed/review/movie", "https://www.douban.com/feed/review/movie",
@ -12,7 +13,5 @@
"https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg" "https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg"
], ],
"refresh": 6, "refresh": 6,
"autoUpdatePush": 7, "autoUpdatePush": 7
"nightStartTime": "06:30:00",
"nightEndTime": "19:30:00"
} }

BIN
demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 KiB

View File

@ -1,13 +1,13 @@
package models package main
type Feed struct { type feed struct {
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Link string `json:"link"` Link string `json:"link"`
Custom map[string]string `json:"custom,omitempty"` Custom map[string]string `json:"custom,omitempty"`
Items []Item `json:"items,omitempty"` Items []item `json:"items,omitempty"`
} }
type Item struct { type item struct {
Title string `json:"title"` Title string `json:"title"`
Link string `json:"link"` Link string `json:"link"`
Description string `json:"description"` Description string `json:"description"`

View File

@ -1,39 +0,0 @@
package globals
import (
"embed"
"rss-reader/models"
"sync"
"github.com/gorilla/websocket"
"github.com/mmcdole/gofeed"
)
var (
DbMap map[string]models.Feed
RssUrls models.Config
Upgrader = websocket.Upgrader{}
Lock sync.RWMutex
//go:embed static
DirStatic embed.FS
HtmlContent []byte
Fp = gofeed.NewParser()
)
func Init() {
conf, err := models.ParseConf()
if err != nil {
panic(err)
}
RssUrls = conf
// 读取 index.html 内容
HtmlContent, err = DirStatic.ReadFile("static/index.html")
if err != nil {
panic(err)
}
DbMap = make(map[string]models.Feed)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
go.mod
View File

@ -3,7 +3,6 @@ module rss-reader
go 1.18 go 1.18
require ( require (
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/mmcdole/gofeed v1.2.1 github.com/mmcdole/gofeed v1.2.1
) )
@ -16,6 +15,5 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.4.0 // indirect golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect
) )

5
go.sum
View File

@ -5,8 +5,6 @@ github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEq
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@ -31,9 +29,6 @@ golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=

View File

@ -8,10 +8,7 @@
<meta name="keywords" content="<< .Keywords >>"> <meta name="keywords" content="<< .Keywords >>">
<meta name="anthor" content="srcrs"> <meta name="anthor" content="srcrs">
<!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> --> <!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> -->
<link rel="stylesheet" href="static/index.css"> <link rel="stylesheet" href="static/index.min.css">
<< if .DarkMode >>
<!-- <link rel="stylesheet" href="static/dark-mode.css">-->
<< end >>
<link rel="icon" href="static/favicon.svg" type="image/x-icon"> <link rel="icon" href="static/favicon.svg" type="image/x-icon">
<style> <style>
body { body {
@ -24,18 +21,11 @@
} }
.card-header { .card-header {
font-size: 15px; display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
font-weight: bold; font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
margin-bottom: 10px;
background: linear-gradient(to bottom, #007f80, #007070);
color: white;
line-height: 2em;
border-radius: 6px;
padding: 0 0.5em;
} }
.list-item { .list-item {
@ -46,32 +36,32 @@
} }
.list-item-title { .list-item-title {
display: block; display: flex;
white-space: nowrap; /* white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis; */
flex-grow: 1; flex-grow: 1;
text-align: left; text-align: left;
width: 100%; width: 100%;
margin-bottom: 10px; margin-bottom: 10px;
font-size: 15px;
align-items: center; /* 确保内容垂直居中 */
}
.title-link {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
} }
a { a {
color: black; color: black;
text-decoration: none; text-decoration: none;
} }
a:hover {
text-decoration: underline;
}
.feed-col { .feed-col {
margin-bottom: 20px; margin-bottom: 20px;
} }
.time {
font-size: 12px;
color: #999;
}
</style> </style>
</head> </head>
@ -79,12 +69,7 @@
<div id="app"> <div id="app">
<el-container> <el-container>
<el-header> <el-header>
<h1> <h1>RSS Reader</h1>
RSS Reader
<< if gt .AutoUpdatePush 0 >>
<span v-show="isPc"><br/>{{ countdown }} s</span>
<< end >>
</h1>
</el-header> </el-header>
<el-main v-loading.fullscreen.lock="fullscreenLoading" element-loading-text="拼命加载中"> <el-main v-loading.fullscreen.lock="fullscreenLoading" element-loading-text="拼命加载中">
<el-row :gutter="20"> <el-row :gutter="20">
@ -96,7 +81,7 @@
<< $feed.Title >> << $feed.Title >>
</span> </span>
</div> </div>
<el-scrollbar style="height: 580px;"> <el-scrollbar style="height: 300px;">
<< range $i, $item :=$feed.Items >> << range $i, $item :=$feed.Items >>
<el-list key="<< $i >>"> <el-list key="<< $i >>">
<el-list-item> <el-list-item>
@ -125,7 +110,7 @@
<div slot="header" class="card-header"> <div slot="header" class="card-header">
<span>{{ feed.title }}</span> <span>{{ feed.title }}</span>
</div> </div>
<el-scrollbar style="height: 580px;"> <el-scrollbar style="height: 300px;">
<el-list v-for="(item, i) in feed.items" :key="i"> <el-list v-for="(item, i) in feed.items" :key="i">
<el-list-item> <el-list-item>
<div class="list-item-title"> <div class="list-item-title">
@ -135,9 +120,9 @@
</el-list-item> </el-list-item>
</el-list> </el-list>
</el-scrollbar> </el-scrollbar>
<!-- <div slot="footer" class="card-footer" style="height: 10px;">--> <div slot="footer" class="card-footer" style="height: 10px;">
<!-- <time class="time">{{ feed.custom.lastupdate }}</time>--> <time class="time">{{ feed.custom.lastupdate }}</time>
<!-- </div>--> </div>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@ -162,15 +147,10 @@
feeds: [], feeds: [],
showSEOFlag: true, showSEOFlag: true,
fullscreenLoading: true, fullscreenLoading: true,
countdown: 60,
isPc: true,
autoUpdatePush: << .AutoUpdatePush >>,
}; };
}, },
async created() { async created() {
this.fullscreenLoading = false; this.fullscreenLoading = false;
// 使用媒体查询判断设备类型
this.isPc = !window.matchMedia('(max-width: 767px)').matches;
}, },
async mounted() { async mounted() {
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
@ -186,39 +166,17 @@
} }
this.showSEOFlag = false; this.showSEOFlag = false;
}; };
const reloadHtml = () => {
if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
if (document.visibilityState === 'visible') {
// 刷新网页
console.log("reload...")
location.reload();
}
}
}
socket.onclose = event => { socket.onclose = event => {
if (this.isPc && this.autoUpdatePush > 0) { console.log("WebSocket closed. Reconnecting...");
console.log("WebSocket closed. Reconnecting..."); setTimeout(connect, 300000);
setInterval(reloadHtml, 3000);
}
}; };
// Send heartbeat message every 60 seconds // Send heartbeat message every 120 seconds
const sendHeartbeat = () => { const sendHeartbeat = () => {
if (socket.readyState === WebSocket.OPEN) { if (socket.readyState === WebSocket.OPEN) {
socket.send("heartbeat"); socket.send("heartbeat");
} else if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
reloadHtml()
} }
}; };
if (this.isPc && this.autoUpdatePush > 0) { setInterval(sendHeartbeat, 120000);
setInterval(sendHeartbeat, 60000);
setInterval(() => {
if (this.countdown > 0) {
this.countdown--;
} else {
this.countdown = 60;
}
}, 1000);
}
}; };
connect(); connect();
}, },

162
main.go
View File

@ -1,40 +1,75 @@
package main package main
import ( import (
"embed"
"encoding/json" "encoding/json"
"html/template" "html/template"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"rss-reader/globals" "sync"
"rss-reader/models"
"rss-reader/utils"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/mmcdole/gofeed"
)
type Config struct {
Values []string `json:"values"`
ReFresh int `json:"refresh"`
AutoUpdatePush int `json:"autoUpdatePush"`
}
var (
dbMap map[string]feed
rssUrls Config
upgrader = websocket.Upgrader{}
lock sync.RWMutex
//go:embed static
dirStatic embed.FS
//go:embed index.html
fileIndex embed.FS
htmlContent []byte
) )
func init() { func init() {
globals.Init() // 读取配置文件
data, err := ioutil.ReadFile("config.json")
if err != nil {
panic(err)
}
// 解析JSON数据到Config结构体
err = json.Unmarshal(data, &rssUrls)
if err != nil {
panic(err)
}
// 读取 index.html 内容
htmlContent, err = fileIndex.ReadFile("index.html")
if err != nil {
panic(err)
}
dbMap = make(map[string]feed)
} }
func main() { func main() {
go utils.UpdateFeeds() go updateFeeds()
go utils.WatchConfigFileChanges("config.json")
http.HandleFunc("/feeds", getFeedsHandler) http.HandleFunc("/feeds", getFeedsHandler)
http.HandleFunc("/ws", wsHandler) http.HandleFunc("/ws", wsHandler)
// http.HandleFunc("/", serveHome) // http.HandleFunc("/", serveHome)
http.HandleFunc("/", tplHandler) http.HandleFunc("/", tplHandler)
//加载静态文件 //加载静态文件
fs := http.FileServer(http.FS(globals.DirStatic)) fs := http.FileServer(http.FS(dirStatic))
http.Handle("/static/", fs) http.Handle("/static/", fs)
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }
func serveHome(w http.ResponseWriter, r *http.Request) { func serveHome(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html; charset=utf-8") w.Header().Add("Content-Type", "text/html; charset=utf-8")
w.Write(globals.HtmlContent) w.Write(htmlContent)
} }
func tplHandler(w http.ResponseWriter, r *http.Request) { func tplHandler(w http.ResponseWriter, r *http.Request) {
@ -47,32 +82,19 @@ func tplHandler(w http.ResponseWriter, r *http.Request) {
}, },
} }
// 加载模板文件 // 加载模板文件
tmpl, err := tmplInstance.Funcs(funcMap).ParseFS(globals.DirStatic, "static/index.html") tmpl, err := tmplInstance.Funcs(funcMap).ParseFS(fileIndex, "index.html")
if err != nil { if err != nil {
log.Println("模板加载错误:", err) log.Println("模板加载错误:", err)
return return
} }
//判断现在是否是夜间
formattedTime := time.Now().Format("15:04:05")
darkMode := false
if globals.RssUrls.NightStartTime != "" && globals.RssUrls.NightEndTime != "" {
if globals.RssUrls.NightStartTime > formattedTime || formattedTime > globals.RssUrls.NightEndTime {
darkMode = true
}
}
// 定义一个数据对象 // 定义一个数据对象
data := struct { data := struct {
Keywords string Keywords string
RssDataList []models.Feed RssDataList []feed
DarkMode bool
AutoUpdatePush int
}{ }{
Keywords: getKeywords(), Keywords: getKeywords(),
RssDataList: utils.GetFeeds(), RssDataList: getFeeds(),
DarkMode: darkMode,
AutoUpdatePush: globals.RssUrls.AutoUpdatePush,
} }
// 渲染模板并将结果写入响应 // 渲染模板并将结果写入响应
@ -83,7 +105,7 @@ func tplHandler(w http.ResponseWriter, r *http.Request) {
} }
func wsHandler(w http.ResponseWriter, r *http.Request) { func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := globals.Upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Printf("Upgrade failed: %v", err) log.Printf("Upgrade failed: %v", err)
return return
@ -91,10 +113,10 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
defer conn.Close() defer conn.Close()
for { for {
for _, url := range globals.RssUrls.Values { for _, url := range rssUrls.Values {
globals.Lock.RLock() lock.RLock()
cache, ok := globals.DbMap[url] cache, ok := dbMap[url]
globals.Lock.RUnlock() lock.RUnlock()
if !ok { if !ok {
log.Printf("Error getting feed from db is null %v", url) log.Printf("Error getting feed from db is null %v", url)
continue continue
@ -113,21 +135,83 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
//如果未配置则不自动更新 //如果未配置则不自动更新
if globals.RssUrls.AutoUpdatePush == 0 { if rssUrls.AutoUpdatePush == 0 {
return return
} }
time.Sleep(time.Duration(globals.RssUrls.AutoUpdatePush) * time.Minute) time.Sleep(time.Duration(rssUrls.AutoUpdatePush) * time.Minute)
} }
} }
func updateFeeds() {
var (
tick = time.Tick(time.Duration(rssUrls.ReFresh) * time.Minute)
fp = gofeed.NewParser()
)
for {
formattedTime := time.Now().Format("2006-01-02 15:04:05")
for _, url := range rssUrls.Values {
go updateFeed(fp, url, formattedTime)
}
<-tick
}
}
func updateFeed(fp *gofeed.Parser, url, formattedTime string) {
result, err := fp.ParseURL(url)
if err != nil {
log.Printf("Error fetching feed: %v | %v", url, err)
return
}
//feed内容无更新时无需更新缓存
if cache, ok := dbMap[url]; ok &&
len(result.Items) > 0 &&
len(cache.Items) > 0 &&
result.Items[0].Link == cache.Items[0].Link {
return
}
customFeed := feed{
Title: result.Title,
Link: result.Link,
Custom: map[string]string{"lastupdate": formattedTime},
Items: make([]item, 0, len(result.Items)),
}
for _, v := range result.Items {
customFeed.Items = append(customFeed.Items, item{
Link: v.Link,
Title: v.Title,
Description: v.Description,
})
}
lock.Lock()
defer lock.Unlock()
dbMap[url] = customFeed
}
//获取feeds列表
func getFeeds() []feed {
feeds := make([]feed, 0, len(rssUrls.Values))
for _, url := range rssUrls.Values {
lock.RLock()
cache, ok := dbMap[url]
lock.RUnlock()
if !ok {
log.Printf("Error getting feed from db is null %v", url)
continue
}
feeds = append(feeds, cache)
}
return feeds
}
//获取关键词也就是title //获取关键词也就是title
//获取feeds列表 //获取feeds列表
func getKeywords() string { func getKeywords() string {
words := "" words := ""
for _, url := range globals.RssUrls.Values { for _, url := range rssUrls.Values {
globals.Lock.RLock() lock.RLock()
cache, ok := globals.DbMap[url] cache, ok := dbMap[url]
globals.Lock.RUnlock() lock.RUnlock()
if !ok { if !ok {
log.Printf("Error getting feed from db is null %v", url) log.Printf("Error getting feed from db is null %v", url)
continue continue
@ -140,7 +224,7 @@ func getKeywords() string {
} }
func getFeedsHandler(w http.ResponseWriter, r *http.Request) { func getFeedsHandler(w http.ResponseWriter, r *http.Request) {
feeds := utils.GetFeeds() feeds := getFeeds()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(feeds) json.NewEncoder(w).Encode(feeds)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

View File

@ -1,45 +0,0 @@
package models
import (
"encoding/json"
"os"
)
func ParseConf() (Config, error) {
var conf Config
data, err := os.ReadFile("config.json")
if err != nil {
return conf, err
}
// 解析JSON数据到Config结构体
err = json.Unmarshal(data, &conf)
return conf, err
}
type Config struct {
Values []string `json:"values"`
ReFresh int `json:"refresh"`
AutoUpdatePush int `json:"autoUpdatePush"`
NightStartTime string `json:"nightStartTime"`
NightEndTime string `json:"nightEndTime"`
}
func (older Config) GetIncrement(newer Config) []string {
var (
urlMap = make(map[string]struct{})
increment = make([]string, 0, len(newer.Values))
)
for _, item := range older.Values {
urlMap[item] = struct{}{}
}
for _, item := range newer.Values {
if _, ok := urlMap[item]; ok {
continue
}
increment = append(increment, item)
}
return increment
}

BIN
pc.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 KiB

View File

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 781 B

File diff suppressed because one or more lines are too long

View File

@ -1,117 +0,0 @@
package utils
import (
"log"
"rss-reader/globals"
"rss-reader/models"
"time"
"github.com/fsnotify/fsnotify"
)
func UpdateFeeds() {
var (
tick = time.Tick(time.Duration(globals.RssUrls.ReFresh) * time.Minute)
)
for {
formattedTime := time.Now().Format("2006-01-02 15:04:05")
for _, url := range globals.RssUrls.Values {
go UpdateFeed(url, formattedTime)
}
<-tick
}
}
func UpdateFeed(url, formattedTime string) {
result, err := globals.Fp.ParseURL(url)
if err != nil {
log.Printf("Error fetching feed: %v | %v", url, err)
return
}
//feed内容无更新时无需更新缓存
if cache, ok := globals.DbMap[url]; ok &&
len(result.Items) > 0 &&
len(cache.Items) > 0 &&
result.Items[0].Link == cache.Items[0].Link {
return
}
customFeed := models.Feed{
Title: result.Title,
Link: result.Link,
Custom: map[string]string{"lastupdate": formattedTime},
Items: make([]models.Item, 0, len(result.Items)),
}
for _, v := range result.Items {
customFeed.Items = append(customFeed.Items, models.Item{
Link: v.Link,
Title: v.Title,
Description: v.Description,
})
}
globals.Lock.Lock()
defer globals.Lock.Unlock()
globals.DbMap[url] = customFeed
}
//获取feeds列表
func GetFeeds() []models.Feed {
feeds := make([]models.Feed, 0, len(globals.RssUrls.Values))
for _, url := range globals.RssUrls.Values {
globals.Lock.RLock()
cache, ok := globals.DbMap[url]
globals.Lock.RUnlock()
if !ok {
log.Printf("Error getting feed from db is null %v", url)
continue
}
feeds = append(feeds, cache)
}
return feeds
}
func WatchConfigFileChanges(filePath string) {
// 创建一个新的监控器
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// 添加要监控的文件
err = watcher.Add(filePath)
if err != nil {
log.Fatal(err)
}
// 启动一个 goroutine 来处理文件变化事件
go func() {
for {
time.Sleep(7 * time.Second)
select {
case event, ok := <-watcher.Events:
if !ok {
log.Println("通道关闭1")
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("文件已修改")
globals.Init()
formattedTime := time.Now().Format("2006-01-02 15:04:05")
for _, url := range globals.RssUrls.Values {
go UpdateFeed(url, formattedTime)
}
}
case err, ok := <-watcher.Errors:
if !ok {
log.Println("通道关闭2")
return
}
log.Println("错误:", err)
return
}
}
}()
select {}
}