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.middleware.cors import CORSMiddleware
|
||||
from typing import List
|
||||
|
|
@ -38,6 +7,7 @@ from models import Base, Video
|
|||
from schemas import VideoCreate, VideoInDB
|
||||
from database import engine, get_db
|
||||
from video_scanner import scan_video_directory
|
||||
from fastapi.responses import FileResponse
|
||||
import json
|
||||
|
||||
# Create database tables
|
||||
|
|
@ -82,6 +52,17 @@ def get_video_paths():
|
|||
"""
|
||||
return {"paths": video_paths}
|
||||
|
||||
@app.delete("/video-paths/")
|
||||
def delete_video_path(path: str = Query(...)):
|
||||
"""
|
||||
Delete a video path from the library
|
||||
"""
|
||||
if path in video_paths:
|
||||
video_paths.remove(path)
|
||||
return {"message": f"Path {path} removed successfully", "paths": video_paths}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Path not found")
|
||||
|
||||
@app.post("/scan-videos/")
|
||||
def scan_videos(db: Session = Depends(get_db)):
|
||||
"""
|
||||
|
|
@ -108,48 +89,50 @@ def scan_videos(db: Session = Depends(get_db)):
|
|||
return {"message": f"Scan complete. Added {added_count} new videos.", "count": added_count}
|
||||
|
||||
@app.get("/videos/", response_model=List[VideoInDB])
|
||||
def get_videos(db: Session = Depends(get_db)):
|
||||
def get_videos(db: Session = Depends(get_db), search: str = Query(None)):
|
||||
"""
|
||||
Get all videos in the library
|
||||
Get all videos in the library, optionally filtered by search query
|
||||
"""
|
||||
videos = db.query(Video).all()
|
||||
query = db.query(Video)
|
||||
if search:
|
||||
query = query.filter(
|
||||
(Video.title.ilike(f"%{search}%")) |
|
||||
(Video.path.ilike(f"%{search}%"))
|
||||
)
|
||||
videos = query.all()
|
||||
return videos
|
||||
|
||||
@app.delete("/videos/{video_id}")
|
||||
def delete_video(video_id: int, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Delete a video from the library by its ID
|
||||
"""
|
||||
video = db.query(Video).filter(Video.id == video_id).first()
|
||||
if not video:
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
|
||||
db.delete(video)
|
||||
db.commit()
|
||||
return {"message": f"Video with ID {video_id} deleted successfully"}
|
||||
|
||||
@app.get("/videos/{video_id}/stream")
|
||||
def stream_video(video_id: int, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Stream a video file by its ID
|
||||
"""
|
||||
print(f"Attempting to stream video with ID: {video_id}") # Log
|
||||
video = db.query(Video).filter(Video.id == video_id).first()
|
||||
if not video:
|
||||
print(f"Video with ID {video_id} not found in database.") # Log
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
|
||||
if not os.path.exists(video.path):
|
||||
print(f"Video file not found on server: {video.path}") # Log
|
||||
raise HTTPException(status_code=404, detail="Video file not found on server")
|
||||
|
||||
print(f"Streaming video from path: {video.path}") # Log
|
||||
return FileResponse(video.path, media_type="video/mp4")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
|
||||
@app.get("/video-paths/")
|
||||
def get_video_paths():
|
||||
"""
|
||||
Get all video paths in the library
|
||||
"""
|
||||
return {"paths": video_paths}
|
||||
|
||||
@app.post("/scan-videos/")
|
||||
def scan_videos():
|
||||
"""
|
||||
Scan all video paths and return the videos found
|
||||
"""
|
||||
all_videos = []
|
||||
for path in video_paths:
|
||||
try:
|
||||
videos = scan_video_directory(path)
|
||||
all_videos.extend(videos)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
return {"videos": all_videos, "count": len(all_videos)}
|
||||
|
||||
@app.get("/videos/")
|
||||
def get_videos():
|
||||
"""
|
||||
Get all videos in the library
|
||||
"""
|
||||
# This is a placeholder - in a real implementation, we would query the database
|
||||
return {"message": "List of videos"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
|
@ -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
|
||||
from typing import List
|
||||
from models import Video
|
||||
import ffmpeg
|
||||
from PIL import Image
|
||||
|
||||
def generate_thumbnail(video_path: str, thumbnail_path: str):
|
||||
"""
|
||||
Generates a thumbnail from a video file.
|
||||
"""
|
||||
try:
|
||||
# Extract a frame at 1 second mark
|
||||
(
|
||||
ffmpeg
|
||||
.input(video_path)
|
||||
.output(thumbnail_path, vframes=1)
|
||||
.overwrite_output()
|
||||
.run(capture_stdout=True, capture_stderr=True)
|
||||
)
|
||||
except ffmpeg.Error as e:
|
||||
print(f"Error generating thumbnail for {video_path}: {e.stderr.decode()}")
|
||||
# Fallback to a default thumbnail or handle error
|
||||
# For now, we'll just let it fail and the frontend will use a placeholder
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
|
||||
def scan_video_directory(directory_path: str) -> List[dict]:
|
||||
"""
|
||||
|
|
@ -12,17 +35,29 @@ def scan_video_directory(directory_path: str) -> List[dict]:
|
|||
if not os.path.exists(directory_path):
|
||||
raise FileNotFoundError(f"Directory {directory_path} does not exist")
|
||||
|
||||
# Create a directory for thumbnails if it doesn't exist
|
||||
thumbnails_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "thumbnails")
|
||||
os.makedirs(thumbnails_dir, exist_ok=True)
|
||||
|
||||
for root, dirs, files in os.walk(directory_path):
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1].lower() in video_extensions:
|
||||
full_path = os.path.join(root, file)
|
||||
stat = os.stat(full_path)
|
||||
|
||||
# Generate thumbnail path
|
||||
thumbnail_filename = f"{os.path.splitext(file)[0]}.jpg"
|
||||
thumbnail_path = os.path.join(thumbnails_dir, thumbnail_filename)
|
||||
|
||||
# Generate thumbnail
|
||||
generate_thumbnail(full_path, thumbnail_path)
|
||||
|
||||
video_info = {
|
||||
"title": os.path.splitext(file)[0],
|
||||
"path": full_path,
|
||||
"size": stat.st_size,
|
||||
"thumbnail_path": thumbnail_path if os.path.exists(thumbnail_path) else None,
|
||||
}
|
||||
videos.append(video_info)
|
||||
|
||||
return videos
|
||||
return videos
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-player": "2.16.1",
|
||||
"react-pro-sidebar": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -1901,6 +1902,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
|
|
@ -2714,6 +2724,12 @@
|
|||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/load-script": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
|
||||
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
|
|
@ -2737,6 +2753,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -2756,6 +2784,12 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
|
|
@ -2829,6 +2863,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
|
|
@ -3002,6 +3045,17 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
|
@ -3039,6 +3093,12 @@
|
|||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
|
|
@ -3054,6 +3114,22 @@
|
|||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-player": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.1.tgz",
|
||||
"integrity": "sha512-mxP6CqjSWjidtyDoMOSHVPdhX0pY16aSvw5fVr44EMaT7X5Xz46uQ4b/YBm1v2x+3hHkB9PmjEEkmbHb9PXQ4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.0.0",
|
||||
"load-script": "^1.0.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-pro-sidebar": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-pro-sidebar/-/react-pro-sidebar-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-player": "2.16.1",
|
||||
"react-pro-sidebar": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -24,23 +24,6 @@
|
|||
margin-left: 80px; /* Same as collapsed sidebar width */
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.path-section {
|
||||
padding: 10px 20px;
|
||||
|
|
@ -100,9 +83,27 @@
|
|||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Scan Button */
|
||||
.scan-btn {
|
||||
.remove-path-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #dc3545;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-left: auto; /* Pushes the button to the right */
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-path-btn:hover {
|
||||
color: #c82333;
|
||||
}
|
||||
|
||||
.scan-btn-sidebar {
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
margin-top: 10px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
|
|
@ -112,11 +113,11 @@
|
|||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.scan-btn:hover {
|
||||
.scan-btn-sidebar:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.scan-btn:disabled {
|
||||
.scan-btn-sidebar:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
|
||||
.video-details {
|
||||
flex-grow: 1;
|
||||
position: relative; /* For positioning edit/delete buttons */
|
||||
}
|
||||
|
||||
.video-title {
|
||||
|
|
@ -198,11 +200,7 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-channel, .video-stats {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
|
||||
.video-path, .video-size {
|
||||
margin: 4px 0;
|
||||
|
|
@ -210,3 +208,75 @@
|
|||
color: #606060;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Video Player Modal */
|
||||
.video-player-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.video-player-modal-content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 90%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.close-player-modal {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.video-player-modal-content h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 22px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.player-wrapper {
|
||||
position: relative;
|
||||
padding-top: 56.25%; /* 16:9 Aspect Ratio */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.react-player {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Sidebar, Menu, MenuItem, SubMenu } from 'react-pro-sidebar';
|
||||
import { FaBars, FaHome, FaFolder, FaVideo, FaPlus, FaList } from 'react-icons/fa';
|
||||
import { FaBars, FaHome, FaFolder, FaVideo, FaPlus, FaList, FaTrash } from 'react-icons/fa';
|
||||
import ReactPlayer from 'react-player';
|
||||
import './App.css';
|
||||
|
||||
const API_BASE_URL = 'http://192.168.2.220:8000';
|
||||
|
|
@ -12,6 +13,9 @@ function App() {
|
|||
const [videos, setVideos] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [selectedVideo, setSelectedVideo] = useState(null);
|
||||
const [showPlayerModal, setShowPlayerModal] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Fetch initial data on component mount
|
||||
useEffect(() => {
|
||||
|
|
@ -28,15 +32,26 @@ function App() {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchVideos = async () => {
|
||||
const fetchVideos = async (search = '') => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/videos/`);
|
||||
const response = await axios.get(`${API_BASE_URL}/videos/`, {
|
||||
params: { search: search }
|
||||
});
|
||||
setVideos(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching videos:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a new useEffect to trigger search when searchQuery changes
|
||||
useEffect(() => {
|
||||
const delayDebounceFn = setTimeout(() => {
|
||||
fetchVideos(searchQuery);
|
||||
}, 500); // Debounce for 500ms
|
||||
|
||||
return () => clearTimeout(delayDebounceFn);
|
||||
}, [searchQuery]); // Re-run when searchQuery changes
|
||||
|
||||
const addVideoPath = async () => {
|
||||
if (!newPath.trim()) return;
|
||||
|
||||
|
|
@ -52,6 +67,18 @@ function App() {
|
|||
}
|
||||
};
|
||||
|
||||
const removeVideoPath = async (pathToRemove) => {
|
||||
try {
|
||||
await axios.delete(`${API_BASE_URL}/video-paths/`, {
|
||||
params: { path: pathToRemove }
|
||||
});
|
||||
fetchVideoPaths(); // Refresh the list of paths
|
||||
} catch (error) {
|
||||
console.error('Error removing video path:', error);
|
||||
alert('Error removing video path: ' + (error.response?.data?.detail || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const scanVideos = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
|
@ -66,15 +93,28 @@ function App() {
|
|||
}
|
||||
};
|
||||
|
||||
const openVideoPlayer = (video) => {
|
||||
console.log('Attempting to open video player for:', video);
|
||||
setSelectedVideo(video);
|
||||
setShowPlayerModal(true);
|
||||
};
|
||||
|
||||
const closeVideoPlayer = () => {
|
||||
console.log('Closing video player modal.');
|
||||
setShowPlayerModal(false);
|
||||
// Delay setting selectedVideo to null to allow for unmount animation if any
|
||||
setTimeout(() => setSelectedVideo(null), 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`App ${collapsed ? 'collapsed' : ''}`}>
|
||||
<Sidebar collapsed={collapsed} className="app-sidebar">
|
||||
<Menu iconShape="square">
|
||||
<MenuItem icon={<FaBars />} onClick={() => setCollapsed(!collapsed)}>
|
||||
<h2>{collapsed ? '' : 'Menu'}</h2>
|
||||
<h2>{collapsed ? '' : 'Video App'}</h2>
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FaHome />}>Home</MenuItem>
|
||||
<SubMenu icon={<FaFolder />} title="Management">
|
||||
<SubMenu icon={<FaFolder />} label="Library">
|
||||
<div className="path-section">
|
||||
<h2>Video Library Paths</h2>
|
||||
<div className="path-form">
|
||||
|
|
@ -95,13 +135,25 @@ function App() {
|
|||
{videoPaths.length > 0 ? (
|
||||
<ul>
|
||||
{videoPaths.map((path, index) => (
|
||||
<li key={index}><FaList /> {path}</li>
|
||||
<li key={index}>
|
||||
<FaList /> {path}
|
||||
<button onClick={() => removeVideoPath(path)} className="remove-path-btn">
|
||||
<FaTrash />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No video paths added yet</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={scanVideos}
|
||||
disabled={loading}
|
||||
className="scan-btn-sidebar"
|
||||
>
|
||||
{loading ? 'Scanning...' : 'Scan Videos'}
|
||||
</button>
|
||||
</div>
|
||||
</SubMenu>
|
||||
<MenuItem icon={<FaVideo />}>Videos</MenuItem>
|
||||
|
|
@ -109,24 +161,22 @@ function App() {
|
|||
</Sidebar>
|
||||
|
||||
<main className="app-main">
|
||||
<header className="app-header">
|
||||
<h1>Video Organization App</h1>
|
||||
<button
|
||||
onClick={scanVideos}
|
||||
disabled={loading}
|
||||
className="scan-btn"
|
||||
>
|
||||
{loading ? 'Scanning...' : 'Scan Videos'}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section className="videos-section">
|
||||
<h2>Video Library</h2>
|
||||
<div className="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search videos by title or path..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
{videos.length > 0 ? (
|
||||
<div className="video-grid">
|
||||
{videos.map((video) => (
|
||||
<div key={video.id} className="video-card">
|
||||
<div className="video-thumbnail">
|
||||
<div className="video-thumbnail" onClick={() => openVideoPlayer(video)}>
|
||||
<div className="thumbnail-placeholder">▶</div>
|
||||
</div>
|
||||
<div className="video-info">
|
||||
|
|
@ -145,6 +195,35 @@ function App() {
|
|||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{showPlayerModal && selectedVideo && (
|
||||
<div className="video-player-modal-overlay" onClick={closeVideoPlayer}>
|
||||
<div className="video-player-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="close-player-modal" onClick={closeVideoPlayer}>×</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
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