Compare commits

..

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

22 changed files with 251 additions and 695 deletions

View File

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

5
config.json Executable file → Normal file
View File

@ -5,6 +5,7 @@
"http://www.ruanyifeng.com/blog/atom.xml",
"https://feeds.appinn.com/appinns/",
"https://v2ex.com/feed/tab/tech.xml",
"https://www.cmooc.com/feed",
"http://www.sciencenet.cn/xml/blog.aspx?di=30",
"https://www.douban.com/feed/review/book",
"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"
],
"refresh": 6,
"autoUpdatePush": 7,
"nightStartTime": "06:30:00",
"nightEndTime": "19:30:00"
"autoUpdatePush": 7
}

BIN
demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 KiB

View File

@ -8,4 +8,6 @@ services:
ports:
- "8080:8080"
volumes:
- "$PWD/index.html:/app/index.html"
- "$PWD/db:/app/db"
- "$PWD/config.json:/app/config.json"

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

View File

@ -1,235 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RSS Reader</title>
<meta name="description" content='RSS Reader一个将订阅信息聚合展示的开源web工具便于了解近期关注的信息同时页面数据数据也实现了自动刷新。'>
<meta name="keywords" content="<< .Keywords >>">
<meta name="anthor" content="srcrs">
<!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> -->
<link rel="stylesheet" href="static/index.css">
<< if .DarkMode >>
<!-- <link rel="stylesheet" href="static/dark-mode.css">-->
<< end >>
<link rel="icon" href="static/favicon.svg" type="image/x-icon">
<style>
body {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 30px;
}
.card-header {
font-size: 15px;
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 {
display: flex;
align-items: center;
text-align: left;
width: 100%;
}
.list-item-title {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
text-align: left;
width: 100%;
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 {
color: black;
text-decoration: none;
}
.feed-col {
margin-bottom: 20px;
}
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h1>
RSS Reader
<< if gt .AutoUpdatePush 0 >>
<span v-show="isPc"><br/>{{ countdown }} s</span>
<< end >>
</h1>
</el-header>
<el-main v-loading.fullscreen.lock="fullscreenLoading" element-loading-text="拼命加载中">
<el-row :gutter="20">
<< range $index, $feed :=.RssDataList >>
<el-col v-if="showSEOFlag" :xs="24" :sm="12" :md="8" :lg="6" :key="index" class="feed-col">
<el-card class="box-card">
<div slot="header" class="card-header">
<span>
<< $feed.Title >>
</span>
</div>
<el-scrollbar style="height: 580px;">
<< range $i, $item :=$feed.Items >>
<el-list key="<< $i >>">
<el-list-item>
<div class="list-item-title">
<span>
<< inc $i >>.
</span>
<el-link href="<< $item.Link >>" target="_blank" title="<< $item.Title >>">
<< $item.Title >>
</el-link>
</div>
</el-list-item>
</el-list>
<< end >>
</el-scrollbar>
<div slot="footer" class="card-footer" style="height: 10px;">
<time class="time">
<< $feed.Custom.lastupdate >>
</time>
</div>
</el-card>
</el-col>
<< end>>
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="(feed, index) in feeds" :key="index" class="feed-col">
<el-card class="box-card">
<div slot="header" class="card-header">
<span>{{ feed.title }}</span>
</div>
<el-scrollbar style="height: 580px;">
<el-list v-for="(item, i) in feed.items" :key="i">
<el-list-item>
<div class="list-item-title">
<span>{{ i+1 }}. </span>
<el-link :href="item.link" target="_blank" :title="item.title">{{ item.title }}</el-link>
</div>
</el-list-item>
</el-list>
</el-scrollbar>
<!-- <div slot="footer" class="card-footer" style="height: 10px;">-->
<!-- <time class="time">{{ feed.custom.lastupdate }}</time>-->
<!-- </div>-->
</el-card>
</el-col>
</el-row>
</el-main>
<el-footer>
<el-link href="https://github.com/srcrs/rss-reader" target="_blank">Rss-reader</el-link>
<span> | </span>
<el-link href="https://github.com/srcrs" target="_blank">By srcrs</el-link>
</el-footer>
</el-container>
</div>
<!-- <script src="https://unpkg.com/vue@next"></script> -->
<script src="static/vue.global.prod.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.3.3/index.full.js"></script> -->
<script src="static/index.full.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
feeds: [],
showSEOFlag: true,
fullscreenLoading: true,
countdown: 60,
isPc: true,
autoUpdatePush: << .AutoUpdatePush >>,
};
},
async created() {
this.fullscreenLoading = false;
// 使用媒体查询判断设备类型
this.isPc = !window.matchMedia('(max-width: 767px)').matches;
},
async mounted() {
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const connect = () => {
const socket = new WebSocket(protocol + window.location.host + "/ws");
socket.onmessage = event => {
const feed = JSON.parse(event.data);
const existingFeed = this.feeds.find(f => f.link === feed.link);
if (existingFeed) {
Object.assign(existingFeed, feed);
} else {
this.feeds.push(feed);
}
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 => {
if (this.isPc && this.autoUpdatePush > 0) {
console.log("WebSocket closed. Reconnecting...");
setInterval(reloadHtml, 3000);
}
};
// Send heartbeat message every 60 seconds
const sendHeartbeat = () => {
if (socket.readyState === WebSocket.OPEN) {
socket.send("heartbeat");
} else if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
reloadHtml()
}
};
if (this.isPc && this.autoUpdatePush > 0) {
setInterval(sendHeartbeat, 60000);
setInterval(() => {
if (this.countdown > 0) {
this.countdown--;
} else {
this.countdown = 60;
}
}, 1000);
}
};
connect();
},
beforeDestroy() {
// 在组件销毁前手动关闭 WebSocket 连接
this.socket.close();
}
});
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

