Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5586f9621 | |||
| 3a4a22587a | |||
|
|
129b3916ab | ||
|
|
9f30853ded | ||
|
|
dcbeeebc20 | ||
|
|
3b2fb607a3 | ||
|
|
a9fdd31a4e | ||
|
|
8796bcc6c7 | ||
|
|
79ab3e511a | ||
|
|
3bf1dc961b | ||
|
|
837f187db7 | ||
|
|
4b968b7c81 | ||
|
|
d0e4e09aab | ||
|
|
40840821cb | ||
|
|
55496722ce | ||
|
|
1580b9197d | ||
|
|
a1d7b02b36 | ||
|
|
762ca0e6ec | ||
|
|
9215669443 | ||
|
|
a42e18b5f5 | ||
|
|
d115310bb9 | ||
|
|
007eae0b9d | ||
|
|
6160450fcf | ||
|
|
57ec9939ab | ||
|
|
c8583357cf | ||
|
|
e3250bf686 | ||
|
|
b743521695 | ||
|
|
fd30dfaedb |
36
README.md
36
README.md
@ -1,12 +1,30 @@
|
|||||||
# 简述
|
# 简述
|
||||||
|
|
||||||
RSS将信息聚合,曾寻找过一些RSS客户端,但觉得都太过于复杂,会需要登陆、保存历史消息、
|
实时展示rss订阅最新消息。
|
||||||
使用缓存加快响应速度,但我想要看到的是,打开页面看到关注网站的即时消息即可(一般通过RSS订阅获取到的数据即是热点),
|
|
||||||
看到有感兴趣的信息,可以跳转过去再详细的了解。
|
## 特性
|
||||||
|
|
||||||
|
- 打包后镜像大小仅有约20MB,通过docker实现一键部署
|
||||||
|
|
||||||
|
- 支持自定义配置页面数据自动刷新
|
||||||
|
|
||||||
|
- 响应式布局,能够兼容不同的屏幕大小
|
||||||
|
|
||||||
|
- 良好的SEO,首次加载使用模版引擎快速展示页面内容
|
||||||
|
|
||||||
|
- 支持添加多个RSS订阅链接
|
||||||
|
|
||||||
|
- 简洁的页面布局,可以查看每个订阅链接最后更新时间
|
||||||
|
|
||||||
|
- 支持夜间模式
|
||||||
|
|
||||||
|
- config.json配置文件支持热更新
|
||||||
|
|
||||||
2023年7月28日,进行了界面改版和升级
|
2023年7月28日,进行了界面改版和升级
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
# 配置文件
|
# 配置文件
|
||||||
|
|
||||||
@ -28,7 +46,9 @@ RSS将信息聚合,曾寻找过一些RSS客户端,但觉得都太过于复
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -37,6 +57,8 @@ RSS将信息聚合,曾寻找过一些RSS客户端,但觉得都太过于复
|
|||||||
values | rss订阅链接(必填)
|
values | rss订阅链接(必填)
|
||||||
refresh | rss订阅更新时间间隔,单位分钟(必填)
|
refresh | rss订阅更新时间间隔,单位分钟(必填)
|
||||||
autoUpdatePush | 自动刷新间隔,默认为0,不开启。效果为前端每autoUpdatePush分钟自动更新页面信息,单位分钟(非必填)
|
autoUpdatePush | 自动刷新间隔,默认为0,不开启。效果为前端每autoUpdatePush分钟自动更新页面信息,单位分钟(非必填)
|
||||||
|
nightStartTime | 日间开始时间 ,如 06:30:00
|
||||||
|
nightEndTime | 日间结束时间,如 19:30:00
|
||||||
|
|
||||||
# 使用方式
|
# 使用方式
|
||||||
|
|
||||||
@ -66,7 +88,7 @@ docker-compose up -d
|
|||||||
|
|
||||||
# nginx反代
|
# nginx反代
|
||||||
|
|
||||||
这里需要注意/ws,若不设置proxy_read_timeout参数,则默认1分钟断开。
|
这里需要注意/ws,若不设置proxy_read_timeout参数,则默认1分钟断开。静态文件增加gzip可以大幅压缩网络传输数据
|
||||||
|
|
||||||
```conf
|
```conf
|
||||||
server {
|
server {
|
||||||
@ -74,6 +96,8 @@ server {
|
|||||||
server_name rss.lass.cc;
|
server_name rss.lass.cc;
|
||||||
ssl_certificate fullchain.cer;
|
ssl_certificate fullchain.cer;
|
||||||
ssl_certificate_key lass.cc.key;
|
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 / {
|
location / {
|
||||||
proxy_pass http://localhost:8080;
|
proxy_pass http://localhost:8080;
|
||||||
}
|
}
|
||||||
|
|||||||
5
config.json
Normal file → Executable file
5
config.json
Normal file → Executable file
@ -5,7 +5,6 @@
|
|||||||
"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",
|
||||||
@ -13,5 +12,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,4 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- "$PWD/index.html:/app/index.html"
|
|
||||||
- "$PWD/db:/app/db"
|
|
||||||
- "$PWD/config.json:/app/config.json"
|
- "$PWD/config.json:/app/config.json"
|
||||||
|
|||||||
39
globals/global.go
Normal file
39
globals/global.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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
1
globals/static/dark-mode.css
Normal file
1
globals/static/dark-mode.css
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 781 B |
114
globals/static/index-1.html
Normal file
114
globals/static/index-1.html
Normal file
File diff suppressed because one or more lines are too long
1
globals/static/index.css
Normal file
1
globals/static/index.css
Normal file
File diff suppressed because one or more lines are too long
235
globals/static/index.html
Normal file
235
globals/static/index.html
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
<!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
2
go.mod
@ -3,6 +3,7 @@ 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
|
||||||
)
|
)
|
||||||
@ -15,5 +16,6 @@ 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
5
go.sum
@ -5,6 +5,8 @@ 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=
|
||||||
@ -29,6 +31,9 @@ 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=
|
||||||
|
|||||||
155
index.html
155
index.html
@ -1,155 +0,0 @@
|
|||||||
<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>
|
|
||||||
171
main.go
171
main.go
@ -1,87 +1,111 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"rss-reader/globals"
|
||||||
|
"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 sync.Map
|
|
||||||
rssUrls Config
|
|
||||||
upgrader = websocket.Upgrader{}
|
|
||||||
|
|
||||||
//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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
go updateFeeds()
|
go utils.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)
|
||||||
|
|
||||||
//加载静态文件
|
//加载静态文件
|
||||||
fs := http.FileServer(http.FS(dirStatic))
|
fs := http.FileServer(http.FS(globals.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(htmlContent)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := globals.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
|
||||||
}
|
}
|
||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
for {
|
for {
|
||||||
for _, url := range rssUrls.Values {
|
for _, url := range globals.RssUrls.Values {
|
||||||
feedJSON, ok := dbMap.Load(url)
|
globals.Lock.RLock()
|
||||||
|
cache, ok := globals.DbMap[url]
|
||||||
|
globals.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
|
||||||
}
|
}
|
||||||
err = conn.WriteMessage(websocket.TextMessage, []byte(feedJSON.(string)))
|
data, err := json.Marshal(cache)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("json marshal failure: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
//错误直接关闭更新
|
//错误直接关闭更新
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error sending message or Connection closed: %v", err)
|
log.Printf("Error sending message or Connection closed: %v", err)
|
||||||
@ -89,55 +113,34 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//如果未配置则不自动更新
|
//如果未配置则不自动更新
|
||||||
if rssUrls.AutoUpdatePush == 0 {
|
if globals.RssUrls.AutoUpdatePush == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(rssUrls.AutoUpdatePush) * time.Minute)
|
time.Sleep(time.Duration(globals.RssUrls.AutoUpdatePush) * time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFeeds() {
|
//获取关键词也就是title
|
||||||
for {
|
//获取feeds列表
|
||||||
for _, url := range rssUrls.Values {
|
func getKeywords() string {
|
||||||
fp := gofeed.NewParser()
|
words := ""
|
||||||
feed, err := fp.ParseURL(url)
|
for _, url := range globals.RssUrls.Values {
|
||||||
if err != nil {
|
globals.Lock.RLock()
|
||||||
log.Printf("Error fetching feed: %v | %v", url, err)
|
cache, ok := globals.DbMap[url]
|
||||||
continue
|
globals.Lock.RUnlock()
|
||||||
}
|
|
||||||
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 {
|
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
|
||||||
}
|
}
|
||||||
|
if cache.Title != "" {
|
||||||
var feed gofeed.Feed
|
words += cache.Title + ","
|
||||||
if err := json.Unmarshal([]byte(feedJSON.(string)), &feed); err != nil {
|
|
||||||
log.Printf("Error unmarshaling feed: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
feeds = append(feeds, feed)
|
|
||||||
}
|
}
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFeedsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
feeds := utils.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)
|
||||||
|
|||||||
BIN
mobile.png
Normal file
BIN
mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
45
models/config.go
Normal file
45
models/config.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
14
models/feed.go
Normal file
14
models/feed.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
117
utils/feed.go
Normal file
117
utils/feed.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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 {}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user