277 lines
7.8 KiB
Go
277 lines
7.8 KiB
Go
package router
|
|
|
|
import (
|
|
"encoding/json"
|
|
"html/template"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"videoweb/app/api/web"
|
|
"videoweb/app/appg"
|
|
"videoweb/app/service/playletser"
|
|
"videoweb/app/service/seoser"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func Init() *gin.Engine {
|
|
setGinMode()
|
|
engine := gin.Default()
|
|
engine.HandleMethodNotAllowed = true
|
|
setupEngine(engine)
|
|
engine.NoRoute(renderNotFound)
|
|
engine.NoMethod(renderNotFound)
|
|
|
|
registerM3U8Routes(engine)
|
|
registerRoutes(engine)
|
|
return engine
|
|
}
|
|
|
|
// DHInit 导航站独立端口:仅暴露官方导航页及静态资源。
|
|
func DHInit() *gin.Engine {
|
|
setGinMode()
|
|
engine := gin.Default()
|
|
engine.HandleMethodNotAllowed = true
|
|
setupEngine(engine)
|
|
engine.StaticFile("/favicon.ico", "./static/favicon.ico")
|
|
engine.StaticFile("/favicon.svg", "./static/favicon.svg")
|
|
engine.StaticFile("/favicon.png", "./static/favicon.png")
|
|
engine.GET("/ui/dialog/:name", web.DialogFragment)
|
|
engine.GET("/", web.Nav)
|
|
engine.GET("/nav/", web.Nav)
|
|
engine.GET("/dh/", web.Nav)
|
|
engine.NoRoute(renderNotFound)
|
|
engine.NoMethod(renderNotFound)
|
|
return engine
|
|
}
|
|
|
|
func setGinMode() {
|
|
switch strings.ToLower(strings.TrimSpace(appg.Conf.RunMod)) {
|
|
case "test":
|
|
gin.SetMode(gin.TestMode)
|
|
case "prod":
|
|
gin.SetMode(gin.ReleaseMode)
|
|
default:
|
|
gin.SetMode(gin.DebugMode)
|
|
}
|
|
}
|
|
|
|
func setupEngine(engine *gin.Engine) {
|
|
tplFuncs := playletser.TemplateFuncs()
|
|
tplFuncs["InJSONScript"] = inJSONScript
|
|
tplFuncs["jsonStr"] = jsonStr
|
|
engine.SetFuncMap(tplFuncs)
|
|
engine.LoadHTMLFiles(listHTMLTemplates("templates")...)
|
|
engine.Use(staticCacheHeaders(), crawlerHeaders())
|
|
registerBrowserDiscoveryRoutes(engine)
|
|
engine.Static("/static", "./static")
|
|
}
|
|
|
|
// registerBrowserDiscoveryRoutes 处理 Chrome DevTools 的本地工作区探测请求。
|
|
// 该请求并非站点页面,返回 204 可避免进入业务 404 页面和产生误导日志。
|
|
func registerBrowserDiscoveryRoutes(engine *gin.Engine) {
|
|
engine.GET("/.well-known/appspecific/com.chrome.devtools.json", func(c *gin.Context) {
|
|
c.Header("Cache-Control", "public, max-age=86400")
|
|
c.Header("X-Robots-Tag", "noindex, nofollow")
|
|
c.Status(http.StatusNoContent)
|
|
})
|
|
}
|
|
|
|
// staticCacheHeaders 为静态资源设置浏览器缓存,减少重复下载。
|
|
func staticCacheHeaders() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
|
c.Header("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400")
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// crawlerHeaders 防止接口、片段和健康检查等非 HTML 内容进入搜索索引。
|
|
func crawlerHeaders() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
path := c.Request.URL.Path
|
|
if strings.HasPrefix(path, "/api/") ||
|
|
strings.HasPrefix(path, "/ui/") ||
|
|
strings.HasPrefix(path, "/h5/") ||
|
|
path == "/healthz" || path == "/enkey" || path == "/newenkey" {
|
|
c.Header("X-Robots-Tag", "noindex, nofollow")
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// listHTMLTemplates 收集 templates 下页面模板,跳过遗留广告文件。
|
|
func listHTMLTemplates(root string) []string {
|
|
var files []string
|
|
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return err
|
|
}
|
|
name := strings.ToLower(d.Name())
|
|
if !strings.HasSuffix(name, ".html") {
|
|
return nil
|
|
}
|
|
if name == "banners.html" {
|
|
return nil
|
|
}
|
|
files = append(files, path)
|
|
return nil
|
|
})
|
|
if len(files) == 0 {
|
|
panic("no html templates found under " + root)
|
|
}
|
|
return files
|
|
}
|
|
|
|
// inJSONScript 将任意数据序列化为可嵌入 <script> 的 JSON。
|
|
func inJSONScript(v any) template.JS {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return template.JS("null")
|
|
}
|
|
return template.JS(b)
|
|
}
|
|
|
|
// jsonStr 将字符串序列化为可嵌入 application/ld+json 的 JSON 字符串字面量。
|
|
func jsonStr(s string) template.JS {
|
|
b, err := json.Marshal(s)
|
|
if err != nil {
|
|
return template.JS(`""`)
|
|
}
|
|
return template.JS(b)
|
|
}
|
|
|
|
func registerRoutes(r *gin.Engine) {
|
|
r.GET("/", web.Index)
|
|
r.GET("/ui/dialog/:name", web.DialogFragment)
|
|
|
|
r.POST("/api/feedback/add", web.AddFeedback)
|
|
r.POST("/api/contactus/add", web.AddContactUs)
|
|
r.GET("/api/comment/list", web.CommentList)
|
|
r.POST("/api/playlet/forward/heat", web.PlayletForwardHeat)
|
|
r.GET("/api/playlet/chapter/detail", web.ChapterDetail)
|
|
r.GET("/api/playlet/chapter/list", web.ChapterList)
|
|
r.GET("/api/playlet/operate/list", web.OperateList)
|
|
r.GET("/api/playlet/hot-like", web.HotLike)
|
|
r.GET("/api/app/playlet/hot-like", web.HotLike)
|
|
|
|
r.GET("/list/", web.List)
|
|
r.GET("/list/:slug/", web.List)
|
|
r.GET("/list/:slug/:sortOrder/", web.List)
|
|
r.GET("/list/:slug/:sortOrder/page/:page/", web.List)
|
|
|
|
r.GET("/series/", web.Series)
|
|
r.GET("/series/page/:page/", web.Series)
|
|
r.GET("/series/:filter/", web.Series)
|
|
r.GET("/series/:filter/page/:page/", web.Series)
|
|
|
|
// 标签页面使用 tag 路由;tag-list 仅作为历史地址保留并由 handler 301 到 canonical。
|
|
r.GET("/tag/", web.TagList)
|
|
r.GET("/tag/page/:page/", web.TagList)
|
|
r.GET("/tag/:slug/", web.TagList)
|
|
r.GET("/tag/:slug/page/:page/", web.TagList)
|
|
r.GET("/tag-list/", web.TagList)
|
|
r.GET("/tag-list/page/:page/", web.TagList)
|
|
r.GET("/tag-list/:slug/", web.TagList)
|
|
r.GET("/tag-list/:slug/page/:page/", web.TagList)
|
|
|
|
r.GET("/rank/", web.Rank)
|
|
r.GET("/rank/page/:page/", web.Rank)
|
|
r.GET("/rank/:sortOrder/", web.Rank)
|
|
r.GET("/rank/:sortOrder/page/:page/", web.Rank)
|
|
|
|
r.GET("/today-update/", web.TodayUpdate)
|
|
r.GET("/today-update/page/:page/", web.TodayUpdate)
|
|
|
|
// 分类详情使用语义化 category 路由,分类标识优先使用 slug、无 slug 时使用 ID。
|
|
r.GET("/category/:slug/", web.CategoryDetail)
|
|
r.GET("/category/:slug/page/:page/", web.CategoryDetail)
|
|
|
|
// 专题列表和详情统一使用 topic 路由;more 仅作为历史地址保留并由 handler 301 到 canonical。
|
|
r.GET("/topic/", web.ThemeIndex)
|
|
r.GET("/topic/page/:page/", web.ThemeIndex)
|
|
r.GET("/topic/:slug/", web.More)
|
|
r.GET("/topic/:slug/page/:page/", web.More)
|
|
r.GET("/more/", web.More)
|
|
r.GET("/more/page/:page/", web.More)
|
|
r.GET("/more/:slug/", web.More)
|
|
r.GET("/more/:slug/page/:page/", web.More)
|
|
|
|
r.GET("/download-app/", func(c *gin.Context) {
|
|
web.HTMLOK(c, "downloadApp.html", gin.H{
|
|
"title": "下载APP",
|
|
"navActive": "downloadApp",
|
|
})
|
|
})
|
|
|
|
r.GET("/application/", web.Application)
|
|
r.GET("/home-road/", web.HomeRoad)
|
|
|
|
r.GET("/detail/:slug/", web.Detail)
|
|
|
|
r.GET("/play/:slug/", web.Play)
|
|
r.GET("/play/:slug/:chapterId/", web.Play)
|
|
|
|
r.GET("/search/", web.Search)
|
|
r.GET("/search/:keyword/", web.Search)
|
|
r.GET("/search/:keyword/page/:page/", web.Search)
|
|
|
|
r.GET("/about-us/", web.RichtextPage("关于我们"))
|
|
r.GET("/user-agreement/", web.RichtextPage("用户协议"))
|
|
r.GET("/privacy-policy/", web.RichtextPage("隐私政策"))
|
|
r.GET("/help-center/", web.RichtextPage("帮助中心"))
|
|
|
|
r.GET("/richtext/", web.Richtext)
|
|
r.GET("/richtext/:title/", web.Richtext)
|
|
|
|
r.GET("/feedback/", func(c *gin.Context) {
|
|
web.HTMLOK(c, "feedback.html", gin.H{
|
|
"title": "问题反馈",
|
|
"noindex": true,
|
|
"robots": "noindex,nofollow",
|
|
})
|
|
})
|
|
|
|
r.GET("/nav/", web.Nav)
|
|
|
|
r.GET("/healthz", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "ok",
|
|
})
|
|
})
|
|
|
|
registerSitemapRoutes(r)
|
|
r.GET("/robots.txt", web.RobotsTXT)
|
|
registerSEOVerifyRoutes(r)
|
|
|
|
r.GET("/favicon.ico", func(c *gin.Context) {
|
|
c.File("./static/favicon.ico")
|
|
})
|
|
r.GET("/favicon.svg", func(c *gin.Context) {
|
|
c.File("./static/favicon.svg")
|
|
})
|
|
r.GET("/favicon.png", func(c *gin.Context) {
|
|
c.File("./static/favicon.png")
|
|
})
|
|
}
|
|
|
|
func renderNotFound(c *gin.Context) {
|
|
web.HTML(c, http.StatusNotFound, "404.html", gin.H{
|
|
"path": c.Request.URL.Path,
|
|
"noindex": true,
|
|
"robots": "noindex,follow",
|
|
})
|
|
}
|
|
|
|
func registerSEOVerifyRoutes(r *gin.Engine) {
|
|
codes := seoser.VerifyFromConfig()
|
|
if codes.BaiduHTML != "" {
|
|
r.GET("/"+codes.BaiduHTML, web.SEOVerifyFile)
|
|
}
|
|
if codes.IndexNowKey != "" {
|
|
r.GET("/"+codes.IndexNowKey+".txt", web.SEOVerifyFile)
|
|
}
|
|
}
|