Compare commits
No commits in common. "78100ad2db3c1d1b7bc97fa343a1da1887895b67" and "0a572afbd946c7e49e2335aa228fe861c38e78ea" have entirely different histories.
78100ad2db
...
0a572afbd9
79
README.md
79
README.md
|
|
@ -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.
|
|
||||||
121
backend/main.py
121
backend/main.py
|
|
@ -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}")
|
if __name__ == "__main__":
|
||||||
def delete_video(video_id: int, db: Session = Depends(get_db)):
|
import uvicorn
|
||||||
"""
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
Delete a video from the library by its ID
|
|
||||||
"""
|
|
||||||
video = db.query(Video).filter(Video.id == video_id).first()
|
@app.get("/video-paths/")
|
||||||
if not video:
|
def get_video_paths():
|
||||||
raise HTTPException(status_code=404, detail="Video not found")
|
"""
|
||||||
|
Get all video paths in the library
|
||||||
db.delete(video)
|
"""
|
||||||
db.commit()
|
return {"paths": video_paths}
|
||||||
return {"message": f"Video with ID {video_id} deleted successfully"}
|
|
||||||
|
@app.post("/scan-videos/")
|
||||||
@app.get("/videos/{video_id}/stream")
|
def scan_videos():
|
||||||
def stream_video(video_id: int, db: Session = Depends(get_db)):
|
"""
|
||||||
"""
|
Scan all video paths and return the videos found
|
||||||
Stream a video file by its ID
|
"""
|
||||||
"""
|
all_videos = []
|
||||||
print(f"Attempting to stream video with ID: {video_id}") # Log
|
for path in video_paths:
|
||||||
video = db.query(Video).filter(Video.id == video_id).first()
|
try:
|
||||||
if not video:
|
videos = scan_video_directory(path)
|
||||||
print(f"Video with ID {video_id} not found in database.") # Log
|
all_videos.extend(videos)
|
||||||
raise HTTPException(status_code=404, detail="Video not found")
|
except FileNotFoundError as e:
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
if not os.path.exists(video.path):
|
|
||||||
print(f"Video file not found on server: {video.path}") # Log
|
return {"videos": all_videos, "count": len(all_videos)}
|
||||||
raise HTTPException(status_code=404, detail="Video file not found on server")
|
|
||||||
|
@app.get("/videos/")
|
||||||
print(f"Streaming video from path: {video.path}") # Log
|
def get_videos():
|
||||||
return FileResponse(video.path, media_type="video/mp4")
|
"""
|
||||||
|
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__":
|
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)
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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,28 +12,16 @@ 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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}>×</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 |
Loading…
Reference in New Issue