2
go.mod
View File

@ -3,7 +3,6 @@ module rss-reader
go 1.18
require (
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/websocket v1.5.0
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/reflect2 v1.0.2 // 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
)

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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
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/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-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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=

155
index.html Normal file
View File

@ -0,0 +1,155 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RSS Reader</title>
<!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> -->
<link rel="stylesheet" href="static/index.min.css">
<link rel="icon" href="static/favicon.svg" type="image/x-icon">
<style>
body {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 30px;
}
.card-header {
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
font-weight: bold;
}
.list-item {
display: flex;
align-items: center;
text-align: left;
width: 100%;
}
.list-item-title {
display: flex;
/* white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; */
flex-grow: 1;
text-align: left;
width: 100%;
margin-bottom: 10px;
}
a {
color: black;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.feed-col {
margin-bottom: 20px;
}
.time {
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h1>RSS Reader</h1>
</el-header>
<el-main
v-loading.fullscreen.lock="fullscreenLoading"
element-loading-text="拼命加载中"
>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="(feed, index) in feeds" :key="index" class="feed-col">
<el-card class="box-card">
<div slot="header" class="card-header">
<span>{{ feed.title }}</span>
</div>
<el-scrollbar style="height: 300px;">
<el-list v-for="(item, i) in feed.items" :key="i">
<el-list-item>
<div class="list-item-title">
<span>{{ i+1 }}. </span>
<el-link :href="item.link" target="_blank" :title="item.title">{{ item.title }}</el-link>
</div>
</el-list-item>
</el-list>
</el-scrollbar>
<div slot="footer" class="card-footer" style="height: 10px;">
<time class="time">{{ feed.custom.lastupdate }}</time>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
<el-footer>
<el-link href="https://github.com/srcrs/rss-reader" target="_blank">Rss-reader</el-link>
<span> | </span>
<el-link href="https://github.com/srcrs" target="_blank">By srcrs</el-link>
</el-footer>
</el-container>
</div>
<!-- <script src="https://unpkg.com/vue@next"></script> -->
<script src="static/vue.global.prod.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.3.3/index.full.js"></script> -->
<script src="static/index.full.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
feeds: [],
fullscreenLoading: true,
};
},
async mounted() {
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const connect = () => {
const socket = new WebSocket(protocol + window.location.host + "/ws");
socket.onmessage = event => {
const feed = JSON.parse(event.data);
const existingFeed = this.feeds.find(f => f.link === feed.link);
if (existingFeed) {
Object.assign(existingFeed, feed);
} else {
this.feeds.push(feed);
}
this.fullscreenLoading = false;
};
socket.onclose = event => {
console.log("WebSocket closed. Reconnecting...");
setTimeout(connect, 300000);
};
// Send heartbeat message every 120 seconds
const sendHeartbeat = () => {
if (socket.readyState === WebSocket.OPEN) {
socket.send("heartbeat");
}
};
setInterval(sendHeartbeat, 120000);
};
connect();
},
beforeDestroy() {
// 在组件销毁前手动关闭 WebSocket 连接
this.socket.close();
}
});
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

173
main.go
View File

