1273 lines
37 KiB
Python
1273 lines
37 KiB
Python
import os
|
||
import subprocess
|
||
import sys
|
||
import json
|
||
import shutil
|
||
from pathlib import Path
|
||
|
||
class ProjectSetup:
|
||
def __init__(self):
|
||
self.root_dir = Path.cwd()
|
||
self.backend_dir = self.root_dir / "backend"
|
||
self.frontend_dir = self.root_dir / "frontend"
|
||
self.venv_python = None
|
||
|
||
def clean(self):
|
||
"""Очистка старых файлов и директорий"""
|
||
print("Очистка проекта...")
|
||
if self.backend_dir.exists():
|
||
shutil.rmtree(self.backend_dir)
|
||
if self.frontend_dir.exists():
|
||
shutil.rmtree(self.frontend_dir)
|
||
|
||
def create_backend(self):
|
||
"""Настройка backend"""
|
||
print("\nНастройка backend...")
|
||
|
||
# Создаем структуру директорий
|
||
app_dir = self.backend_dir / "app"
|
||
app_dir.mkdir(parents=True)
|
||
|
||
# Путь к Python 3.11
|
||
python_path = "python.exe" # Измените путь на ваш
|
||
|
||
# Создаем виртуальное окружение с Python 3.11
|
||
print("Создание виртуального окружения...")
|
||
subprocess.run([python_path, "-m", "venv", str(self.backend_dir / "venv")], check=True)
|
||
|
||
# Определяем путь к Python в виртуальном окружении
|
||
if sys.platform == "win32":
|
||
self.venv_python = self.backend_dir / "venv" / "Scripts" / "python.exe"
|
||
else:
|
||
self.venv_python = self.backend_dir / "venv" / "bin" / "python"
|
||
|
||
# Обновляем pip
|
||
subprocess.run([str(self.venv_python), "-m", "pip", "install", "--upgrade", "pip"], check=True)
|
||
|
||
# Создаем файлы проекта
|
||
self.create_backend_files()
|
||
|
||
# Устанавливаем зависимости
|
||
print("Установка зависимостей...")
|
||
requirements_txt = self.backend_dir / "requirements.txt"
|
||
subprocess.run([str(self.venv_python), "-m", "pip", "install", "-r", str(requirements_txt)], check=True)
|
||
|
||
# Инициализируем базу данных
|
||
self.init_database()
|
||
|
||
def create_frontend(self):
|
||
"""Настройка frontend"""
|
||
print("\nНастройка frontend...")
|
||
self.frontend_dir.mkdir(parents=True)
|
||
os.chdir(self.frontend_dir)
|
||
|
||
# Инициализируем package.json
|
||
package_json = {
|
||
"name": "smart-player-frontend",
|
||
"private": True,
|
||
"version": "0.1.0",
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "vite",
|
||
"build": "tsc && vite build",
|
||
"preview": "vite preview"
|
||
},
|
||
"dependencies": {
|
||
"@emotion/react": "^11.11.0",
|
||
"@emotion/styled": "^11.11.0",
|
||
"@mui/icons-material": "^5.11.16",
|
||
"@mui/material": "^5.13.0",
|
||
"axios": "^1.4.0",
|
||
"react": "^18.2.0",
|
||
"react-dom": "^18.2.0",
|
||
"react-router-dom": "^6.11.1",
|
||
"zustand": "^4.3.8"
|
||
},
|
||
"devDependencies": {
|
||
"@types/react": "^18.2.6",
|
||
"@types/react-dom": "^18.2.4",
|
||
"@vitejs/plugin-react": "^4.0.0",
|
||
"typescript": "^5.0.4",
|
||
"vite": "^4.3.5"
|
||
}
|
||
}
|
||
|
||
with open("package.json", "w") as f:
|
||
json.dump(package_json, f, indent=2)
|
||
|
||
# Устанавливаем зависимости
|
||
print("Установка npm пакетов...")
|
||
subprocess.run("npm install", shell=True, check=True)
|
||
|
||
# Создаем файлы frontend
|
||
self.create_frontend_files()
|
||
|
||
def create_backend_files(self):
|
||
"""Создание файлов backend"""
|
||
files = {
|
||
self.backend_dir / "app" / "__init__.py": "",
|
||
|
||
self.backend_dir / "app" / "config.py": '''
|
||
from pydantic_settings import BaseSettings
|
||
|
||
class Settings(BaseSettings):
|
||
DATABASE_URL: str = "sqlite:///./smartplayer.db"
|
||
SECRET_KEY: str = "your-super-secret-key-keep-it-safe"
|
||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||
ALGORITHM: str = "HS256"
|
||
|
||
model_config = {
|
||
"env_file": ".env",
|
||
"env_file_encoding": "utf-8"
|
||
}
|
||
|
||
settings = Settings()
|
||
''',
|
||
|
||
self.backend_dir / "app" / "database.py": '''
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.ext.declarative import declarative_base
|
||
from sqlalchemy.orm import sessionmaker
|
||
from .config import settings
|
||
|
||
engine = create_engine(settings.DATABASE_URL, connect_args={"check_same_thread": False})
|
||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||
Base = declarative_base()
|
||
|
||
def get_db():
|
||
db = SessionLocal()
|
||
try:
|
||
yield db
|
||
finally:
|
||
db.close()
|
||
''',
|
||
|
||
self.backend_dir / "app" / "models.py": '''
|
||
from sqlalchemy import Boolean, Column, Integer, String, DateTime, ForeignKey, Table, Enum
|
||
import enum
|
||
from sqlalchemy.orm import relationship
|
||
from datetime import datetime
|
||
from .database import Base
|
||
|
||
class MediaType(str, enum.Enum):
|
||
AUDIO = "audio"
|
||
VIDEO = "video"
|
||
STREAM = "stream"
|
||
|
||
# Таблица для связи many-to-many между плейлистами и медиа
|
||
playlist_media = Table('playlist_media', Base.metadata,
|
||
Column('playlist_id', Integer, ForeignKey('playlists.id')),
|
||
Column('media_id', Integer, ForeignKey('media.id'))
|
||
)
|
||
|
||
class User(Base):
|
||
__tablename__ = "users"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
email = Column(String, unique=True, index=True)
|
||
username = Column(String, unique=True, index=True)
|
||
hashed_password = Column(String)
|
||
is_active = Column(Boolean, default=True)
|
||
is_admin = Column(Boolean, default=False)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
# Связи
|
||
playlists = relationship("Playlist", back_populates="owner")
|
||
media_items = relationship("Media", back_populates="owner")
|
||
streams = relationship("Stream", back_populates="owner")
|
||
|
||
class Media(Base):
|
||
__tablename__ = "media"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
title = Column(String, index=True)
|
||
artist = Column(String, index=True)
|
||
duration = Column(Integer) # Длительность в секундах
|
||
file_path = Column(String)
|
||
media_type = Column(Enum(MediaType))
|
||
thumbnail_path = Column(String, nullable=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
owner_id = Column(Integer, ForeignKey("users.id"))
|
||
|
||
owner = relationship("User", back_populates="media_items")
|
||
playlists = relationship("Playlist", secondary=playlist_media, back_populates="media")
|
||
|
||
class Stream(Base):
|
||
__tablename__ = "streams"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
title = Column(String, index=True)
|
||
description = Column(String, nullable=True)
|
||
stream_key = Column(String, unique=True)
|
||
is_live = Column(Boolean, default=False)
|
||
thumbnail_path = Column(String, nullable=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
owner_id = Column(Integer, ForeignKey("users.id"))
|
||
|
||
owner = relationship("User", back_populates="streams")
|
||
|
||
class Playlist(Base):
|
||
__tablename__ = "playlists"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
name = Column(String, index=True)
|
||
description = Column(String)
|
||
is_public = Column(Boolean, default=False)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
owner_id = Column(Integer, ForeignKey("users.id"))
|
||
|
||
owner = relationship("User", back_populates="playlists")
|
||
media = relationship("Media", secondary=playlist_media, back_populates="playlists")
|
||
''',
|
||
|
||
self.backend_dir / "app" / "schemas.py": '''
|
||
from pydantic import BaseModel, EmailStr
|
||
from typing import Optional, List
|
||
from datetime import datetime
|
||
from .models import MediaType
|
||
|
||
class UserBase(BaseModel):
|
||
email: EmailStr
|
||
username: str
|
||
|
||
class User(BaseModel):
|
||
id: int
|
||
is_active: bool
|
||
is_admin: bool
|
||
created_at: datetime
|
||
|
||
model_config = {
|
||
"from_attributes": True
|
||
}
|
||
|
||
class Media(BaseModel):
|
||
id: int
|
||
file_path: str
|
||
created_at: datetime
|
||
owner_id: int
|
||
|
||
model_config = {
|
||
"from_attributes": True
|
||
}
|
||
|
||
class Stream(BaseModel):
|
||
id: int
|
||
stream_key: str
|
||
is_live: bool
|
||
created_at: datetime
|
||
owner_id: int
|
||
|
||
model_config = {
|
||
"from_attributes": True
|
||
}
|
||
|
||
class Playlist(BaseModel):
|
||
id: int
|
||
created_at: datetime
|
||
owner_id: int
|
||
media_items: List[Media] = []
|
||
|
||
model_config = {
|
||
"from_attributes": True
|
||
}
|
||
|
||
class UserCreate(UserBase):
|
||
password: str
|
||
|
||
class User(UserBase):
|
||
id: int
|
||
is_active: bool
|
||
is_admin: bool
|
||
created_at: datetime
|
||
|
||
class Config:
|
||
orm_mode = True
|
||
|
||
class Token(BaseModel):
|
||
access_token: str
|
||
token_type: str
|
||
user: User
|
||
|
||
class MediaBase(BaseModel):
|
||
title: str
|
||
artist: str
|
||
duration: int
|
||
media_type: MediaType
|
||
thumbnail_path: Optional[str] = None
|
||
|
||
class MediaCreate(MediaBase):
|
||
file_path: str
|
||
|
||
class Media(MediaBase):
|
||
id: int
|
||
file_path: str
|
||
created_at: datetime
|
||
owner_id: int
|
||
|
||
class Config:
|
||
orm_mode = True
|
||
|
||
class StreamBase(BaseModel):
|
||
title: str
|
||
description: Optional[str] = None
|
||
thumbnail_path: Optional[str] = None
|
||
|
||
class StreamCreate(StreamBase):
|
||
pass
|
||
|
||
class Stream(StreamBase):
|
||
id: int
|
||
stream_key: str
|
||
is_live: bool
|
||
created_at: datetime
|
||
owner_id: int
|
||
|
||
class Config:
|
||
orm_mode = True
|
||
|
||
class PlaylistBase(BaseModel):
|
||
name: str
|
||
description: Optional[str] = None
|
||
is_public: bool = False
|
||
|
||
class PlaylistCreate(PlaylistBase):
|
||
pass
|
||
|
||
class Playlist(PlaylistBase):
|
||
id: int
|
||
created_at: datetime
|
||
owner_id: int
|
||
media_items: List[Media] = []
|
||
|
||
class Config:
|
||
orm_mode = True
|
||
''',
|
||
|
||
self.backend_dir / "app" / "auth.py": '''
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional
|
||
from jose import JWTError, jwt
|
||
from passlib.context import CryptContext
|
||
from fastapi import Depends, HTTPException, status
|
||
from fastapi.security import OAuth2PasswordBearer
|
||
from sqlalchemy.orm import Session
|
||
from . import models, schemas
|
||
from .database import get_db
|
||
from .config import settings
|
||
|
||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||
|
||
# Определите oauth2_scheme
|
||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # Добавьте эту строку
|
||
|
||
def verify_password(plain_password, hashed_password):
|
||
return pwd_context.verify(plain_password, hashed_password)
|
||
|
||
def get_password_hash(password):
|
||
return pwd_context.hash(password)
|
||
|
||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||
to_encode = data.copy()
|
||
if expires_delta:
|
||
expire = datetime.utcnow() + expires_delta
|
||
else:
|
||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||
to_encode.update({"exp": expire})
|
||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||
return encoded_jwt
|
||
|
||
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
||
credentials_exception = HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Could not validate credentials",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
try:
|
||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||
username: str = payload.get("sub")
|
||
if username is None:
|
||
raise credentials_exception
|
||
except JWTError:
|
||
raise credentials_exception
|
||
|
||
user = db.query(models.User).filter(models.User.username == username).first()
|
||
if user is None:
|
||
raise credentials_exception
|
||
return user
|
||
|
||
def get_current_active_user(current_user: models.User = Depends(get_current_user)):
|
||
if not current_user.is_active:
|
||
raise HTTPException(status_code=400, detail="Inactive user")
|
||
return current_user
|
||
''',
|
||
|
||
self.backend_dir / "app" / "main.py": '''
|
||
from fastapi import FastAPI, Depends, HTTPException, status, File, UploadFile
|
||
from fastapi.security import OAuth2PasswordRequestForm
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
from sqlalchemy.orm import Session
|
||
from typing import List, Optional
|
||
import os
|
||
import secrets
|
||
from . import models, schemas, auth
|
||
from .database import engine, get_db
|
||
from .models import MediaType
|
||
|
||
models.Base.metadata.create_all(bind=engine)
|
||
|
||
app = FastAPI(title="Smart Player API")
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["http://localhost:5173"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
@app.post("/token", response_model=schemas.Token)
|
||
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||
user = db.query(models.User).filter(models.User.username == form_data.username).first()
|
||
if not user or not auth.verify_password(form_data.password, user.hashed_password):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Incorrect username or password",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
|
||
access_token = auth.create_access_token(data={"sub": user.username})
|
||
return {
|
||
"access_token": access_token,
|
||
"token_type": "bearer",
|
||
"user": user
|
||
}
|
||
|
||
@app.get("/users/me", response_model=schemas.User)
|
||
async def read_users_me(current_user: models.User = Depends(auth.get_current_active_user)):
|
||
return current_user
|
||
|
||
@app.post("/upload/", response_model=schemas.Media)
|
||
async def upload_media(
|
||
file: UploadFile = File(...),
|
||
title: str = Form(...),
|
||
artist: str = Form(...),
|
||
media_type: MediaType = Form(...),
|
||
db: Session = Depends(get_db),
|
||
current_user: models.User = Depends(auth.get_current_active_user)
|
||
):
|
||
# Сохраняем файл на диск
|
||
file_path = f"uploads/{file.filename}"
|
||
with open(file_path, "wb") as buffer:
|
||
buffer.write(await file.read())
|
||
|
||
# Создаем запись в базе данных
|
||
new_media = models.Media(
|
||
title=title,
|
||
artist=artist,
|
||
duration=0, # Можно рассчитать длительность позже
|
||
file_path=file_path,
|
||
media_type=media_type,
|
||
owner_id=current_user.id
|
||
)
|
||
db.add(new_media)
|
||
db.commit()
|
||
db.refresh(new_media)
|
||
return new_media
|
||
|
||
@app.post("/media/", response_model=schemas.Media)
|
||
async def create_media(
|
||
file: UploadFile = File(...),
|
||
title: str = Form(...),
|
||
artist: str = Form(...),
|
||
media_type: MediaType = Form(...),
|
||
db: Session = Depends(get_db),
|
||
current_user: models.User = Depends(auth.get_current_active_user)
|
||
):
|
||
# Логика обработки загрузки файла
|
||
pass
|
||
|
||
@app.post("/streams/", response_model=schemas.Stream)
|
||
async def create_stream(
|
||
title: str = Form(...),
|
||
description: Optional[str] = Form(None),
|
||
db: Session = Depends(get_db),
|
||
current_user: models.User = Depends(auth.get_current_active_user)
|
||
):
|
||
# Логика создания потока
|
||
pass
|
||
''',
|
||
|
||
self.backend_dir / "create_admin.py": '''
|
||
import sys
|
||
from pathlib import Path
|
||
backend_dir = Path(__file__).resolve().parent
|
||
sys.path.insert(0, str(backend_dir))
|
||
|
||
from app.database import SessionLocal, engine, Base
|
||
from app.models import User
|
||
from app.auth import get_password_hash
|
||
|
||
def create_admin():
|
||
Base.metadata.create_all(bind=engine)
|
||
db = SessionLocal()
|
||
try:
|
||
admin = db.query(User).filter(User.username == "admin").first()
|
||
if not admin:
|
||
admin = User(
|
||
email="admin@example.com",
|
||
username="admin",
|
||
hashed_password=get_password_hash("admin"),
|
||
is_active=True,
|
||
is_admin=True
|
||
)
|
||
db.add(admin)
|
||
db.commit()
|
||
print("Админ создан успешно!")
|
||
print("Логин: admin")
|
||
print("Пароль: admin")
|
||
else:
|
||
print("Админ уже существует")
|
||
finally:
|
||
db.close()
|
||
|
||
if __name__ == "__main__":
|
||
create_admin()
|
||
''',
|
||
|
||
self.backend_dir / "requirements.txt": '''
|
||
fastapi==0.103.1
|
||
uvicorn==0.23.2
|
||
sqlalchemy==1.4.46
|
||
pydantic==2.10.6
|
||
pydantic-settings==2.8.0
|
||
python-jose[cryptography]==3.4.0
|
||
passlib==1.7.4
|
||
bcrypt==4.0.1
|
||
python-multipart==0.0.6
|
||
python-dotenv==1.0.0
|
||
email-validator==2.0.0.post1
|
||
''',
|
||
}
|
||
|
||
# Создаем все файлы
|
||
for file_path, content in files.items():
|
||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
f.write(content.strip())
|
||
|
||
def create_frontend_files(self):
|
||
"""Создание файлов frontend"""
|
||
files = {
|
||
self.frontend_dir / "index.html": '''
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Smart Player</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "main.tsx": '''
|
||
import React from 'react'
|
||
import ReactDOM from 'react-dom/client'
|
||
import { CssBaseline } from '@mui/material'
|
||
import App from './App'
|
||
|
||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||
<React.StrictMode>
|
||
<CssBaseline />
|
||
<App />
|
||
</React.StrictMode>,
|
||
)
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "App.tsx": '''
|
||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||
import { ThemeProvider, createTheme } from '@mui/material';
|
||
import { LoginPage } from './components/LoginPage';
|
||
import { Layout } from './components/Layout';
|
||
import { useAuthStore } from './store/authStore';
|
||
import { RegisterPage } from './components/RegisterPage';
|
||
|
||
const theme = createTheme();
|
||
|
||
const PrivateRoute = ({ children }: { children: React.ReactNode }) => {
|
||
const isAuthenticated = useAuthStore(state => state.isAuthenticated);
|
||
return isAuthenticated ? <>{children}</> : <Navigate to="/login" />;
|
||
};
|
||
|
||
function App() {
|
||
return (
|
||
<ThemeProvider theme={theme}>
|
||
<BrowserRouter>
|
||
<Routes>
|
||
<Route path="/login" element={<LoginPage />} />
|
||
<Route path="/register" element={<RegisterPage />} />
|
||
<Route path="/" element={
|
||
<PrivateRoute>
|
||
<Layout />
|
||
</PrivateRoute>
|
||
} />
|
||
</Routes>
|
||
</BrowserRouter>
|
||
</ThemeProvider>
|
||
);
|
||
}
|
||
|
||
export default App;
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "store" / "authStore.ts": '''
|
||
import create from 'zustand'
|
||
|
||
interface User {
|
||
id: number;
|
||
username: string;
|
||
email: string;
|
||
is_admin: boolean;
|
||
}
|
||
|
||
interface AuthState {
|
||
isAuthenticated: boolean;
|
||
user: User | null;
|
||
token: string | null;
|
||
login: (username: string, password: string) => Promise<void>;
|
||
logout: () => void;
|
||
}
|
||
|
||
export const useAuthStore = create<AuthState>((set) => ({
|
||
isAuthenticated: !!localStorage.getItem('token'),
|
||
user: JSON.parse(localStorage.getItem('user') || 'null'),
|
||
token: localStorage.getItem('token'),
|
||
|
||
login: async (username: string, password: string) => {
|
||
const formData = new FormData();
|
||
formData.append('username', username);
|
||
formData.append('password', password);
|
||
|
||
const response = await fetch('http://localhost:8000/token', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Auth failed');
|
||
|
||
const data = await response.json();
|
||
set({
|
||
isAuthenticated: true,
|
||
user: data.user,
|
||
token: data.access_token
|
||
});
|
||
|
||
localStorage.setItem('token', data.access_token);
|
||
localStorage.setItem('user', JSON.stringify(data.user));
|
||
},
|
||
|
||
logout: () => {
|
||
set({ isAuthenticated: false, user: null, token: null });
|
||
localStorage.removeItem('token');
|
||
localStorage.removeItem('user');
|
||
}
|
||
}));
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "components" / "LoginPage.tsx": '''
|
||
import { useState } from 'react';
|
||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||
import { Box, Button, TextField, Typography, Container, Paper, Alert, Link } from '@mui/material';
|
||
import { useAuthStore } from '../store/authStore';
|
||
|
||
export const LoginPage = () => {
|
||
const [username, setUsername] = useState('');
|
||
const [password, setPassword] = useState('');
|
||
const [error, setError] = useState('');
|
||
const login = useAuthStore(state => state.login);
|
||
const navigate = useNavigate();
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
try {
|
||
await login(username, password);
|
||
navigate('/');
|
||
} catch (err) {
|
||
setError('Неверные учетные данные');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Container component="main" maxWidth="xs">
|
||
<Box sx={{
|
||
marginTop: 8,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
}}>
|
||
<Paper elevation={3} sx={{ p: 4, width: '100%' }}>
|
||
<Typography component="h1" variant="h5" align="center">
|
||
Smart Player
|
||
</Typography>
|
||
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
|
||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||
<TextField
|
||
margin="normal"
|
||
required
|
||
fullWidth
|
||
label="Имя пользователя"
|
||
autoFocus
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
/>
|
||
<TextField
|
||
margin="normal"
|
||
required
|
||
fullWidth
|
||
label="Пароль"
|
||
type="password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
/>
|
||
<Button
|
||
type="submit"
|
||
fullWidth
|
||
variant="contained"
|
||
sx={{ mt: 3, mb: 2 }}
|
||
>
|
||
Войти
|
||
</Button>
|
||
<Box sx={{ textAlign: 'center' }}>
|
||
<Link component={RouterLink} to="/register" variant="body2">
|
||
Нет аккаунта? Зарегистрироваться
|
||
</Link>
|
||
</Box>
|
||
</Box>
|
||
</Paper>
|
||
</Box>
|
||
</Container>
|
||
);
|
||
};
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "components" / "Layout.tsx": '''
|
||
import { useState } from 'react';
|
||
import { Box, Drawer, AppBar, Toolbar, Typography, List, ListItem, ListItemIcon, ListItemText, IconButton } from '@mui/material';
|
||
import { Menu as MenuIcon, Dashboard as DashboardIcon, ExitToApp as LogoutIcon } from '@mui/icons-material';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useAuthStore } from '../store/authStore';
|
||
|
||
const drawerWidth = 240;
|
||
|
||
export const Layout = () => {
|
||
const [mobileOpen, setMobileOpen] = useState(false);
|
||
const navigate = useNavigate();
|
||
const logout = useAuthStore(state => state.logout);
|
||
|
||
const handleDrawerToggle = () => {
|
||
setMobileOpen(!mobileOpen);
|
||
};
|
||
|
||
const handleLogout = () => {
|
||
logout();
|
||
navigate('/login');
|
||
};
|
||
|
||
const drawer = (
|
||
<div>
|
||
<Toolbar>
|
||
<Typography variant="h6" noWrap component="div">
|
||
Smart Player
|
||
</Typography>
|
||
</Toolbar>
|
||
<List>
|
||
<ListItem button>
|
||
<ListItemIcon>
|
||
<DashboardIcon />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Панель управления" />
|
||
</ListItem>
|
||
<ListItem button onClick={handleLogout}>
|
||
<ListItemIcon>
|
||
<LogoutIcon />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Выход" />
|
||
</ListItem>
|
||
</List>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<Box sx={{ display: 'flex' }}>
|
||
<AppBar position="fixed">
|
||
<Toolbar>
|
||
<IconButton
|
||
color="inherit"
|
||
aria-label="open drawer"
|
||
edge="start"
|
||
onClick={handleDrawerToggle}
|
||
sx={{ mr: 2, display: { sm: 'none' } }}
|
||
>
|
||
<MenuIcon />
|
||
</IconButton>
|
||
<Typography variant="h6" noWrap component="div">
|
||
Smart Player
|
||
</Typography>
|
||
</Toolbar>
|
||
</AppBar>
|
||
<Box
|
||
component="nav"
|
||
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
|
||
>
|
||
<Drawer
|
||
variant="temporary"
|
||
open={mobileOpen}
|
||
onClose={handleDrawerToggle}
|
||
ModalProps={{ keepMounted: true }}
|
||
sx={{
|
||
display: { xs: 'block', sm: 'none' },
|
||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||
}}
|
||
>
|
||
{drawer}
|
||
</Drawer>
|
||
<Drawer
|
||
variant="permanent"
|
||
sx={{
|
||
display: { xs: 'none', sm: 'block' },
|
||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||
}}
|
||
open
|
||
>
|
||
{drawer}
|
||
</Drawer>
|
||
</Box>
|
||
<Box
|
||
component="main"
|
||
sx={{
|
||
flexGrow: 1,
|
||
p: 3,
|
||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||
marginTop: '64px'
|
||
}}
|
||
>
|
||
<Typography paragraph>
|
||
Добро пожаловать в Smart Player
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
};
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "vite-env.d.ts": '''
|
||
/// <reference types="vite/client" />
|
||
''',
|
||
|
||
self.frontend_dir / "tsconfig.json": '''
|
||
{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true,
|
||
"noUnusedLocals": true,
|
||
"noUnusedParameters": true,
|
||
"noFallthroughCasesInSwitch": true
|
||
},
|
||
"include": ["src"],
|
||
"references": [{ "path": "./tsconfig.node.json" }]
|
||
}
|
||
''',
|
||
|
||
self.frontend_dir / "tsconfig.node.json": '''
|
||
{
|
||
"compilerOptions": {
|
||
"composite": true,
|
||
"skipLibCheck": true,
|
||
"module": "ESNext",
|
||
"moduleResolution": "bundler",
|
||
"allowSyntheticDefaultImports": true
|
||
},
|
||
"include": ["vite.config.ts"]
|
||
}
|
||
''',
|
||
|
||
self.frontend_dir / "vite.config.ts": '''
|
||
import { defineConfig } from 'vite'
|
||
import react from '@vitejs/plugin-react'
|
||
|
||
export default defineConfig({
|
||
plugins: [react()],
|
||
})
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "components" / "RegisterPage.tsx": '''
|
||
import { useState } from 'react';
|
||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||
import {
|
||
Box,
|
||
Button,
|
||
TextField,
|
||
Typography,
|
||
Container,
|
||
Paper,
|
||
Alert,
|
||
Link
|
||
} from '@mui/material';
|
||
|
||
export const RegisterPage = () => {
|
||
const [username, setUsername] = useState('');
|
||
const [email, setEmail] = useState('');
|
||
const [password, setPassword] = useState('');
|
||
const [error, setError] = useState('');
|
||
const navigate = useNavigate();
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
try {
|
||
const response = await fetch('http://localhost:8000/register', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
username,
|
||
email,
|
||
password
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Registration failed');
|
||
}
|
||
|
||
// Регистрация успешна, перенаправляем на страницу входа
|
||
navigate('/login');
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Registration failed');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Container component="main" maxWidth="xs">
|
||
<Box sx={{
|
||
marginTop: 8,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
}}>
|
||
<Paper elevation={3} sx={{ p: 4, width: '100%' }}>
|
||
<Typography component="h1" variant="h5" align="center">
|
||
Регистрация
|
||
</Typography>
|
||
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
|
||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||
<TextField
|
||
margin="normal"
|
||
required
|
||
fullWidth
|
||
label="Имя пользователя"
|
||
autoFocus
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
/>
|
||
<TextField
|
||
margin="normal"
|
||
required
|
||
fullWidth
|
||
label="Email"
|
||
type="email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
/>
|
||
<TextField
|
||
margin="normal"
|
||
required
|
||
fullWidth
|
||
label="Пароль"
|
||
type="password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
/>
|
||
<Button
|
||
type="submit"
|
||
fullWidth
|
||
variant="contained"
|
||
sx={{ mt: 3, mb: 2 }}
|
||
>
|
||
Зарегистрироваться
|
||
</Button>
|
||
<Box sx={{ textAlign: 'center' }}>
|
||
<Link component={RouterLink} to="/login" variant="body2">
|
||
Уже есть аккаунт? Войти
|
||
</Link>
|
||
</Box>
|
||
</Box>
|
||
</Paper>
|
||
</Box>
|
||
</Container>
|
||
);
|
||
};
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "components" / "UploadMedia.tsx": '''
|
||
import { useState } from 'react';
|
||
import { Button, TextField, Dialog, DialogTitle, DialogContent, DialogActions, Box, Alert } from '@mui/material';
|
||
|
||
interface UploadMediaProps {
|
||
onUpload: () => void;
|
||
}
|
||
|
||
export const UploadMedia = ({ onUpload }: UploadMediaProps) => {
|
||
const [open, setOpen] = useState(false);
|
||
const [title, setTitle] = useState('');
|
||
const [artist, setArtist] = useState('');
|
||
const [mediaType, setMediaType] = useState('audio');
|
||
const [file, setFile] = useState<File | null>(null);
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('title', title);
|
||
formData.append('artist', artist);
|
||
formData.append('media_type', mediaType);
|
||
|
||
const response = await fetch('http://localhost:8000/upload/', {
|
||
method: 'POST',
|
||
body: formData,
|
||
headers: {
|
||
Authorization: `Bearer ${useAuthStore(state => state.token)}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
setOpen(false);
|
||
onUpload();
|
||
} else {
|
||
alert('Ошибка при загрузке файла');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<Button variant="contained" component="label" onClick={() => setOpen(true)}>
|
||
Загрузить медиа
|
||
</Button>
|
||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||
<DialogTitle>Загрузка медиа</DialogTitle>
|
||
<DialogContent>
|
||
<TextField
|
||
label="Название"
|
||
fullWidth
|
||
value={title}
|
||
onChange={(e) => setTitle(e.target.value)}
|
||
margin="normal"
|
||
/>
|
||
<TextField
|
||
label="Исполнитель"
|
||
fullWidth
|
||
value={artist}
|
||
onChange={(e) => setArtist(e.target.value)}
|
||
margin="normal"
|
||
/>
|
||
<select value={mediaType} onChange={(e) => setMediaType(e.target.value)}>
|
||
<option value="audio">Аудио</option>
|
||
<option value="video">Видео</option>
|
||
</select>
|
||
<input type="file" onChange={(e) => setFile(e.target.files?.[0])} />
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setOpen(false)}>Отмена</Button>
|
||
<Button onClick={handleSubmit}>Загрузить</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
};
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "components" / "CreateStream.tsx": '''
|
||
import { useState } from 'react';
|
||
import {
|
||
Button,
|
||
TextField,
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
Box,
|
||
Alert,
|
||
Typography
|
||
} from '@mui/material';
|
||
|
||
interface CreateStreamProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onCreated: () => void;
|
||
}
|
||
|
||
export const CreateStream = ({ open, onClose, onCreated }: CreateStreamProps) => {
|
||
// ... содержимое компонента ...
|
||
};
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "components" / "MediaPlayer.tsx": '''
|
||
import { useRef, useEffect } from 'react';
|
||
import { Box, Paper } from '@mui/material';
|
||
|
||
interface MediaPlayerProps {
|
||
src: string;
|
||
type: 'audio' | 'video';
|
||
poster?: string;
|
||
}
|
||
|
||
export const MediaPlayer = ({ src, type, poster }: MediaPlayerProps) => {
|
||
// ... содержимое компонента ...
|
||
};
|
||
''',
|
||
|
||
self.frontend_dir / "src" / "components" / "StreamPlayer.tsx": '''
|
||
import { useEffect, useRef } from 'react';
|
||
import { Box, Paper, Typography } from '@mui/material';
|
||
import Hls from 'hls.js';
|
||
|
||
interface StreamPlayerProps {
|
||
streamId: string;
|
||
title: string;
|
||
}
|
||
|
||
export const StreamPlayer = ({ streamId }: { streamId: string }) => {
|
||
const videoRef = useRef<HTMLVideoElement>(null);
|
||
|
||
useEffect(() => {
|
||
const video = videoRef.current;
|
||
if (video) {
|
||
const hls = new Hls();
|
||
hls.loadSource(`/streams/${streamId}/playlist.m3u8`);
|
||
hls.attachMedia(video);
|
||
return () => hls.destroy();
|
||
}
|
||
}, [streamId]);
|
||
|
||
return (
|
||
<video ref={videoRef} controls autoPlay width="640" height="360">
|
||
Ваш браузер не поддерживает HTML5 видео.
|
||
</video>
|
||
);
|
||
};
|
||
|
||
export const PlaylistManager = () => {
|
||
const [playlists, setPlaylists] = useState<Playlist[]>([]);
|
||
|
||
useEffect(() => {
|
||
fetch('http://localhost:8000/playlists/', {
|
||
headers: {
|
||
Authorization: `Bearer ${useAuthStore(state => state.token)}`
|
||
}
|
||
})
|
||
.then((res) => res.json())
|
||
.then(setPlaylists);
|
||
}, []);
|
||
|
||
return (
|
||
<Box>
|
||
<Typography variant="h5">Мои плейлисты</Typography>
|
||
{playlists.map((playlist) => (
|
||
<Paper key={playlist.id} elevation={3} style={{ padding: '16px', marginBottom: '16px' }}>
|
||
<Typography>{playlist.name}</Typography>
|
||
</Paper>
|
||
))}
|
||
</Box>
|
||
);
|
||
};
|
||
''',
|
||
|
||
self.frontend_dir / "package.json": '''
|
||
{
|
||
"name": "smart-player-frontend",
|
||
"private": true,
|
||
"version": "0.1.0",
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "vite",
|
||
"build": "tsc && vite build",
|
||
"preview": "vite preview"
|
||
},
|
||
"dependencies": {
|
||
"@emotion/react": "^11.11.0",
|
||
"@emotion/styled": "^11.11.0",
|
||
"@mui/icons-material": "^5.11.16",
|
||
"@mui/material": "^5.13.0",
|
||
"axios": "^1.4.0",
|
||
"react": "^18.2.0",
|
||
"react-dom": "^18.2.0",
|
||
"react-router-dom": "^6.11.1",
|
||
"zustand": "^4.3.8",
|
||
"hls.js": "^1.4.12"
|
||
},
|
||
"devDependencies": {
|
||
"@types/react": "^18.2.6",
|
||
"@types/react-dom": "^18.2.4",
|
||
"@vitejs/plugin-react": "^4.0.0",
|
||
"typescript": "^5.0.4",
|
||
"vite": "^4.3.5"
|
||
}
|
||
}
|
||
''',
|
||
}
|
||
|
||
# Создаем все файлы
|
||
for file_path, content in files.items():
|
||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
f.write(content.strip())
|
||
|
||
def init_database(self):
|
||
"""Инициализация базы данных"""
|
||
print("Инициализация базы данных...")
|
||
|
||
# Создаем .env файл
|
||
env_file = self.backend_dir / ".env"
|
||
with open(env_file, "w", encoding='utf-8') as f:
|
||
f.write("DATABASE_URL=sqlite:///./smartplayer.db\n")
|
||
f.write("SECRET_KEY=your-super-secret-key-keep-it-safe\n")
|
||
f.write("ACCESS_TOKEN_EXPIRE_MINUTES=30\n")
|
||
|
||
# Запускаем скрипт создания админа
|
||
subprocess.run([str(self.venv_python), str(self.backend_dir / "create_admin.py")], check=True)
|
||
|
||
def run(self):
|
||
"""Запуск проекта"""
|
||
try:
|
||
self.clean()
|
||
self.create_backend()
|
||
self.create_frontend()
|
||
print("\nПроект успешно создан!")
|
||
print("\nДля запуска backend:")
|
||
print(f"cd {self.backend_dir} && {self.venv_python} -m uvicorn app.main:app --reload")
|
||
print("\nДля запуска frontend:")
|
||
print(f"cd {self.frontend_dir} && npm run dev")
|
||
except Exception as e:
|
||
print(f"\nОшибка при создании проекта: {e}")
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
setup = ProjectSetup()
|
||
setup.run() |