Compare commits

..

No commits in common. "78100ad2db3c1d1b7bc97fa343a1da1887895b67" and "0a572afbd946c7e49e2335aa228fe861c38e78ea" have entirely different histories.

12 changed files with 114 additions and 580 deletions

View File

@ -1,79 +0,0 @@
# 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,3 +1,34 @@
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 import FastAPI, HTTPException, Query, Depends
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from typing import List from typing import List
@ -7,7 +38,6 @@ from models import Base, Video
from schemas import VideoCreate, VideoInDB from schemas import VideoCreate, VideoInDB
from database import engine, get_db from database import engine, get_db
from video_scanner import scan_video_directory from video_scanner import scan_video_directory
from fastapi.responses import FileResponse
import json import json
# Create database tables # Create database tables
@ -52,17 +82,6 @@ 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)):
""" """
@ -89,50 +108,48 @@ def scan_videos(db: Session = Depends(get_db)):
return {"message": f"Scan complete. Added {added_count} new videos.", "count": added_count} return {"message": f"Scan complete. Added {added_count} new videos.", "count": added_count}
@app.get("/videos/", response_model=List[VideoInDB]) @app.get("/videos/", response_model=List[VideoInDB])
def get_videos(db: Session = Depends(get_db), search: str = Query(None)): def get_videos(db: Session = Depends(get_db)):
""" """
Get all videos in the library, optionally filtered by search query Get all videos in the library
""" """
query = db.query(Video) videos = db.query(Video).all()
if search:
query = query.filter(
(Video.title.ilike(f"%{search}%")) |
(Video.path.ilike(f"%{search}%"))
)
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")
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__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) 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)

View File

@ -1,143 +0,0 @@
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,29 +1,6 @@
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]:
""" """
@ -35,29 +12,17 @@ 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

View File

