Compare commits

..

5 Commits

Author SHA1 Message Date
tigeren 78100ad2db fix: downgrade react-player to version 2.16.1 and update import statement
- Changed the react-player dependency version in package.json from 3.3.1 to 2.16.1.
- Updated the import statement in App.jsx from VideoPlayer to ReactPlayer.
- Added onReady and onError event handlers to the ReactPlayer component for better error handling and logging.
2025-08-23 17:00:27 +00:00
tigeren 08dab37208 Implement code changes to enhance functionality and improve performance 2025-08-23 16:16:19 +00:00
tigeren 2fb5d6b413 feat: add delete functionality for video paths and videos, and enhance UI with remove buttons 2025-08-23 15:58:50 +00:00
tigeren 3782556c03 feat: add video player functionality and search feature
- Added `react-player` dependency for video playback.
- Implemented a modal for video playback with a close button.
- Integrated search functionality to filter videos by title or path.
- Updated CSS for video player modal and search bar styling.
- Enhanced video fetching logic to support search queries.
2025-08-23 15:49:17 +00:00
tigeren 27dd0bee39 Refactor code structure and remove redundant functions for improved readability and maintainability 2025-08-23 15:41:15 +00:00
12 changed files with 580 additions and 114 deletions

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# Video Organization App
This project consists of a backend API (FastAPI) and a frontend web application (React).
## Getting Started
Follow these instructions to set up and run the project on your local machine.
### Prerequisites
* Node.js (LTS version recommended)
* npm (comes with Node.js)
* Python 3.8+
* pip (comes with Python)
### Backend Setup and Run
The backend is a FastAPI application.
1. **Navigate to the backend directory:**
```bash
cd backend
```
2. **Install Python dependencies:**
It's recommended to use a virtual environment.
```bash
python -m venv venv
source venv/bin/activate # On Windows, use `venv\Scripts\activate`
pip install -r requirements.txt
```
3. **Run the backend server:**
```bash
uvicorn main:app --host 0.0.0.0 --port 8000
```
The backend server will be accessible at `http://0.0.0.0:8000`.
### Frontend Setup and Run
The frontend is a React application built with Vite.
1. **Navigate to the frontend directory:**
```bash
cd frontend
```
2. **Install Node.js dependencies:**
```bash
npm install
```
3. **Run the frontend development server:**
```bash
npm run dev -- --host
```
The frontend development server will typically run on `http://localhost:5173` (or another available port).
### Rebuild and Restart Frontend
If you make changes to the frontend code and need to ensure a clean rebuild, or if you encounter caching issues, follow these steps:
1. **Stop the frontend development server** (if running). You might need to find and kill the process manually if it's running in the background.
2. **Clear cache and reinstall dependencies:**
```bash
rm -rf frontend/node_modules frontend/.vite
npm install --prefix frontend
```
3. **Restart the frontend development server:**
```bash
npm run dev --prefix frontend &
```
## Project Structure
* `backend/`: Contains the FastAPI backend application.
* `frontend/`: Contains the React frontend application.
* `test_videos/`: Sample video files.
* `screenshot/`: Screenshots of the application.

View File

