feat: add delete functionality for video paths and videos, and enhance UI with remove buttons

This commit is contained in:
tigeren 2025-08-23 15:58:50 +00:00
parent 3782556c03
commit 2fb5d6b413
5 changed files with 168 additions and 16 deletions

View File

@ -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)):
""" """

View File

@ -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

View File

@ -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,16 +35,28 @@ 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)

View File

@ -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;

View File

@ -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,6 +200,7 @@ function App() {
<button className="close-player-modal" onClick={closeVideoPlayer}>&times;</button> <button className="close-player-modal" onClick={closeVideoPlayer}>&times;</button>
<h3>{selectedVideo.title}</h3> <h3>{selectedVideo.title}</h3>
<div className="player-wrapper"> <div className="player-wrapper">
{selectedVideo && (
<ReactPlayer <ReactPlayer
url={`${API_BASE_URL}/videos/${selectedVideo.id}/stream`} url={`${API_BASE_URL}/videos/${selectedVideo.id}/stream`}
className="react-player" className="react-player"
@ -190,6 +209,7 @@ function App() {
controls={true} controls={true}
playing={true} playing={true}
/> />
)}
</div> </div>
<p>{selectedVideo.path}</p> <p>{selectedVideo.path}</p>
</div> </div>