@ -1,111 +1,87 @@
package main
import (
"embed"
"encoding/json"
"html/template"
"io/ioutil"
"log"
"net/http"
"rss-reader/globals"
"rss-reader/models"
"rss-reader/utils"
"sync"
"time"
"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 sync.Map
rssUrls Config
upgrader = websocket.Upgrader{}
//go:embed static
dirStatic embed.FS
//go:embed index.html
fileIndex embed.FS
htmlContent []byte
)
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)
}
}
func main() {
go utils.UpdateFeeds()
go utils.WatchConfigFileChanges("config.json")
go updateFeeds()
http.HandleFunc("/feeds", getFeedsHandler)
http.HandleFunc("/ws", wsHandler)
// http.HandleFunc("/", serveHome)
http.HandleFunc("/", tplHandler)
http.HandleFunc("/", serveHome)
//加载静态文件
fs := http.FileServer(http.FS(globals.DirStatic))
fs := http.FileServer(http.FS(dirStatic))
http.Handle("/static/", fs)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func serveHome(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html; charset=utf-8")
w.Write(globals.HtmlContent)
}
func tplHandler(w http.ResponseWriter, r *http.Request) {
// 创建一个新的模板,并设置自定义分隔符为<< >>避免与Vue的语法冲突
tmplInstance := template.New("index.html").Delims("<<", ">>")
//添加加法函数计数
funcMap := template.FuncMap{
"inc": func(i int) int {
return i + 1
},
}
// 加载模板文件
tmpl, err := tmplInstance.Funcs(funcMap).ParseFS(globals.DirStatic, "static/index.html")
if err != nil {
log.Println("模板加载错误:", err)
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 {
Keywords string
RssDataList []models.Feed
DarkMode bool
AutoUpdatePush int
}{
Keywords: getKeywords(),
RssDataList: utils.GetFeeds(),
DarkMode: darkMode,
AutoUpdatePush: globals.RssUrls.AutoUpdatePush,
}
// 渲染模板并将结果写入响应
err = tmpl.Execute(w, data)
if err != nil {
log.Println("模板渲染错误:", err)
}
w.Write(htmlContent)
}
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 {
log.Printf("Upgrade failed: %v", err)
return
}
defer conn.Close()
for {
for _, url := range globals.RssUrls.Values {
globals.Lock.RLock()
cache, ok := globals.DbMap[url]
globals.Lock.RUnlock()
for _, url := range rssUrls.Values {
feedJSON, ok := dbMap.Load(url)
if !ok {
log.Printf("Error getting feed from db is null %v", url)
continue
}
data, err := json.Marshal(cache)
if err != nil {
log.Printf("json marshal failure: %s", err.Error())
continue
}
err = conn.WriteMessage(websocket.TextMessage, data)
err = conn.WriteMessage(websocket.TextMessage, []byte(feedJSON.(string)))
//错误直接关闭更新
if err != nil {
log.Printf("Error sending message or Connection closed: %v", err)
@ -113,34 +89,55 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
}
}
//如果未配置则不自动更新
if globals.RssUrls.AutoUpdatePush == 0 {
if rssUrls.AutoUpdatePush == 0 {
return
}
time.Sleep(time.Duration(globals.RssUrls.AutoUpdatePush) * time.Minute)
time.Sleep(time.Duration(rssUrls.AutoUpdatePush) * time.Minute)
}
}
//获取关键词也就是title
//获取feeds列表
func getKeywords() string {
words := ""
for _, url := range globals.RssUrls.Values {
globals.Lock.RLock()
cache, ok := globals.DbMap[url]
globals.Lock.RUnlock()
func updateFeeds() {
for {
for _, url := range rssUrls.Values {
fp := gofeed.NewParser()
feed, err := fp.ParseURL(url)
if err != nil {
log.Printf("Error fetching feed: %v | %v", url, err)
continue
}
currentTime := time.Now()
formattedTime := currentTime.Format("2006-01-02 15:04:05")
feed.Custom = map[string]string{"lastupdate": formattedTime}
feedJSON, err := json.Marshal(feed)
if err != nil {
log.Printf("Error marshaling feed: %v", err)
continue
}
dbMap.Store(url, string(feedJSON))
}
time.Sleep(time.Duration(rssUrls.ReFresh) * time.Minute)
}
}
func getFeedsHandler(w http.ResponseWriter, r *http.Request) {
feeds := make([]gofeed.Feed, 0, len(rssUrls.Values))
for _, url := range rssUrls.Values {
feedJSON, ok := dbMap.Load(url)
if !ok {
log.Printf("Error getting feed from db is null %v", url)
continue
}
if cache.Title != "" {
words += cache.Title + ","
}
}
return words
}
func getFeedsHandler(w http.ResponseWriter, r *http.Request) {
feeds := utils.GetFeeds()
var feed gofeed.Feed
if err := json.Unmarshal([]byte(feedJSON.(string)), &feed); err != nil {
log.Printf("Error unmarshaling feed: %v", err)
continue
}
feeds = append(feeds, feed)
}
w.Header().Set("Content-Type", "application/json")
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
}

View File

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

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 {}
}