Compare commits
5 Commits
0a572afbd9
...
78100ad2db
| Author | SHA1 | Date |
|---|---|---|
|
|
78100ad2db | |
|
|
08dab37208 | |
|
|
2fb5d6b413 | |
|
|
3782556c03 | |
|
|
27dd0bee39 |
|
|
@ -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.
|
||||||
121
backend/main.py
121
backend/main.py
|
|
@ -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 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
|
||||||
|
|
@ -38,6 +7,7 @@ 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
|
||||||
|
|
@ -82,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)):
|
||||||
"""
|
"""
|
||||||
|
|
@ -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}
|
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)):
|
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
|
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)
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"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": {
|
||||||
|
|
@ -1901,6 +1902,15 @@
|
||||||
"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",
|
||||||
|
|
@ -2714,6 +2724,12 @@
|
||||||
"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",
|
||||||
|
|
@ -2737,6 +2753,18 @@
|
||||||
"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",
|
||||||
|
|
@ -2756,6 +2784,12 @@
|
||||||
"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",
|
||||||
|
|
@ -2829,6 +2863,15 @@
|
||||||
"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",
|
||||||
|
|
@ -3002,6 +3045,17 @@
|
||||||
"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",
|
||||||
|
|
@ -3039,6 +3093,12 @@
|
||||||
"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",
|
||||||
|
|
@ -3054,6 +3114,22 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"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": {
|
||||||
|
|
|
||||||
|
|
@ -24,23 +24,6 @@
|
||||||
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;
|
||||||
|
|
@ -100,9 +83,27 @@
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scan Button */
|
.remove-path-btn {
|
||||||
.scan-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;
|
padding: 10px 20px;
|
||||||
|
margin-top: 10px;
|
||||||
background-color: #28a745;
|
background-color: #28a745;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -112,11 +113,11 @@
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-btn:hover {
|
.scan-btn-sidebar:hover {
|
||||||
background-color: #218838;
|
background-color: #218838;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-btn:disabled {
|
.scan-btn-sidebar:disabled {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +186,7 @@
|
||||||
|
|
||||||
.video-details {
|
.video-details {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
position: relative; /* For positioning edit/delete buttons */
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-title {
|
.video-title {
|
||||||
|
|
@ -198,11 +200,7 @@
|
||||||
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;
|
||||||
|
|
@ -210,3 +208,75 @@
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
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 './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';
|
||||||
|
|
@ -12,6 +13,9 @@ 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(() => {
|
||||||
|
|
@ -28,15 +32,26 @@ function App() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchVideos = async () => {
|
const fetchVideos = async (search = '') => {
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -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 () => {
|
const scanVideos = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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 (
|
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 ? '' : 'Menu'}</h2>
|
<h2>{collapsed ? '' : 'Video App'}</h2>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<FaHome />}>Home</MenuItem>
|
<MenuItem icon={<FaHome />}>Home</MenuItem>
|
||||||
<SubMenu icon={<FaFolder />} title="Management">
|
<SubMenu icon={<FaFolder />} label="Library">
|
||||||
<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">
|
||||||
|
|
@ -95,13 +135,25 @@ 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>
|
||||||
) : (
|
) : (
|
||||||
<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>
|
||||||
|
|
@ -109,24 +161,22 @@ 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">
|
<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">
|
||||||
|
|
@ -145,6 +195,35 @@ 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}>×</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.
|
After Width: | Height: | Size: 672 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Loading…
Reference in New Issue