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": ''' Smart Player
''', 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( , ) ''', 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} : ; }; function App() { return ( } /> } /> } /> ); } 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; logout: () => void; } export const useAuthStore = create((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 ( Smart Player {error && {error}} setUsername(e.target.value)} /> setPassword(e.target.value)} /> Нет аккаунта? Зарегистрироваться ); }; ''', 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 = (
Smart Player
); return ( Smart Player {drawer} {drawer} Добро пожаловать в Smart Player ); }; ''', self.frontend_dir / "src" / "vite-env.d.ts": ''' /// ''', 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 ( Регистрация {error && {error}} setUsername(e.target.value)} /> setEmail(e.target.value)} /> setPassword(e.target.value)} /> Уже есть аккаунт? Войти ); }; ''', 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(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 ( <> setOpen(false)}> Загрузка медиа setTitle(e.target.value)} margin="normal" /> setArtist(e.target.value)} margin="normal" /> setFile(e.target.files?.[0])} /> ); }; ''', 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(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 ( ); }; export const PlaylistManager = () => { const [playlists, setPlaylists] = useState([]); useEffect(() => { fetch('http://localhost:8000/playlists/', { headers: { Authorization: `Bearer ${useAuthStore(state => state.token)}` } }) .then((res) => res.json()) .then(setPlaylists); }, []); return ( Мои плейлисты {playlists.map((playlist) => ( {playlist.name} ))} ); }; ''', 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()