@ -12,7 +12,6 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-player": "2.16.1",
"react-pro-sidebar": "^1.1.0" "react-pro-sidebar": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -1902,15 +1901,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -2724,12 +2714,6 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT" "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": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -2753,18 +2737,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -2784,12 +2756,6 @@
"node": ">= 0.4" "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": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -2863,15 +2829,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -3045,17 +3002,6 @@
"node": ">= 0.8.0" "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": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -3093,12 +3039,6 @@
"react": "^19.1.1" "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": { "node_modules/react-icons": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
@ -3114,22 +3054,6 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "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": { "node_modules/react-pro-sidebar": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-pro-sidebar/-/react-pro-sidebar-1.1.0.tgz", "resolved": "https://registry.npmjs.org/react-pro-sidebar/-/react-pro-sidebar-1.1.0.tgz",

View File

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

View File

@ -24,6 +24,23 @@
margin-left: 80px; /* Same as collapsed sidebar width */ 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 */ /* Sidebar Styles */
.path-section { .path-section {
padding: 10px 20px; padding: 10px 20px;
@ -83,27 +100,9 @@
word-break: break-all; word-break: break-all;
} }
.remove-path-btn { /* Scan Button */
background: none; .scan-btn {
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; padding: 10px 20px;
margin-top: 10px;
background-color: #28a745; background-color: #28a745;
color: white; color: white;
border: none; border: none;
@ -113,11 +112,11 @@
transition: background-color 0.3s; transition: background-color 0.3s;
} }
.scan-btn-sidebar:hover { .scan-btn:hover {
background-color: #218838; background-color: #218838;
} }
.scan-btn-sidebar:disabled { .scan-btn:disabled {
background-color: #ccc; background-color: #ccc;
cursor: not-allowed; cursor: not-allowed;
} }
@ -186,7 +185,6 @@
.video-details { .video-details {
flex-grow: 1; flex-grow: 1;
position: relative; /* For positioning edit/delete buttons */
} }
.video-title { .video-title {
@ -200,7 +198,11 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.video-channel, .video-stats {
margin: 0;
font-size: 14px;
color: #606060;
}
.video-path, .video-size { .video-path, .video-size {
margin: 4px 0; margin: 4px 0;
@ -208,75 +210,3 @@
color: #606060; color: #606060;
word-break: break-all; 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,8 +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, FaTrash } from 'react-icons/fa'; import { FaBars, FaHome, FaFolder, FaVideo, FaPlus, FaList } from 'react-icons/fa';
import ReactPlayer from 'react-player';
import './App.css'; import './App.css';
const API_BASE_URL = 'http://192.168.2.220:8000'; const API_BASE_URL = 'http://192.168.2.220:8000';
@ -13,9 +12,6 @@ function App() {
const [videos, setVideos] = useState([]); const [videos, setVideos] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [collapsed, setCollapsed] = 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 // Fetch initial data on component mount
useEffect(() => { useEffect(() => {
@ -32,26 +28,15 @@ function App() {
} }
}; };
const fetchVideos = async (search = '') => { const fetchVideos = async () => {
try { 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); setVideos(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching videos:', 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 () => { const addVideoPath = async () => {
if (!newPath.trim()) return; if (!newPath.trim()) return;
@ -67,18 +52,6 @@ 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 {
@ -93,28 +66,15 @@ 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 ( return (
<div className={`App ${collapsed ? 'collapsed' : ''}`}> <div className={`App ${collapsed ? 'collapsed' : ''}`}>
<Sidebar collapsed={collapsed} className="app-sidebar"> <Sidebar collapsed={collapsed} className="app-sidebar">
<Menu iconShape="square"> <Menu iconShape="square">
<MenuItem icon={<FaBars />} onClick={() => setCollapsed(!collapsed)}> <MenuItem icon={<FaBars />} onClick={() => setCollapsed(!collapsed)}>
<h2>{collapsed ? '' : 'Video App'}</h2> <h2>{collapsed ? '' : 'Menu'}</h2>
</MenuItem> </MenuItem>
<MenuItem icon={<FaHome />}>Home</MenuItem> <MenuItem icon={<FaHome />}>Home</MenuItem>
<SubMenu icon={<FaFolder />} label="Library"> <SubMenu icon={<FaFolder />} title="Management">
<div className="path-section"> <div className="path-section">
<h2>Video Library Paths</h2> <h2>Video Library Paths</h2>
<div className="path-form"> <div className="path-form">
@ -135,25 +95,13 @@ function App() {
{videoPaths.length > 0 ? ( {videoPaths.length > 0 ? (
<ul> <ul>
{videoPaths.map((path, index) => ( {videoPaths.map((path, index) => (
<li key={index}> <li key={index}><FaList /> {path}</li>
<FaList /> {path}
<button onClick={() => removeVideoPath(path)} className="remove-path-btn">
<FaTrash />
</button>
</li>
))} ))}
</ul> </ul>
) : ( ) : (
<p>No video paths added yet</p> <p>No video paths added yet</p>
)} )}
</div> </div>
<button
onClick={scanVideos}
disabled={loading}
className="scan-btn-sidebar"
>
{loading ? 'Scanning...' : 'Scan Videos'}
</button>
</div> </div>
</SubMenu> </SubMenu>
<MenuItem icon={<FaVideo />}>Videos</MenuItem> <MenuItem icon={<FaVideo />}>Videos</MenuItem>
@ -161,22 +109,24 @@ function App() {
</Sidebar> </Sidebar>
<main className="app-main"> <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"> <section className="videos-section">
<h2>Video Library</h2> <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 ? ( {videos.length > 0 ? (
<div className="video-grid"> <div className="video-grid">
{videos.map((video) => ( {videos.map((video) => (
<div key={video.id} className="video-card"> <div key={video.id} className="video-card">
<div className="video-thumbnail" onClick={() => openVideoPlayer(video)}> <div className="video-thumbnail">
<div className="thumbnail-placeholder"></div> <div className="thumbnail-placeholder"></div>
</div> </div>
<div className="video-info"> <div className="video-info">
@ -195,35 +145,6 @@ function App() {
)} )}
</section> </section>
</main> </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> </div>
); );
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB