smartplayer/1.py
2025-02-23 08:22:12 +00:00

1273 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()