feat: add delete functionality for video paths and videos, and enhance UI with remove buttons
This commit is contained in:
parent
3782556c03
commit
2fb5d6b413
|
|
@ -52,6 +52,17 @@ def get_video_paths():
|
||||||
"""
|
"""
|
||||||
return {"paths": video_paths}
|
return {"paths": video_paths}
|
||||||
|
|
||||||
|
@app.delete("/video-paths/")
|
||||||
|
def delete_video_path(path: str = Query(...)):
|
||||||
|
"""
|
||||||
|
Delete a video path from the library
|
||||||
|
"""
|
||||||
|
if path in video_paths:
|
||||||
|
video_paths.remove(path)
|
||||||
|
return {"message": f"Path {path} removed successfully", "paths": video_paths}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Path not found")
|
||||||
|
|
||||||
@app.post("/scan-videos/")
|
@app.post("/scan-videos/")
|
||||||
def scan_videos(db: Session = Depends(get_db)):
|
def scan_videos(db: Session = Depends(get_db)):
|
||||||
"""
|
"""
|
||||||
|
|
@ -91,6 +102,19 @@ def get_videos(db: Session = Depends(get_db), search: str = Query(None)):
|
||||||
videos = query.all()
|
videos = query.all()
|
||||||
return videos
|
return videos
|
||||||
|
|
||||||
|
@app.delete("/videos/{video_id}")
|
||||||
|
def delete_video(video_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Delete a video from the library by its ID
|
||||||
|
"""
|
||||||
|
video = db.query(Video).filter(Video.id == video_id).first()
|
||||||
|
if not video:
|
||||||
|
raise HTTPException(status_code=404, detail="Video not found")
|
||||||
|
|
||||||
|
db.delete(video)
|
||||||
|
db.commit()
|
||||||
|
return {"message": f"Video with ID {video_id} deleted successfully"}
|
||||||
|
|
||||||
@app.get("/videos/{video_id}/stream")
|
@app.get("/videos/{video_id}/stream")
|
||||||
def stream_video(video_id: int, db: Session = Depends(get_db)):
|
def stream_video(video_id: int, db: Session = Depends(get_db)):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,52 @@ INFO: 192.168.2.244:49769 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
INFO: 192.168.2.244:49768 - "GET /video-paths/ HTTP/1.1" 200 OK
|
INFO: 192.168.2.244:49768 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
INFO: 192.168.2.244:49769 - "GET /videos/?search= HTTP/1.1" 200 OK
|
INFO: 192.168.2.244:49769 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
INFO: 192.168.2.244:49769 - "GET /videos/?search= HTTP/1.1" 200 OK
|
INFO: 192.168.2.244:49769 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50248 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50249 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50248 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50250 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50250 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50614 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50614 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50615 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50614 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50614 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50807 - "GET /videos/?search=mini HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50807 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50845 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50844 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50846 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50849 - "OPTIONS /videos/2 HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50849 - "PUT /videos/2 HTTP/1.1" 404 Not Found
|
||||||
|
INFO: 192.168.2.244:50860 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50861 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50860 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50862 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50862 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50936 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50937 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50936 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50937 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50937 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50940 - "OPTIONS /videos/1 HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:50940 - "PUT /videos/1 HTTP/1.1" 404 Not Found
|
||||||
|
INFO: 192.168.2.244:51004 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51005 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51004 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51005 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51005 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51010 - "POST /scan-videos/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51010 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51020 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51021 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51020 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51022 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51022 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51081 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51082 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51082 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51391 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51391 - "GET /video-paths/ HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51392 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51391 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
INFO: 192.168.2.244:51391 - "GET /videos/?search= HTTP/1.1" 200 OK
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,29 @@
|
||||||
import os
|
import os
|
||||||
from typing import List
|
from typing import List
|
||||||
from models import Video
|
from models import Video
|
||||||
|
import ffmpeg
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
def generate_thumbnail(video_path: str, thumbnail_path: str):
|
||||||
|
"""
|
||||||
|
Generates a thumbnail from a video file.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Extract a frame at 1 second mark
|
||||||
|
(
|
||||||
|
ffmpeg
|
||||||
|
.input(video_path)
|
||||||
|
.output(thumbnail_path, vframes=1)
|
||||||
|
.overwrite_output()
|
||||||
|
.run(capture_stdout=True, capture_stderr=True)
|
||||||
|
)
|
||||||
|
except ffmpeg.Error as e:
|
||||||
|
print(f"Error generating thumbnail for {video_path}: {e.stderr.decode()}")
|
||||||
|
# Fallback to a default thumbnail or handle error
|
||||||
|
# For now, we'll just let it fail and the frontend will use a placeholder
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
def scan_video_directory(directory_path: str) -> List[dict]:
|
def scan_video_directory(directory_path: str) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -12,17 +35,29 @@ def scan_video_directory(directory_path: str) -> List[dict]:
|
||||||
if not os.path.exists(directory_path):
|
if not os.path.exists(directory_path):
|
||||||
raise FileNotFoundError(f"Directory {directory_path} does not exist")
|
raise FileNotFoundError(f"Directory {directory_path} does not exist")
|
||||||
|
|
||||||
|
# Create a directory for thumbnails if it doesn't exist
|
||||||
|
thumbnails_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "thumbnails")
|
||||||
|
os.makedirs(thumbnails_dir, exist_ok=True)
|
||||||
|
|
||||||
for root, dirs, files in os.walk(directory_path):
|
for root, dirs, files in os.walk(directory_path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if os.path.splitext(file)[1].lower() in video_extensions:
|
if os.path.splitext(file)[1].lower() in video_extensions:
|
||||||
full_path = os.path.join(root, file)
|
full_path = os.path.join(root, file)
|
||||||
stat = os.stat(full_path)
|
stat = os.stat(full_path)
|
||||||
|
|
||||||
|
# Generate thumbnail path
|
||||||
|
thumbnail_filename = f"{os.path.splitext(file)[0]}.jpg"
|
||||||
|
thumbnail_path = os.path.join(thumbnails_dir, thumbnail_filename)
|
||||||
|
|
||||||
|
# Generate thumbnail
|
||||||
|
generate_thumbnail(full_path, thumbnail_path)
|
||||||
|
|
||||||
video_info = {
|
video_info = {
|
||||||
"title": os.path.splitext(file)[0],
|
"title": os.path.splitext(file)[0],
|
||||||
"path": full_path,
|
"path": full_path,
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
|
"thumbnail_path": thumbnail_path if os.path.exists(thumbnail_path) else None,
|
||||||
}
|
}
|
||||||
videos.append(video_info)
|
videos.append(video_info)
|
||||||
|
|
||||||
return videos
|
return videos
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,23 @@
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remove-path-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #dc3545;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: auto; /* Pushes the button to the right */
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-path-btn:hover {
|
||||||
|
color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
.scan-btn-sidebar {
|
.scan-btn-sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
|
@ -169,6 +186,7 @@
|
||||||
|
|
||||||
.video-details {
|
.video-details {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
position: relative; /* For positioning edit/delete buttons */
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-title {
|
.video-title {
|
||||||
|
|
@ -182,6 +200,8 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.video-path, .video-size {
|
.video-path, .video-size {
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -189,6 +209,10 @@
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Video Player Modal */
|
/* Video Player Modal */
|
||||||
.video-player-modal-overlay {
|
.video-player-modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -255,4 +279,4 @@
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Sidebar, Menu, MenuItem, SubMenu } from 'react-pro-sidebar';
|
import { Sidebar, Menu, MenuItem, SubMenu } from 'react-pro-sidebar';
|
||||||
import { FaBars, FaHome, FaFolder, FaVideo, FaPlus, FaList } from 'react-icons/fa';
|
import { FaBars, FaHome, FaFolder, FaVideo, FaPlus, FaList, FaTrash } from 'react-icons/fa';
|
||||||
import ReactPlayer from 'react-player';
|
import ReactPlayer from 'react-player';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
|
|
@ -67,6 +67,18 @@ function App() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeVideoPath = async (pathToRemove) => {
|
||||||
|
try {
|
||||||
|
await axios.delete(`${API_BASE_URL}/video-paths/`, {
|
||||||
|
params: { path: pathToRemove }
|
||||||
|
});
|
||||||
|
fetchVideoPaths(); // Refresh the list of paths
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing video path:', error);
|
||||||
|
alert('Error removing video path: ' + (error.response?.data?.detail || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const scanVideos = async () => {
|
const scanVideos = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -87,8 +99,9 @@ function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeVideoPlayer = () => {
|
const closeVideoPlayer = () => {
|
||||||
setSelectedVideo(null);
|
|
||||||
setShowPlayerModal(false);
|
setShowPlayerModal(false);
|
||||||
|
// Delay setting selectedVideo to null to allow for unmount animation if any
|
||||||
|
setTimeout(() => setSelectedVideo(null), 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -120,7 +133,12 @@ function App() {
|
||||||
{videoPaths.length > 0 ? (
|
{videoPaths.length > 0 ? (
|
||||||
<ul>
|
<ul>
|
||||||
{videoPaths.map((path, index) => (
|
{videoPaths.map((path, index) => (
|
||||||
<li key={index}><FaList /> {path}</li>
|
<li key={index}>
|
||||||
|
<FaList /> {path}
|
||||||
|
<button onClick={() => removeVideoPath(path)} className="remove-path-btn">
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -155,8 +173,8 @@ function App() {
|
||||||
{videos.length > 0 ? (
|
{videos.length > 0 ? (
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{videos.map((video) => (
|
{videos.map((video) => (
|
||||||
<div key={video.id} className="video-card" onClick={() => openVideoPlayer(video)}>
|
<div key={video.id} className="video-card">
|
||||||
<div className="video-thumbnail">
|
<div className="video-thumbnail" onClick={() => openVideoPlayer(video)}>
|
||||||
<div className="thumbnail-placeholder">▶</div>
|
<div className="thumbnail-placeholder">▶</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="video-info">
|
<div className="video-info">
|
||||||
|
|
@ -182,14 +200,16 @@ function App() {
|
||||||
<button className="close-player-modal" onClick={closeVideoPlayer}>×</button>
|
<button className="close-player-modal" onClick={closeVideoPlayer}>×</button>
|
||||||
<h3>{selectedVideo.title}</h3>
|
<h3>{selectedVideo.title}</h3>
|
||||||
<div className="player-wrapper">
|
<div className="player-wrapper">
|
||||||
<ReactPlayer
|
{selectedVideo && (
|
||||||
url={`${API_BASE_URL}/videos/${selectedVideo.id}/stream`}
|
<ReactPlayer
|
||||||
className="react-player"
|
url={`${API_BASE_URL}/videos/${selectedVideo.id}/stream`}
|
||||||
width="100%"
|
className="react-player"
|
||||||
height="100%"
|
width="100%"
|
||||||
controls={true}
|
height="100%"
|
||||||
playing={true}
|
controls={true}
|
||||||
/>
|
playing={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p>{selectedVideo.path}</p>
|
<p>{selectedVideo.path}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,4 +219,4 @@ function App() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
Loading…
Reference in New Issue