成功添加重名文件功能
This commit is contained in:
parent
a92d9837aa
commit
728ba79eb6
@ -10,6 +10,98 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 重命名电影的处理函数
|
||||
func PostRename(c *gin.Context) {
|
||||
response := gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"message": "An unexpected error occurred.",
|
||||
}
|
||||
|
||||
// 定义请求结构体
|
||||
type RenameRequest struct {
|
||||
OldName string `json:"old_name" binding:"required"`
|
||||
NewName string `json:"new_name" binding:"required"`
|
||||
}
|
||||
|
||||
var req RenameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response["code"] = http.StatusBadRequest
|
||||
response["message"] = "Invalid request body: " + err.Error()
|
||||
c.JSON(http.StatusBadRequest, response)
|
||||
return
|
||||
}
|
||||
|
||||
oldName := req.OldName
|
||||
newName := req.NewName
|
||||
|
||||
if oldName == "" || newName == "" {
|
||||
response["code"] = http.StatusBadRequest
|
||||
response["message"] = "Old name and new name are required."
|
||||
c.JSON(http.StatusBadRequest, response)
|
||||
return
|
||||
}
|
||||
|
||||
if oldName == newName {
|
||||
response["code"] = http.StatusBadRequest
|
||||
response["message"] = "Old name and new name cannot be the same."
|
||||
c.JSON(http.StatusBadRequest, response)
|
||||
return
|
||||
}
|
||||
|
||||
MovieDictLock.Lock()
|
||||
defer MovieDictLock.Unlock()
|
||||
|
||||
// 检查新名称是否已存在
|
||||
if _, exists := MovieDict[newName]; exists {
|
||||
response["code"] = http.StatusConflict
|
||||
response["message"] = "New name already exists."
|
||||
c.JSON(http.StatusConflict, response)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找旧名称对应的电影
|
||||
movie, exists := MovieDict[oldName]
|
||||
if exists {
|
||||
// 更新电影信息
|
||||
err := movie.RenameVideo(newName) // 重命名视频文件
|
||||
if err != nil {
|
||||
response["code"] = http.StatusInternalServerError
|
||||
response["message"] = "Failed to rename video file."
|
||||
c.JSON(http.StatusInternalServerError, response)
|
||||
return
|
||||
}
|
||||
|
||||
delete(MovieDict, oldName) // 删除旧名称的条目
|
||||
MovieDict[newName] = movie // 添加新名称的条目
|
||||
|
||||
response["code"] = http.StatusOK
|
||||
response["message"] = "Movie renamed successfully."
|
||||
response["data"] = gin.H{"old_name": oldName, "new_name": newName}
|
||||
c.JSON(http.StatusOK, response)
|
||||
log.Printf("Movie renamed from %s to %s successfully", oldName, newName)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果旧名称不存在,返回404错误
|
||||
response["code"] = http.StatusNotFound
|
||||
response["message"] = "Movie not found."
|
||||
c.JSON(http.StatusNotFound, response)
|
||||
log.Printf("Movie rename failed: %s not found", oldName)
|
||||
}
|
||||
|
||||
func GetCategoryList(c *gin.Context) {
|
||||
response := gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"message": "An unexpected error occurred.",
|
||||
"data": []string{},
|
||||
}
|
||||
|
||||
response["code"] = http.StatusOK
|
||||
response["message"] = "Success"
|
||||
response["data"] = Categories
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// MovieList 电影列表
|
||||
func MovieList(c *gin.Context) {
|
||||
response := gin.H{
|
||||
@ -18,27 +110,27 @@ func MovieList(c *gin.Context) {
|
||||
"data": gin.H{"items": []Movie{}, "total": 0},
|
||||
}
|
||||
|
||||
var moviesToPaginate []Movie
|
||||
var moviesToPaginate []*Movie
|
||||
categoryName := c.Query("category")
|
||||
searchQuery := c.Query("search") // 获取搜索参数
|
||||
|
||||
// === 第一步:基础过滤(类别)===
|
||||
if categoryName != "" && categoryName != "最新添加" {
|
||||
// 普通类别:直接过滤
|
||||
for _, m := range movies {
|
||||
for _, m := range Movies {
|
||||
if m.TimeCategory == categoryName {
|
||||
moviesToPaginate = append(moviesToPaginate, m)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 最新添加/所有电影:使用全局movies
|
||||
moviesToPaginate = make([]Movie, len(movies))
|
||||
copy(moviesToPaginate, movies)
|
||||
moviesToPaginate = make([]*Movie, len(Movies))
|
||||
copy(moviesToPaginate, Movies)
|
||||
}
|
||||
|
||||
// === 第二步:搜索过滤 ===
|
||||
if searchQuery != "" {
|
||||
var searchResults []Movie
|
||||
var searchResults []*Movie
|
||||
lowerSearch := strings.ToLower(searchQuery)
|
||||
|
||||
for _, m := range moviesToPaginate {
|
||||
|
@ -32,7 +32,9 @@ func main() {
|
||||
|
||||
movie := eg.Group("movie")
|
||||
movie.Use(jwtMiddleware())
|
||||
movie.GET("/", MovieList)
|
||||
movie.GET("/list/category", GetCategoryList)
|
||||
movie.GET("/list", MovieList)
|
||||
movie.POST("/rename", PostRename)
|
||||
|
||||
eg.Run("0.0.0.0:4444")
|
||||
}
|
||||
|
112
server/movie.go
112
server/movie.go
@ -12,20 +12,71 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Movie 电影信息结构
|
||||
type Movie struct {
|
||||
FileName string `json:"filename"` // 原始文件名(如 "video1.mp4")
|
||||
Image string `json:"image"` // 对应的缩略图文件名(如 "video1.png")
|
||||
Duration int `json:"duration"` // 视频时长(分钟)
|
||||
TimeCategory string `json:"time_category"` // 时长分类(如 "15min", "30min")
|
||||
VideoPath string `json:"-"` // 视频文件完整路径(用于ffmpeg)
|
||||
CreatedTime int64 `json:"created_time"` // 文件创建时间戳
|
||||
FileName string `json:"filename"` // 原始文件名(如 "video1.mp4")
|
||||
Image string `json:"image"` // 对应的缩略图文件名(如 "video1.png")
|
||||
Duration int `json:"duration"` // 视频时长(分钟)
|
||||
TimeCategory string `json:"time_category"` // 时长分类(如 "15min", "30min")
|
||||
VideoPath string `json:"-"` // 视频文件完整路径(用于ffmpeg)
|
||||
CreatedTime int64 `json:"created_time"` // 文件创建时间戳
|
||||
mu sync.Mutex // 保护视频文件的并发访问
|
||||
}
|
||||
|
||||
var movies []Movie // 存储所有电影信息的全局切片
|
||||
var IsRemakePNG = false // 是否重新生成所有PNG缩略图
|
||||
func (m *Movie) RenameVideo(newName string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if newName == m.FileName {
|
||||
return nil // 如果新名称与当前名称相同,则不需要重命名
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
videoExt := filepath.Ext(m.FileName)
|
||||
imageExt := filepath.Ext(m.Image)
|
||||
|
||||
// 获取当前文件的目录
|
||||
dir := filepath.Dir(m.VideoPath)
|
||||
|
||||
// 构建新的文件名(不带扩展名)
|
||||
newBaseName := strings.TrimSuffix(newName, videoExt)
|
||||
|
||||
// 重命名视频文件
|
||||
oldVideoPath := m.VideoPath
|
||||
newVideoPath := filepath.Join(dir, newBaseName+videoExt)
|
||||
if err := os.Rename(oldVideoPath, newVideoPath); err != nil {
|
||||
return fmt.Errorf("重命名视频文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 重命名缩略图文件
|
||||
oldImagePath := filepath.Join(dir, m.Image)
|
||||
newImagePath := filepath.Join(dir, newBaseName+imageExt)
|
||||
if _, err := os.Stat(oldImagePath); err == nil {
|
||||
if err := os.Rename(oldImagePath, newImagePath); err != nil {
|
||||
// 如果缩略图重命名失败,尝试回滚视频文件重命名
|
||||
_ = os.Rename(newVideoPath, oldVideoPath)
|
||||
return fmt.Errorf("重命名缩略图失败: %w", err)
|
||||
}
|
||||
m.Image = newBaseName + imageExt
|
||||
}
|
||||
|
||||
// 更新结构体信息
|
||||
m.VideoPath = newVideoPath
|
||||
m.FileName = newBaseName + videoExt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var Movies []*Movie // 存储所有电影信息的全局切片
|
||||
var MovieDict = make(map[string]*Movie) // 存储需要处理缩略图的视频字典
|
||||
var MovieDictLock sync.Mutex // 保护MovieDict的并发访问
|
||||
|
||||
var IsRemakePNG = false // 是否重新生成所有PNG缩略图
|
||||
var Categories = []string{ // 分类
|
||||
"15min", "30min", "60min", "大于60min", "最新添加"}
|
||||
|
||||
// getVideoDuration 获取视频时长(秒)
|
||||
func getVideoDuration(videoPath string) (float64, error) {
|
||||
@ -131,7 +182,7 @@ func processVideoFile(path string, movieDict map[string]*Movie) error {
|
||||
timeCat := getTimeCategory(durationMin)
|
||||
|
||||
// 创建电影记录
|
||||
movie := Movie{
|
||||
movie := &Movie{
|
||||
FileName: filename,
|
||||
Image: baseName + ".png",
|
||||
Duration: durationMin,
|
||||
@ -139,7 +190,8 @@ func processVideoFile(path string, movieDict map[string]*Movie) error {
|
||||
VideoPath: path,
|
||||
CreatedTime: createdTime,
|
||||
}
|
||||
movies = append(movies, movie)
|
||||
Movies = append(Movies, movie)
|
||||
MovieDict[movie.FileName] = movie // 添加到全局字典
|
||||
|
||||
// 更新缩略图字典
|
||||
if entry, exists := movieDict[baseName]; exists {
|
||||
@ -166,34 +218,6 @@ func getTimeCategory(durationMin int) string {
|
||||
}
|
||||
}
|
||||
|
||||
// sortMovies 对电影列表排序
|
||||
func sortMovies() {
|
||||
categoryOrder := map[string]int{
|
||||
"15min": 0,
|
||||
"30min": 1,
|
||||
"60min": 2,
|
||||
"大于60min": 3,
|
||||
"最新添加": 4, // 新增的"最新添加"类别
|
||||
}
|
||||
|
||||
sort.Slice(movies, func(i, j int) bool {
|
||||
catI := categoryOrder[movies[i].TimeCategory]
|
||||
catJ := categoryOrder[movies[j].TimeCategory]
|
||||
|
||||
if catI != catJ {
|
||||
return catI < catJ
|
||||
}
|
||||
|
||||
// 如果是recent类别,按创建时间倒序排序
|
||||
if movies[i].TimeCategory == "最新添加" {
|
||||
return movies[i].CreatedTime > movies[j].CreatedTime
|
||||
}
|
||||
|
||||
// 其他类别按时长排序
|
||||
return movies[i].Duration < movies[j].Duration
|
||||
})
|
||||
}
|
||||
|
||||
// generateThumbnail 生成单个视频缩略图
|
||||
func generateThumbnail(movie *Movie) {
|
||||
baseName := strings.TrimSuffix(movie.FileName, filepath.Ext(movie.FileName))
|
||||
@ -244,7 +268,7 @@ func initMovie() {
|
||||
|
||||
// 创建需要处理缩略图的视频字典
|
||||
movieDict := createMovieDict(files)
|
||||
movies = make([]Movie, 0) // 重置全局电影列表
|
||||
Movies = make([]*Movie, 0) // 重置全局电影列表
|
||||
|
||||
// 遍历处理每个视频文件
|
||||
err = filepath.WalkDir("./movie", func(path string, d fs.DirEntry, err error) error {
|
||||
@ -274,17 +298,19 @@ func initMovie() {
|
||||
}
|
||||
generateThumbnail(movie)
|
||||
}
|
||||
|
||||
log.Printf("成功初始化电影数据,共找到 %d 个视频文件", len(Movies))
|
||||
}
|
||||
|
||||
// markRecentMovies 标记最近添加的电影
|
||||
func markRecentMovies() {
|
||||
// 首先按创建时间排序
|
||||
sort.Slice(movies, func(i, j int) bool {
|
||||
return movies[i].CreatedTime > movies[j].CreatedTime
|
||||
sort.Slice(Movies, func(i, j int) bool {
|
||||
return Movies[i].CreatedTime > Movies[j].CreatedTime
|
||||
})
|
||||
|
||||
// 标记前20个最新文件为"最新添加"类别
|
||||
for i := 0; i < len(movies) && i < 20; i++ {
|
||||
movies[i].TimeCategory = "最新添加"
|
||||
for i := 0; i < len(Movies) && i < 20; i++ {
|
||||
Movies[i].TimeCategory = "最新添加"
|
||||
}
|
||||
}
|
||||
|
33
src/Main.jsx
33
src/Main.jsx
@ -47,6 +47,9 @@ const Main = () => {
|
||||
const isFirstLoad = useRef(true);
|
||||
const scrollListenerActive = useRef(false);
|
||||
|
||||
|
||||
|
||||
|
||||
// 初始化状态 - 添加搜索相关状态
|
||||
const [params, setParams] = usePersistedState('params', {
|
||||
lastCategory: DEFAULT_CATEGORY,
|
||||
@ -119,7 +122,7 @@ const Main = () => {
|
||||
const isSearch = category === SEARCH_CATEGORY;
|
||||
const isLatest = category === '最新添加';
|
||||
|
||||
let apiUrl = `/movie?page=${page}&limit=${LIMIT}`;
|
||||
let apiUrl = `/movie/list?page=${page}&limit=${LIMIT}`;
|
||||
|
||||
if (isSearch) {
|
||||
// 搜索请求
|
||||
@ -326,6 +329,32 @@ const Main = () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const handleRename = useCallback(async (oldName, newName) => {
|
||||
try {
|
||||
const response = await axios.post('/movie/rename', {
|
||||
old_name: oldName,
|
||||
new_name: newName
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
// 重命名成功后重新加载当前页面数据
|
||||
const category = params.lastCategory;
|
||||
const categoryHistory = params.history[category] || { lastPage: 1 };
|
||||
|
||||
if (category === SEARCH_CATEGORY) {
|
||||
await fetchMovies(category, categoryHistory.lastPage, params.searchQuery);
|
||||
} else {
|
||||
await fetchMovies(category, categoryHistory.lastPage);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.data.message || '重命名失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重命名错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [fetchMovies, params.lastCategory, params.history, params.searchQuery]);
|
||||
|
||||
return (
|
||||
<Container style={{ marginTop: 20 }}>
|
||||
{/* 添加搜索框 */}
|
||||
@ -379,7 +408,7 @@ const Main = () => {
|
||||
style={{ textDecoration: 'none', paddingBottom: 10 }}
|
||||
onClick={handleMovieCardClick}
|
||||
>
|
||||
<MovieCard movie={item} config={config} />
|
||||
<MovieCard movie={item} config={config} onRename={handleRename} />
|
||||
</Link>
|
||||
</Grid>
|
||||
))}
|
||||
|
@ -1,19 +1,78 @@
|
||||
import React from 'react';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
|
||||
import { Card, CardContent, CardMedia, Typography } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardMedia, Typography, IconButton, TextField, Dialog, DialogActions, DialogContent, DialogTitle, Button } from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { styled } from '@mui/system';
|
||||
|
||||
const MovieCard = ({ movie, config }) => {
|
||||
|
||||
const MovieCard = ({ movie, config, onRename }) => {
|
||||
const [newName, setNewName] = useState('');
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
|
||||
const truncateFilename = (filename, maxLength) => {
|
||||
return filename.length > maxLength
|
||||
? filename.substring(0, maxLength - 3) + '...'
|
||||
: filename;
|
||||
};
|
||||
|
||||
|
||||
const handleRenameClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // 添加阻止默认行为
|
||||
const lastDotIndex = movie.filename.lastIndexOf('.');
|
||||
const nameWithoutExt = lastDotIndex === -1
|
||||
? movie.filename
|
||||
: movie.filename.substring(0, lastDotIndex);
|
||||
setNewName(nameWithoutExt);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleDialogClose = (e) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
setOpenDialog(false);
|
||||
};
|
||||
|
||||
const handleRenameSubmit = async (e) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // 添加阻止默认行为
|
||||
}
|
||||
|
||||
if (!newName.trim()) return;
|
||||
|
||||
try {
|
||||
const lastDotIndex = movie.filename.lastIndexOf('.');
|
||||
const extension = lastDotIndex === -1
|
||||
? ''
|
||||
: movie.filename.substring(lastDotIndex);
|
||||
const fullNewName = newName.trim() + extension;
|
||||
|
||||
if (fullNewName === movie.filename) {
|
||||
handleDialogClose();
|
||||
return;
|
||||
}
|
||||
|
||||
await onRename(movie.filename, fullNewName);
|
||||
handleDialogClose();
|
||||
} catch (error) {
|
||||
console.error('重命名失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理输入框回车键提交
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleRenameSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 阻止对话框内容区域的点击事件冒泡
|
||||
const handleDialogContentClick = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const StyledCard = styled(Card)({
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
@ -23,31 +82,84 @@ const MovieCard = ({ movie, config }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledCard>
|
||||
<Card
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 200,
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
transition: 'box-shadow 0.3s ease-in-out',
|
||||
'&:hover': {
|
||||
boxShadow: '0 8px 12px rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="120"
|
||||
image={`${window.location.origin}/res/${movie.image}`}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography>{truncateFilename(movie.filename, 15)}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
时长: {movie.duration} min
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</StyledCard>
|
||||
<>
|
||||
<StyledCard>
|
||||
<Card
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 200,
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
transition: 'box-shadow 0.3s ease-in-out',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleRenameClick}
|
||||
size="small"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
right: 5,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="120"
|
||||
image={`${window.location.origin}/res/${movie.image}`}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography>{truncateFilename(movie.filename, 15)}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
时长: {movie.duration} min
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</StyledCard>
|
||||
|
||||
<Dialog
|
||||
open={openDialog}
|
||||
onClose={handleDialogClose}
|
||||
onClick={handleDialogContentClick} // 阻止对话框点击事件冒泡
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>重命名文件</DialogTitle>
|
||||
<DialogContent onClick={handleDialogContentClick}>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="新文件名"
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onKeyPress={handleKeyPress} // 支持回车键提交
|
||||
helperText="请不要修改文件后缀"
|
||||
onClick={(e) => e.stopPropagation()} // 防止输入框点击冒泡
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions onClick={handleDialogContentClick}>
|
||||
<Button
|
||||
onClick={handleDialogClose}
|
||||
onMouseDown={(e) => e.stopPropagation()} // 防止鼠标按下事件冒泡
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRenameSubmit}
|
||||
onMouseDown={(e) => e.stopPropagation()} // 防止鼠标按下事件冒泡
|
||||
variant="contained"
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user