@ -1,34 +1,3 @@
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from typing import List
import os
from models import Base, Video
from schemas import VideoCreate, VideoInDB
from database import engine, get_db
from video_scanner import scan_video_directory
import json
# Create database tables
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Video Organization API", version="0.1.0")
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# In-memory storage for video paths (in a real app, this would be in a database)
video_paths = []
@app.get("/")
def read_root():
return {"message": "Welcome to Video Organization API"}
from fastapi import FastAPI, HTTPException, Query, Depends
from fastapi.middleware.cors import CORSMiddleware
from typing import List
@ -38,6 +7,7 @@ from models import Base, Video
from schemas import VideoCreate, VideoInDB
from database import engine, get_db
from video_scanner import scan_video_directory
from fastapi.responses import FileResponse
import json
# Create database tables
@ -82,6 +52,17 @@ def get_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/")
def scan_videos(db: Session = Depends(get_db)):
"""
@ -108,48 +89,50 @@ def scan_videos(db: Session = Depends(get_db)):
return {"message": f"Scan complete. Added {added_count} new videos.", "count": added_count}
@app.get("/videos/", response_model=List[VideoInDB])
def get_videos(db: Session = Depends(get_db)):
def get_videos(db: Session = Depends(get_db), search: str = Query(None)):
"""
Get all videos in the library
Get all videos in the library, optionally filtered by search query
"""
videos = db.query(Video).all()
query = db.query(Video)
if search:
query = query.filter(
(Video.title.ilike(f"%{search}%")) |
(Video.path.ilike(f"%{search}%"))
)
videos = query.all()
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")
def stream_video(video_id: int, db: Session = Depends(get_db)):
"""
Stream a video file by its ID
"""
print(f"Attempting to stream video with ID: {video_id}") # Log
video = db.query(Video).filter(Video.id == video_id).first()
if not video:
print(f"Video with ID {video_id} not found in database.") # Log
raise HTTPException(status_code=404, detail="Video not found")
if not os.path.exists(video.path):
print(f"Video file not found on server: {video.path}") # Log
raise HTTPException(status_code=404, detail="Video file not found on server")
print(f"Streaming video from path: {video.path}") # Log
return FileResponse(video.path, media_type="video/mp4")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
@app.get("/video-paths/")
def get_video_paths():
"""
Get all video paths in the library
"""
return {"paths": video_paths}
@app.post("/scan-videos/")
def scan_videos():
"""
Scan all video paths and return the videos found
"""
all_videos = []
for path in video_paths:
try:
videos = scan_video_directory(path)
all_videos.extend(videos)
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
return {"videos": all_videos, "count": len(all_videos)}
@app.get("/videos/")
def get_videos():
"""
Get all videos in the library
"""
# This is a placeholder - in a real implementation, we would query the database
return {"message": "List of videos"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

143
backend/uvicorn.log Normal file
View File

@ -0,0 +1,143 @@
INFO: Started server process [25188]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: 192.168.2.244:52112 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:52112 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:52113 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52112 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52112 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52161 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:52162 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52161 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:52162 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52162 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52442 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:52443 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52444 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:52445 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:52445 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53093 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53094 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53093 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53095 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53095 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53244 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53245 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53244 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53245 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53245 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53564 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53565 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53564 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53566 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53566 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53834 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53835 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53834 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:53835 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:53842 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:54212 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:54213 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:54212 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:54213 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:54217 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:54512 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:54513 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:54512 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:54513 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:54513 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:57131 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:57132 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:57131 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:57133 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:57133 - "GET /videos/?search= HTTP/1.1" 200 OK
Attempting to stream video with ID: 2
Streaming video from path: /mnt/data1/Porn/twitter/我艹girl/我艹girl (完整版已放推文评论区) (@minimimm2006) - X_10.mp4
INFO: 192.168.2.244:57308 - "GET /videos/2/stream HTTP/1.1" 200 OK
INFO: 192.168.2.244:57313 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:57313 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:57350 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:57350 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:58522 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:58523 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:58522 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:58523 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:58526 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:59275 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:59276 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:59275 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:59277 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:59277 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:60135 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:60136 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:60135 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:60137 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:60137 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:61752 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:61753 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:61752 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:61754 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:61754 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:63608 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:63607 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:63608 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:63607 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:63607 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64389 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64390 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64389 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64390 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64390 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64448 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64449 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64448 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64450 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64450 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64686 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64687 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64686 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64688 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64689 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64833 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64834 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64833 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:64834 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:64834 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:49672 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:49673 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:49672 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:49673 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:49673 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:49673 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:49672 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:49674 - "GET /video-paths/ HTTP/1.1" 200 OK
INFO: 192.168.2.244:49673 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:49673 - "GET /videos/?search= HTTP/1.1" 200 OK
INFO: 192.168.2.244:49703 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49703 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49703 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49723 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49723 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49723 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49723 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49723 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49723 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49723 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49737 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49737 - "HEAD /videos/2/stream HTTP/1.1" 405 Method Not Allowed
Attempting to stream video with ID: 3
Streaming video from path: /mnt/data1/Porn/twitter/我艹girl/我艹girl (完整版已放推文评论区) (@minimimm2006) - X_12.mp4
INFO: 192.168.2.244:49748 - "GET /videos/3/stream HTTP/1.1" 200 OK
INFO: 192.168.2.244:49751 - "HEAD /videos/3/stream HTTP/1.1" 405 Method Not Allowed
Attempting to stream video with ID: 11
Streaming video from path: /mnt/data1/Porn/twitter/我艹girl/我艹girl (完整版已放推文评论区) (@minimimm2006) - X_7.mp4
INFO: 192.168.2.244:49758 - "GET /videos/11/stream HTTP/1.1" 200 OK
INFO: 192.168.2.244:49772 - "GET /videos/?search=X HTTP/1.1" 200 OK
INFO: 192.168.2.244:49772 - "GET /videos/?search=X_ HTTP/1.1" 200 OK
INFO: 192.168.2.244:49772 - "GET /videos/?search=X_21 HTTP/1.1" 200 OK
Attempting to stream video with ID: 6
Streaming video from path: /mnt/data1/Porn/twitter/我艹girl/我艹girl (完整版已放推文评论区) (@minimimm2006) - X_21.mp4
INFO: 192.168.2.244:49777 - "GET /videos/6/stream HTTP/1.1" 200 OK
INFO: 192.168.2.244:49778 - "HEAD /videos/6/stream HTTP/1.1" 405 Method Not Allowed
INFO: 192.168.2.244:49778 - "HEAD /videos/6/stream HTTP/1.1" 405 Method Not Allowed

View File

@ -1,6 +1,29 @@
import os
from typing import List
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]:
"""
@ -12,17 +35,29 @@ def scan_video_directory(directory_path: str) -> List[dict]:
if not os.path.exists(directory_path):
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 file in files:
if os.path.splitext(file)[1].lower() in video_extensions:
full_path = os.path.join(root, file)
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 = {
"title": os.path.splitext(file)[0],
"path": full_path,
"size": stat.st_size,
"thumbnail_path": thumbnail_path if os.path.exists(thumbnail_path) else None,
}
videos.append(video_info)
return videos
return videos

