diff --git a/.gitignore b/.gitignore index 9836d32..8fd5bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ movie server/server server/main __debug_bin +gpt*.txt diff --git a/server/go.mod b/server/go.mod index 2278958..d48190d 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,4 +5,5 @@ go 1.16 require ( github.com/gin-gonic/gin v1.6.3 github.com/giorgisio/goav v0.1.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect ) diff --git a/server/go.sum b/server/go.sum index eeca7a3..2c56172 100644 --- a/server/go.sum +++ b/server/go.sum @@ -15,6 +15,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/server/loginHandler.go b/server/loginHandler.go new file mode 100644 index 0000000..50acc15 --- /dev/null +++ b/server/loginHandler.go @@ -0,0 +1,65 @@ +package main + +import ( + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" +) + +var jwtSecret = []byte("eson1238752372fs") + +func loginHandler(c *gin.Context) { + username := c.PostForm("username") + password := c.PostForm("password") + + // 在这里验证用户名和密码。在此示例中,我们仅检查它们是否为空。 + if username == "" || password == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username and password required"}) + return + } + + if username != "eson" || password != "6601502.." { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username and password error"}) + return + } + + // 创建 JWT + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + claims["username"] = username + claims["exp"] = time.Now().Add(time.Hour * 24 * 7).Unix() + + tokenString, err := token.SignedString(jwtSecret) + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate JWT"}) + return + } + + c.JSON(http.StatusOK, gin.H{"token": tokenString}) +} + +func jwtMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + return + } + + tokenString := authHeader[7:] + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + + if err != nil || !token.Valid { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + return + } + + c.Next() + } +} diff --git a/server/main.go b/server/main.go index 156130d..e622bde 100644 --- a/server/main.go +++ b/server/main.go @@ -1,142 +1,18 @@ package main import ( - "bytes" - "io/fs" - "log" "net/http" - "os/exec" "path/filepath" - "sort" - "strconv" - "strings" "github.com/gin-gonic/gin" ) -// Category 电影分类结构 -type Category struct { - Name string `json:"name"` - Movies []Movie `json:"movies"` -} - -// Movie 电影结构 -type Movie struct { - Name string `json:"filename"` - Image string `json:"image"` - Duration int `json:"duration"` -} - -// var movies []Movie -var categories []Category - -func initMovie() { - // 需要改进 如果存在这个文件的略缩图, 就不存进movieDict里 - var movieDict map[string]string = make(map[string]string) - - matches, err := filepath.Glob("movie/*") - if err != nil { - log.Println(err) - } - - for _, filename := range matches { - base := filepath.Base(filename) - - // ext := filepath.Ext(base) - base = base[:strings.IndexByte(base, '.')] - - if _, ok := movieDict[base]; ok { - delete(movieDict, base) - } else { - movieDict[base] = filename - } - - } - - for key, filename := range movieDict { - // width := 160 - // height := 120 - log.Println(filename) - - cmd := exec.Command("ffmpeg", - "-i", filename, - "-vf", "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,35)',scale=320:180,tile=3x3", - "-frames:v", "1", - "movie/"+key+".png", - ) - - var buffer bytes.Buffer - cmd.Stdout = &buffer - if cmd.Run() != nil { - log.Println(buffer.String()) - panic("could not generate frame") - } - - } - - // 初始化分类 - categories = []Category{ - {Name: "15min", Movies: []Movie{}}, - {Name: "30min", Movies: []Movie{}}, - {Name: "60min", Movies: []Movie{}}, - {Name: "大于60min", Movies: []Movie{}}, - } - - filepath.Walk("./movie", func(path string, info fs.FileInfo, err error) error { - if !info.IsDir() && filepath.Ext(info.Name()) != ".png" { - base := info.Name() - cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=nw=1:nk=1", "./movie/"+info.Name()) - durationOutput, err := cmd.Output() - if err != nil { - log.Printf("Error getting duration for %s: %v", info.Name(), err) - return err - } - - duration, err := strconv.ParseFloat(strings.Trim(string(durationOutput), "\n "), 64) - if err != nil { - log.Printf("Error parsing duration for %s: %v", info.Name(), err) - return err - } - log.Println(path, info.Name()) - - movie := Movie{ - Name: info.Name(), - Image: base[:strings.IndexByte(base, '.')] + ".png", - Duration: int(duration / 60.0), - } - if movie.Duration <= 15 { - categories[0].Movies = append(categories[0].Movies, movie) - } else if movie.Duration <= 30 { - categories[1].Movies = append(categories[1].Movies, movie) - } else if movie.Duration <= 60 { - categories[2].Movies = append(categories[2].Movies, movie) - } else { - categories[3].Movies = append(categories[3].Movies, movie) - } - - } - return nil - }) - - for _, category := range categories { - var movies = category.Movies - sort.Slice(movies, func(i, j int) bool { - return movies[i].Duration < movies[j].Duration - }) - } - - // log.Printf("%##v", categories) -} - func main() { initMovie() eg := gin.Default() eg.Use(Cors()) - eg.Static("/res", "movie/") - - // 添加以下代码以提供frontend静态文件 eg.Static("/static", "../build/static") eg.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path @@ -147,7 +23,11 @@ func main() { } }) + api := eg.Group("/api") + api.POST("/login", loginHandler) + movie := eg.Group("movie") + movie.Use(jwtMiddleware()) movie.GET("/", MovieList) eg.Run(":4444") diff --git a/server/middleware.go b/server/middleware.go index f5677ff..925d775 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -11,16 +11,6 @@ func Cors() gin.HandlerFunc { return func(c *gin.Context) { method := c.Request.Method //请求方法 origin := c.Request.Header.Get("Origin") //请求头部 - // var headerKeys []string // 声明请求头keys - // for k := range c.Request.Header { - // headerKeys = append(headerKeys, k) - // } - // headerStr := strings.Join(headerKeys, ", ") - // if headerStr != "" { - // headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr) - // } else { - // headerStr = "access-control-allow-origin, access-control-allow-headers" - // } if origin != "" { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") @@ -34,7 +24,6 @@ func Cors() gin.HandlerFunc { c.Header("Access-Control-Allow-Credentials", "true") // 跨域请求是否需要带cookie信息 默认设置为true c.Set("content-type", "application/json") // 设置返回格式是json } - //放行所有OPTIONS方法 if method == "OPTIONS" { c.JSON(http.StatusOK, "Options Request!") diff --git a/server/movie.go b/server/movie.go new file mode 100644 index 0000000..6d5a2ad --- /dev/null +++ b/server/movie.go @@ -0,0 +1,126 @@ +package main + +import ( + "bytes" + "io/fs" + "log" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" +) + +// Category 电影分类结构 +type Category struct { + Name string `json:"name"` + Movies []Movie `json:"movies"` +} + +// Movie 电影结构 +type Movie struct { + Name string `json:"filename"` + Image string `json:"image"` + Duration int `json:"duration"` +} + +// var movies []Movie +var categories []Category + +func initMovie() { + // 需要改进 如果存在这个文件的略缩图, 就不存进movieDict里 + var movieDict map[string]string = make(map[string]string) + + matches, err := filepath.Glob("movie/*") + if err != nil { + log.Println(err) + } + + for _, filename := range matches { + base := filepath.Base(filename) + + // ext := filepath.Ext(base) + base = base[:strings.IndexByte(base, '.')] + + if _, ok := movieDict[base]; ok { + delete(movieDict, base) + } else { + movieDict[base] = filename + } + + } + + for key, filename := range movieDict { + // width := 160 + // height := 120 + log.Println(filename) + + cmd := exec.Command("ffmpeg", + "-i", filename, + "-vf", "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,35)',scale=320:180,tile=3x3", + "-frames:v", "1", + "movie/"+key+".png", + ) + + var buffer bytes.Buffer + cmd.Stdout = &buffer + if cmd.Run() != nil { + log.Println(buffer.String()) + panic("could not generate frame") + } + + } + + // 初始化分类 + categories = []Category{ + {Name: "15min", Movies: []Movie{}}, + {Name: "30min", Movies: []Movie{}}, + {Name: "60min", Movies: []Movie{}}, + {Name: "大于60min", Movies: []Movie{}}, + } + + filepath.Walk("./movie", func(path string, info fs.FileInfo, err error) error { + if !info.IsDir() && filepath.Ext(info.Name()) != ".png" { + base := info.Name() + cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=nw=1:nk=1", "./movie/"+info.Name()) + durationOutput, err := cmd.Output() + if err != nil { + log.Printf("Error getting duration for %s: %v", info.Name(), err) + return err + } + + duration, err := strconv.ParseFloat(strings.Trim(string(durationOutput), "\n "), 64) + if err != nil { + log.Printf("Error parsing duration for %s: %v", info.Name(), err) + return err + } + // log.Println(path, info.Name()) + + movie := Movie{ + Name: info.Name(), + Image: base[:strings.IndexByte(base, '.')] + ".png", + Duration: int(duration / 60.0), + } + if movie.Duration <= 15 { + categories[0].Movies = append(categories[0].Movies, movie) + } else if movie.Duration <= 30 { + categories[1].Movies = append(categories[1].Movies, movie) + } else if movie.Duration <= 60 { + categories[2].Movies = append(categories[2].Movies, movie) + } else { + categories[3].Movies = append(categories[3].Movies, movie) + } + + } + return nil + }) + + for _, category := range categories { + var movies = category.Movies + sort.Slice(movies, func(i, j int) bool { + return movies[i].Duration < movies[j].Duration + }) + } + + // log.Printf("%##v", categories) +} diff --git a/src/App.js b/src/App.js index 3b39db7..07c68b2 100644 --- a/src/App.js +++ b/src/App.js @@ -1,44 +1,74 @@ // App.js import React from 'react'; - + import AppBar from '@mui/material/AppBar'; -import Toolbar from '@mui/material/Toolbar'; import IconButton from '@mui/material/IconButton'; +import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; -import VuetifyLogo from './logo.svg'; -import VuetifyName from './logo.svg'; -import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import Main from './Components'; -import VideoPlayer from './VideoPlayer'; // 导入我们将创建的VideoPlayer组件 +import { Route, Router, Routes } from 'react-router-dom'; import ConfigContext, { config } from './Config'; +import LoginForm from './LoginForm'; +import Main from './Main'; +import VideoPlayer from './VideoPlayer'; // 导入我们将创建的VideoPlayer组件 +import { default as VuetifyLogo, default as VuetifyName } from './logo.svg'; + +import axios from 'axios'; +import { useLocation, useNavigate } from 'react-router-dom'; + +axios.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); const App = () => { - return ( + const navigate = useNavigate(); // 将 navigate 声明移动到组件内部 + const location = useLocation(); + + axios.interceptors.response.use( + (response) => { + return response; + }, + + (error) => { + + + if (error.response.status === 401) { + sessionStorage.setItem('previousLocation', location.pathname); // 使用 window.location.pathname 获取当前路径 + navigate('/login'); // 使用 window.location.assign() 进行导航 + } + return Promise.reject(error); + } + ); + + return ( + - -
- - - - Vuetify Logo - - - Vuetify Name - - - - + +
+ + + + Vuetify Logo + + + Vuetify Name + + + +
- } /> - } /> + } /> + } /> + } /> -
-
-
- + + ); }; diff --git a/src/CategoryNav.js b/src/CategoryNav.js index 437499c..d26478c 100644 --- a/src/CategoryNav.js +++ b/src/CategoryNav.js @@ -1,3 +1,4 @@ +// CategoryNav.js import React from 'react'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; diff --git a/src/LoginForm.js b/src/LoginForm.js new file mode 100644 index 0000000..574eac3 --- /dev/null +++ b/src/LoginForm.js @@ -0,0 +1,100 @@ +// LoginForm.js +import React, { useContext, useState } from 'react'; +import axios from 'axios'; +import ConfigContext from './Config'; +import { useNavigate } from 'react-router-dom'; +import { + Box, + Button, + Container, + Grid, + TextField, + Typography, +} from '@mui/material'; + +function LoginForm() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const config = useContext(ConfigContext); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + const response = await axios.post(`${config.Host}/api/login`, new URLSearchParams({ + username, + password, + }), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + localStorage.setItem('token', response.data.token); + + // 如果存在之前的页面路径,则返回,否则跳转到首页 + const previousLocation = sessionStorage.getItem('previousLocation'); + if (previousLocation) { + sessionStorage.removeItem('previousLocation'); + navigate(previousLocation); + } else { + navigate('/'); + } + } catch (error) { + setError('Invalid username or password'); + } + }; + + return ( + + + + 登录 + + {error && ( + + {error} + + )} + + + + setUsername(e.target.value)} + /> + + + setPassword(e.target.value)} + /> + + + + + + + ); +} + +export default LoginForm; \ No newline at end of file diff --git a/src/Components.js b/src/Main.js similarity index 97% rename from src/Components.js rename to src/Main.js index 12705af..8589a46 100644 --- a/src/Components.js +++ b/src/Main.js @@ -1,3 +1,4 @@ +// Main.js import React, { useState, useEffect, useContext } from 'react'; import axios from 'axios'; import Container from '@mui/material/Container'; @@ -54,7 +55,7 @@ const Main = () => { if (response.status === 200) { const data = response.data.data; - console.log(`${config.Host}/movie/?page=${page}&limit=${limit}&category=${category}`); + if (data.items.length === 0 && page > 1) { // 如果返回的数据为空且请求的页码大于1,则尝试获取上一页的数据 fetchMovies(page - 1); @@ -93,11 +94,12 @@ const Main = () => { return ( - +
{ size="large" />
- -
{loading ? (
{
) : ( - - {pagination.movies.map((item) => ( { )}
- - - +
- + + + + ); // If you want to start measuring performance in your app, pass a function