最新分类版本
This commit is contained in:
parent
fd0bdd7c12
commit
fdf7493195
|
@ -19,28 +19,46 @@ func MovieList(c *gin.Context) {
|
||||||
}
|
}
|
||||||
defer c.JSON(0, response)
|
defer c.JSON(0, response)
|
||||||
|
|
||||||
|
category := c.Query("category")
|
||||||
|
|
||||||
|
var movies []Movie
|
||||||
|
if category != "" {
|
||||||
|
cateidx, err := strconv.Atoi(category)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
movies = categories[cateidx].Movies
|
||||||
|
}
|
||||||
|
|
||||||
var page int = 0
|
var page int = 0
|
||||||
spage := c.Query("page")
|
spage := c.Query("page")
|
||||||
if spage != "" {
|
if spage != "" {
|
||||||
p, err := strconv.Atoi(spage)
|
p, err := strconv.Atoi(spage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
page = p - 1
|
page = p - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var limit int = 10
|
var limit int = 12
|
||||||
slimit := c.Query("limit")
|
slimit := c.Query("limit")
|
||||||
if slimit != "" {
|
if slimit != "" {
|
||||||
l, err := strconv.Atoi(slimit)
|
l, err := strconv.Atoi(slimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
limit = l
|
limit = l
|
||||||
}
|
}
|
||||||
|
|
||||||
start := page * limit
|
start := page * limit
|
||||||
var end int = start + limit
|
var end int = start + limit
|
||||||
|
|
||||||
|
if start > len(movies) {
|
||||||
|
start = len(movies)
|
||||||
|
}
|
||||||
if end > len(movies) {
|
if end > len(movies) {
|
||||||
end = len(movies)
|
end = len(movies)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,20 +7,31 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Category 电影分类结构
|
||||||
|
type Category struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Movies []Movie `json:"movies"`
|
||||||
|
}
|
||||||
|
|
||||||
// Movie 电影结构
|
// Movie 电影结构
|
||||||
type Movie struct {
|
type Movie struct {
|
||||||
Name string `json:"filename"`
|
Name string `json:"filename"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var movies []Movie
|
// var movies []Movie
|
||||||
|
var categories []Category
|
||||||
|
|
||||||
func initMovie() {
|
func initMovie() {
|
||||||
|
// 需要改进 如果存在这个文件的略缩图, 就不存进movieDict里
|
||||||
var movieDict map[string]string = make(map[string]string)
|
var movieDict map[string]string = make(map[string]string)
|
||||||
|
|
||||||
matches, err := filepath.Glob("movie/*")
|
matches, err := filepath.Glob("movie/*")
|
||||||
|
@ -31,7 +42,7 @@ func initMovie() {
|
||||||
for _, filename := range matches {
|
for _, filename := range matches {
|
||||||
base := filepath.Base(filename)
|
base := filepath.Base(filename)
|
||||||
|
|
||||||
ext := filepath.Ext(base)
|
// ext := filepath.Ext(base)
|
||||||
base = base[:strings.IndexByte(base, '.')]
|
base = base[:strings.IndexByte(base, '.')]
|
||||||
|
|
||||||
if _, ok := movieDict[base]; ok {
|
if _, ok := movieDict[base]; ok {
|
||||||
|
@ -40,7 +51,6 @@ func initMovie() {
|
||||||
movieDict[base] = filename
|
movieDict[base] = filename
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(base, ext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, filename := range movieDict {
|
for key, filename := range movieDict {
|
||||||
|
@ -48,7 +58,6 @@ func initMovie() {
|
||||||
// height := 120
|
// height := 120
|
||||||
log.Println(filename)
|
log.Println(filename)
|
||||||
|
|
||||||
// cmd := exec.Command("ffmpeg", "-i", filename, "-vframes", "1", "-s", fmt.Sprintf("%dx%d", width, height), "movie/"+key+".png")
|
|
||||||
cmd := exec.Command("ffmpeg",
|
cmd := exec.Command("ffmpeg",
|
||||||
"-i", filename,
|
"-i", filename,
|
||||||
"-vf", "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,35)',scale=320:180,tile=3x3",
|
"-vf", "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,35)',scale=320:180,tile=3x3",
|
||||||
|
@ -63,25 +72,60 @@ func initMovie() {
|
||||||
panic("could not generate frame")
|
panic("could not generate frame")
|
||||||
}
|
}
|
||||||
|
|
||||||
// err := generateThumbnail(filename, "movie/"+key+".png", width, height)
|
}
|
||||||
// if err != nil {
|
|
||||||
// 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 {
|
filepath.Walk("./movie", func(path string, info fs.FileInfo, err error) error {
|
||||||
if !info.IsDir() && filepath.Ext(info.Name()) != ".png" {
|
if !info.IsDir() && filepath.Ext(info.Name()) != ".png" {
|
||||||
base := info.Name()
|
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())
|
log.Println(path, info.Name())
|
||||||
movies = append(movies, Movie{
|
|
||||||
|
movie := Movie{
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
Image: base[:strings.IndexByte(base, '.')] + ".png",
|
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
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Printf("%##v", movies)
|
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() {
|
func main() {
|
||||||
|
|
25
src/CategoryNav.js
Normal file
25
src/CategoryNav.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Tabs from '@mui/material/Tabs';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
|
||||||
|
const CategoryNav = ({ categories, currentCategory, onCategoryChange }) => {
|
||||||
|
const handleChange = (event, newValue) => {
|
||||||
|
onCategoryChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
value={currentCategory}
|
||||||
|
onChange={handleChange}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
{categories.map((category, idx) => (
|
||||||
|
<Tab key={category.label} label={category.label} value={idx} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryNav;
|
|
@ -9,13 +9,24 @@ import Typography from '@mui/material/Typography';
|
||||||
import Pagination from '@mui/material/Pagination';
|
import Pagination from '@mui/material/Pagination';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import ConfigContext from './Config';
|
|
||||||
|
|
||||||
|
import ConfigContext from './Config';
|
||||||
|
import MovieCard from './MovieCard';
|
||||||
|
import CategoryNav from './CategoryNav';
|
||||||
|
|
||||||
|
|
||||||
const Main = () => {
|
const Main = () => {
|
||||||
const config = useContext(ConfigContext);
|
const config = useContext(ConfigContext);
|
||||||
|
|
||||||
|
const [currentCategory, setCurrentCategory] = useState(0);
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ label: '15min', idx: 0 },
|
||||||
|
{ label: '30min', idx: 1 },
|
||||||
|
{ label: '60min', idx: 2 },
|
||||||
|
{ label: '大于60min', idx: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
movies: [],
|
movies: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
|
@ -25,17 +36,29 @@ const Main = () => {
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const limit = 12;
|
const limit = 20;
|
||||||
|
|
||||||
const fetchMovies = async (page) => {
|
const handleCategoryChange = (category) => {
|
||||||
|
setCurrentCategory(category);
|
||||||
|
fetchMovies(category, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const fetchMovies = async (category, page) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${config.Host}/movie/?page=${page}&limit=${limit}`
|
`${config.Host}/movie/?page=${page}&limit=${limit}&category=${category}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const data = response.data.data;
|
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);
|
||||||
|
} else {
|
||||||
setPagination({
|
setPagination({
|
||||||
movies: data.items,
|
movies: data.items,
|
||||||
page: page,
|
page: page,
|
||||||
|
@ -44,6 +67,7 @@ const Main = () => {
|
||||||
});
|
});
|
||||||
localStorage.setItem('lastPage', page);
|
localStorage.setItem('lastPage', page);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching movies:', error);
|
console.error('Error fetching movies:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -53,11 +77,11 @@ const Main = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lastPage = localStorage.getItem('lastPage') || 1;
|
const lastPage = localStorage.getItem('lastPage') || 1;
|
||||||
fetchMovies(lastPage);
|
fetchMovies(currentCategory, lastPage);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePageChange = (event, value) => {
|
const handlePageChange = (event, value) => {
|
||||||
fetchMovies(value);
|
fetchMovies(currentCategory, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const truncateFilename = (filename, maxLength) => {
|
const truncateFilename = (filename, maxLength) => {
|
||||||
|
@ -66,8 +90,14 @@ const Main = () => {
|
||||||
: filename;
|
: filename;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container style={{ marginTop: 20 }}>
|
<Container style={{ marginTop: 20 }}>
|
||||||
|
<CategoryNav
|
||||||
|
categories={categories}
|
||||||
|
currentCategory={currentCategory}
|
||||||
|
onCategoryChange={handleCategoryChange}
|
||||||
|
/>
|
||||||
<div style={{ textAlign: 'center', marginBottom: 20 }}>
|
<div style={{ textAlign: 'center', marginBottom: 20 }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
count={pagination.length}
|
count={pagination.length}
|
||||||
|
@ -79,6 +109,8 @@ const Main = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div
|
<div
|
||||||
|
@ -92,6 +124,9 @@ const Main = () => {
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Grid container spacing={2} style={{ marginTop: 3 }}>
|
<Grid container spacing={2} style={{ marginTop: 3 }}>
|
||||||
{pagination.movies.map((item) => (
|
{pagination.movies.map((item) => (
|
||||||
<Grid
|
<Grid
|
||||||
|
@ -107,28 +142,7 @@ const Main = () => {
|
||||||
to={`/res/${item.filename}`}
|
to={`/res/${item.filename}`}
|
||||||
style={{ textDecoration: 'none', paddingBottom: 10 }}
|
style={{ textDecoration: 'none', paddingBottom: 10 }}
|
||||||
>
|
>
|
||||||
<Card
|
<MovieCard movie={item} config={config} />
|
||||||
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={`${config.Host}/res/${item.image}`}
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Typography>
|
|
||||||
{truncateFilename(item.filename, 15)}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
@ -136,6 +150,8 @@ const Main = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
count={pagination.length}
|
count={pagination.length}
|
||||||
|
|
41
src/MovieCard.js
Normal file
41
src/MovieCard.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardMedia from '@mui/material/CardMedia';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
const MovieCard = ({ movie, config }) => {
|
||||||
|
const truncateFilename = (filename, maxLength) => {
|
||||||
|
return filename.length > maxLength
|
||||||
|
? filename.substring(0, maxLength - 3) + '...'
|
||||||
|
: filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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={`${config.Host}/res/${movie.image}`}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Typography>{truncateFilename(movie.filename, 15)}</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
时长: {movie.duration} min
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieCard;
|
|
@ -16,6 +16,7 @@ const VideoPlayer = () => {
|
||||||
{filename}
|
{filename}
|
||||||
</Typography>
|
</Typography>
|
||||||
<video
|
<video
|
||||||
|
autoPlay={true}
|
||||||
controls
|
controls
|
||||||
style={{ width: '100%', maxHeight: '70vh' }}
|
style={{ width: '100%', maxHeight: '70vh' }}
|
||||||
src={`${config.Host}/res/${filename}`}
|
src={`${config.Host}/res/${filename}`}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user