View File

@ -12,6 +12,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-player": "2.16.1",
"react-pro-sidebar": "^1.1.0"
},
"devDependencies": {
@ -1901,6 +1902,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -2714,6 +2724,12 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -2737,6 +2753,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -2756,6 +2784,12 @@
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -2829,6 +2863,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -3002,6 +3045,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -3039,6 +3093,12 @@
"react": "^19.1.1"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
@ -3054,6 +3114,22 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-player": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.1.tgz",
"integrity": "sha512-mxP6CqjSWjidtyDoMOSHVPdhX0pY16aSvw5fVr44EMaT7X5Xz46uQ4b/YBm1v2x+3hHkB9PmjEEkmbHb9PXQ4w==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.0.0",
"load-script": "^1.0.0",
"memoize-one": "^5.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.0.1"
},
"peerDependencies": {
"react": ">=16.6.0"
}
},
"node_modules/react-pro-sidebar": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-pro-sidebar/-/react-pro-sidebar-1.1.0.tgz",

View File

@ -14,6 +14,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-player": "2.16.1",
"react-pro-sidebar": "^1.1.0"
},
"devDependencies": {

View File

@ -24,23 +24,6 @@
margin-left: 80px; /* Same as collapsed sidebar width */
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
background: white;
padding: 10px 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.app-header h1 {
margin: 0;
font-size: 24px;
color: #333;
}
/* Sidebar Styles */
.path-section {
padding: 10px 20px;
@ -100,9 +83,27 @@
word-break: break-all;
}
/* Scan Button */
.scan-btn {
.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 {
width: 100%;
padding: 10px 20px;
margin-top: 10px;
background-color: #28a745;
color: white;
border: none;
@ -112,11 +113,11 @@
transition: background-color 0.3s;
}
.scan-btn:hover {
.scan-btn-sidebar:hover {
background-color: #218838;
}
.scan-btn:disabled {
.scan-btn-sidebar:disabled {
background-color: #ccc;
cursor: not-allowed;
}
@ -185,6 +186,7 @@
.video-details {
flex-grow: 1;
position: relative; /* For positioning edit/delete buttons */
}
.video-title {
@ -198,11 +200,7 @@
text-overflow: ellipsis;
}
.video-channel, .video-stats {
margin: 0;
font-size: 14px;
color: #606060;
}
.video-path, .video-size {
margin: 4px 0;
@ -210,3 +208,75 @@
color: #606060;
word-break: break-all;
}
/* Video Player Modal */
.video-player-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.video-player-modal-content {
background-color: #fff;
padding: 20px;
border-radius: 8px;
position: relative;
width: 90%;
max-width: 900px;
max-height: 90%;
overflow-y: auto;
}
.close-player-modal {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #333;
}
.video-player-modal-content h3 {
margin-top: 0;
margin-bottom: 15px;
font-size: 22px;
color: #333;
}
.player-wrapper {
position: relative;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
margin-bottom: 15px;
}
.react-player {
position: absolute;
top: 0;
left: 0;
}
/* Search Bar */
.search-bar {
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}

View File

@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
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 './App.css';
const API_BASE_URL = 'http://192.168.2.220:8000';
@ -12,6 +13,9 @@ function App() {
const [videos, setVideos] = useState([]);
const [loading, setLoading] = useState(false);
const [collapsed, setCollapsed] = useState(false);
const [selectedVideo, setSelectedVideo] = useState(null);
const [showPlayerModal, setShowPlayerModal] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// Fetch initial data on component mount
useEffect(() => {
@ -28,15 +32,26 @@ function App() {
}
};
const fetchVideos = async () => {
const fetchVideos = async (search = '') => {
try {
const response = await axios.get(`${API_BASE_URL}/videos/`);
const response = await axios.get(`${API_BASE_URL}/videos/`, {
params: { search: search }
});
setVideos(response.data);
} catch (error) {
console.error('Error fetching videos:', error);
}
};
// Add a new useEffect to trigger search when searchQuery changes
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
fetchVideos(searchQuery);
}, 500); // Debounce for 500ms
return () => clearTimeout(delayDebounceFn);
}, [searchQuery]); // Re-run when searchQuery changes
const addVideoPath = async () => {
if (!newPath.trim()) return;
@ -52,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 () => {
setLoading(true);
try {
@ -66,15 +93,28 @@ function App() {
}
};
const openVideoPlayer = (video) => {
console.log('Attempting to open video player for:', video);
setSelectedVideo(video);
setShowPlayerModal(true);
};
const closeVideoPlayer = () => {
console.log('Closing video player modal.');
setShowPlayerModal(false);
// Delay setting selectedVideo to null to allow for unmount animation if any
setTimeout(() => setSelectedVideo(null), 300);
};
return (
<div className={`App ${collapsed ? 'collapsed' : ''}`}>
<Sidebar collapsed={collapsed} className="app-sidebar">
<Menu iconShape="square">
<MenuItem icon={<FaBars />} onClick={() => setCollapsed(!collapsed)}>
<h2>{collapsed ? '' : 'Menu'}</h2>
<h2>{collapsed ? '' : 'Video App'}</h2>
</MenuItem>
<MenuItem icon={<FaHome />}>Home</MenuItem>
<SubMenu icon={<FaFolder />} title="Management">
<SubMenu icon={<FaFolder />} label="Library">
<div className="path-section">
<h2>Video Library Paths</h2>
<div className="path-form">
@ -95,13 +135,25 @@ function App() {
{videoPaths.length > 0 ? (
<ul>
{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>
) : (
<p>No video paths added yet</p>
)}
</div>
<button
onClick={scanVideos}
disabled={loading}
className="scan-btn-sidebar"
>
{loading ? 'Scanning...' : 'Scan Videos'}
</button>
</div>
</SubMenu>
<MenuItem icon={<FaVideo />}>Videos</MenuItem>
@ -109,24 +161,22 @@ function App() {
</Sidebar>
<main className="app-main">
<header className="app-header">
<h1>Video Organization App</h1>
<button
onClick={scanVideos}
disabled={loading}
className="scan-btn"
>
{loading ? 'Scanning...' : 'Scan Videos'}
</button>
</header>
<section className="videos-section">
<h2>Video Library</h2>
<div className="search-bar">
<input
type="text"
placeholder="Search videos by title or path..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
</div>
{videos.length > 0 ? (
<div className="video-grid">
{videos.map((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>
<div className="video-info">
@ -145,6 +195,35 @@ function App() {
)}
</section>
</main>
{showPlayerModal && selectedVideo && (
<div className="video-player-modal-overlay" onClick={closeVideoPlayer}>
<div className="video-player-modal-content" onClick={(e) => e.stopPropagation()}>
<button className="close-player-modal" onClick={closeVideoPlayer}>&times;</button>
<h3>{selectedVideo.title}</h3>
<div className="player-wrapper">
{selectedVideo && (
<ReactPlayer
url={`${API_BASE_URL}/videos/${selectedVideo.id}/stream`}
className="react-player"
width="100%"
height="100%"
controls={true}
playing={true}
onReady={() => {
console.log('react-player ready:', `${API_BASE_URL}/videos/${selectedVideo.id}/stream`);
}}
onError={(e) => {
console.error('react-player error:', e, 'URL:', `${API_BASE_URL}/videos/${selectedVideo.id}/stream`);
alert('Video player error. See console for details.');
}}
/>
)}
</div>
<p>{selectedVideo.path}</p>
</div>
</div>
)}
</div>
);
}

BIN
screenshot/image copy 2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

BIN
screenshot/image copy 3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
screenshot/image copy 4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

BIN
screenshot/image copy 5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB