initial commit

This commit is contained in:
2026-02-24 21:24:16 +06:00
commit aa35aaa8ce
46 changed files with 6906 additions and 0 deletions

0
backend/app/__init__.py Normal file
View File

55
backend/app/auth.py Normal file
View File

@@ -0,0 +1,55 @@
from datetime import datetime, timedelta
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 app.database import get_db
from app.models.models import User
SECRET_KEY = "your-secret-key-change-in-production-make-it-very-long-and-secure"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 часа
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
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):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
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, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError as e:
print(f"JWT Error: {e}")
raise credentials_exception
user = db.query(User).filter(User.username == username).first()
if user is None:
raise credentials_exception
if user.is_banned:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Ваш аккаунт заблокирован"
)
return user

16
backend/app/database.py Normal file
View File

@@ -0,0 +1,16 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./music_platform.db"
engine = create_engine(SQLALCHEMY_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()

31
backend/app/main.py Normal file
View File

@@ -0,0 +1,31 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
import os
app = FastAPI(title="Music Platform API")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
os.makedirs("uploads/music", exist_ok=True)
os.makedirs("uploads/covers", exist_ok=True)
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
from app.routes import auth, music, playlists, rooms, admin
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(music.router, prefix="/api/music", tags=["music"])
app.include_router(playlists.router, prefix="/api/playlists", tags=["playlists"])
app.include_router(rooms.router, prefix="/api/rooms", tags=["rooms"])
app.include_router(admin.router)
@app.get("/")
def root():
return {"message": "Music Platform API"}

View File

View File

@@ -0,0 +1,62 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Table, DateTime, Boolean
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database import Base
playlist_songs = Table('playlist_songs', Base.metadata,
Column('playlist_id', Integer, ForeignKey('playlists.id')),
Column('song_id', Integer, ForeignKey('songs.id'))
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_admin = Column(Boolean, default=False)
is_owner = Column(Boolean, default=False)
is_banned = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
songs = relationship("Song", back_populates="owner")
playlists = relationship("Playlist", back_populates="owner")
rooms = relationship("Room", back_populates="creator")
class Song(Base):
__tablename__ = "songs"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
artist = Column(String)
file_path = Column(String)
cover_path = Column(String, nullable=True)
duration = Column(Integer)
owner_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, default=datetime.utcnow)
owner = relationship("User", back_populates="songs")
playlists = relationship("Playlist", secondary=playlist_songs, back_populates="songs")
class Playlist(Base):
__tablename__ = "playlists"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
description = Column(String, nullable=True)
is_public = Column(Boolean, default=False)
owner_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, default=datetime.utcnow)
owner = relationship("User", back_populates="playlists")
songs = relationship("Song", secondary=playlist_songs, back_populates="playlists")
class Room(Base):
__tablename__ = "rooms"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
code = Column(String, unique=True, index=True)
creator_id = Column(Integer, ForeignKey("users.id"))
current_song_id = Column(Integer, ForeignKey("songs.id"), nullable=True)
is_playing = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
creator = relationship("User", back_populates="rooms")

View File

174
backend/app/routes/admin.py Normal file
View File

@@ -0,0 +1,174 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.models import User, Song
from app.auth import get_current_user
from pydantic import BaseModel
import os
router = APIRouter(prefix="/api/admin", tags=["admin"])
def get_admin_user(current_user: User = Depends(get_current_user)):
if not current_user.is_admin and not current_user.is_owner:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступ запрещен. Требуются права администратора"
)
return current_user
def get_owner_user(current_user: User = Depends(get_current_user)):
if not current_user.is_owner:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступ запрещен. Требуются права создателя"
)
return current_user
class UpdateSongRequest(BaseModel):
title: str
artist: str
class BanUserRequest(BaseModel):
user_id: int
is_banned: bool
class PromoteUserRequest(BaseModel):
user_id: int
is_admin: bool
@router.get("/users")
def get_all_users(
db: Session = Depends(get_db),
admin: User = Depends(get_admin_user)
):
users = db.query(User).all()
return [{
"id": user.id,
"username": user.username,
"email": user.email,
"is_admin": user.is_admin,
"is_owner": user.is_owner,
"is_banned": user.is_banned,
"created_at": user.created_at
} for user in users]
@router.get("/songs")
def get_all_songs(
db: Session = Depends(get_db),
admin: User = Depends(get_admin_user)
):
songs = db.query(Song).all()
return [{
"id": song.id,
"title": song.title,
"artist": song.artist,
"file_path": song.file_path,
"cover_path": song.cover_path,
"owner_id": song.owner_id,
"owner_username": song.owner.username if song.owner else None,
"created_at": song.created_at
} for song in songs]
@router.put("/songs/{song_id}")
def update_song(
song_id: int,
request: UpdateSongRequest,
db: Session = Depends(get_db),
admin: User = Depends(get_admin_user)
):
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Песня не найдена")
song.title = request.title
song.artist = request.artist
db.commit()
db.refresh(song)
return {"message": "Песня обновлена", "song": {
"id": song.id,
"title": song.title,
"artist": song.artist
}}
@router.delete("/songs/{song_id}")
def delete_song(
song_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_admin_user)
):
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Песня не найдена")
# Удаляем файлы
if song.file_path and os.path.exists(song.file_path):
os.remove(song.file_path)
if song.cover_path and os.path.exists(song.cover_path):
os.remove(song.cover_path)
db.delete(song)
db.commit()
return {"message": "Песня удалена"}
@router.post("/users/ban")
def ban_user(
request: BanUserRequest,
db: Session = Depends(get_db),
admin: User = Depends(get_admin_user)
):
user = db.query(User).filter(User.id == request.user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
if user.is_admin or user.is_owner:
raise HTTPException(status_code=400, detail="Нельзя забанить администратора или создателя")
user.is_banned = request.is_banned
db.commit()
return {"message": f"Пользователь {'забанен' if request.is_banned else 'разбанен'}"}
@router.post("/users/promote")
def promote_user(
request: PromoteUserRequest,
db: Session = Depends(get_db),
owner: User = Depends(get_owner_user)
):
user = db.query(User).filter(User.id == request.user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
if user.is_owner:
raise HTTPException(status_code=400, detail="Нельзя изменить права создателя")
user.is_admin = request.is_admin
db.commit()
return {"message": f"Пользователь {'повышен до администратора' if request.is_admin else 'понижен до пользователя'}"}
@router.delete("/users/{user_id}")
def delete_user(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_admin_user)
):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
if user.is_admin or user.is_owner:
raise HTTPException(status_code=400, detail="Нельзя удалить администратора или создателя")
# Удаляем все песни пользователя
for song in user.songs:
if song.file_path and os.path.exists(song.file_path):
os.remove(song.file_path)
if song.cover_path and os.path.exists(song.cover_path):
os.remove(song.cover_path)
db.delete(user)
db.commit()
return {"message": "Пользователь удален"}

View File

@@ -0,0 +1,57 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.database import get_db
from app.models.models import User
from app.auth import get_password_hash, verify_password, create_access_token, get_current_user
router = APIRouter()
class UserCreate(BaseModel):
username: str
email: str
password: str
class Token(BaseModel):
access_token: str
token_type: str
@router.post("/register")
def register(user: UserCreate, db: Session = Depends(get_db)):
if db.query(User).filter(User.username == user.username).first():
raise HTTPException(status_code=400, detail="Username already exists")
if db.query(User).filter(User.email == user.email).first():
raise HTTPException(status_code=400, detail="Email already exists")
db_user = User(
username=user.username,
email=user.email,
hashed_password=get_password_hash(user.password)
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return {"message": "User created successfully"}
@router.post("/login", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == form_data.username).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Incorrect username or password")
if user.is_banned:
raise HTTPException(status_code=403, detail="Ваш аккаунт заблокирован")
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/me")
def get_current_user_info(current_user: User = Depends(get_current_user)):
return {
"id": current_user.id,
"username": current_user.username,
"email": current_user.email,
"is_admin": current_user.is_admin,
"is_owner": current_user.is_owner
}

View File

@@ -0,0 +1,72 @@
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from typing import List
from pydantic import BaseModel
import shutil
import os
from app.database import get_db
from app.models.models import Song, User
from app.auth import get_current_user
router = APIRouter()
class SongResponse(BaseModel):
id: int
title: str
artist: str
file_path: str
cover_path: str | None
duration: int
owner_id: int
class Config:
from_attributes = True
@router.post("/upload")
async def upload_song(
title: str = Form(...),
artist: str = Form(...),
duration: int = Form(...),
file: UploadFile = File(...),
cover: UploadFile = File(None),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
file_path = f"uploads/music/{current_user.id}_{file.filename}"
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
cover_path = None
if cover:
cover_path = f"uploads/covers/{current_user.id}_{cover.filename}"
with open(cover_path, "wb") as buffer:
shutil.copyfileobj(cover.file, buffer)
song = Song(
title=title,
artist=artist,
file_path=file_path,
cover_path=cover_path,
duration=duration,
owner_id=current_user.id
)
db.add(song)
db.commit()
db.refresh(song)
return song
@router.get("/songs", response_model=List[SongResponse])
def get_songs(db: Session = Depends(get_db)):
return db.query(Song).all()
@router.get("/my-songs", response_model=List[SongResponse])
def get_my_songs(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
return db.query(Song).filter(Song.owner_id == current_user.id).all()
@router.get("/download/{song_id}")
def download_song(song_id: int, db: Session = Depends(get_db)):
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Song not found")
return FileResponse(song.file_path, filename=f"{song.title}.mp3")

View File

@@ -0,0 +1,112 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from pydantic import BaseModel
from app.database import get_db
from app.models.models import Playlist, Song, User
from app.auth import get_current_user
router = APIRouter()
class PlaylistCreate(BaseModel):
name: str
description: str | None = None
is_public: bool = False
class PlaylistResponse(BaseModel):
id: int
name: str
description: str | None
is_public: bool
owner_id: int
songs: List[dict]
class Config:
from_attributes = True
@router.post("/create")
def create_playlist(
playlist: PlaylistCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
db_playlist = Playlist(
name=playlist.name,
description=playlist.description,
is_public=playlist.is_public,
owner_id=current_user.id
)
db.add(db_playlist)
db.commit()
db.refresh(db_playlist)
return db_playlist
@router.get("/my-playlists")
def get_my_playlists(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
return db.query(Playlist).filter(Playlist.owner_id == current_user.id).all()
@router.get("/public")
def get_public_playlists(db: Session = Depends(get_db)):
return db.query(Playlist).filter(Playlist.is_public == True).all()
@router.post("/{playlist_id}/add-song/{song_id}")
def add_song_to_playlist(
playlist_id: int,
song_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first()
if not playlist or playlist.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized")
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Song not found")
playlist.songs.append(song)
db.commit()
return {"message": "Song added to playlist"}
@router.get("/{playlist_id}")
def get_playlist(playlist_id: int, db: Session = Depends(get_db)):
playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first()
if not playlist:
raise HTTPException(status_code=404, detail="Playlist not found")
songs_data = [{
"id": song.id,
"title": song.title,
"artist": song.artist,
"file_path": song.file_path,
"cover_path": song.cover_path,
"duration": song.duration,
"owner_id": song.owner_id
} for song in playlist.songs]
return {
"id": playlist.id,
"name": playlist.name,
"description": playlist.description,
"is_public": playlist.is_public,
"owner_id": playlist.owner_id,
"songs": songs_data
}
@router.delete("/{playlist_id}/remove-song/{song_id}")
def remove_song_from_playlist(
playlist_id: int,
song_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first()
if not playlist or playlist.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized")
song = db.query(Song).filter(Song.id == song_id).first()
if song in playlist.songs:
playlist.songs.remove(song)
db.commit()
return {"message": "Song removed from playlist"}

128
backend/app/routes/rooms.py Normal file
View File

@@ -0,0 +1,128 @@
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, HTTPException
from sqlalchemy.orm import Session
from typing import Dict, List
from pydantic import BaseModel
import random
import string
from app.database import get_db
from app.models.models import Room, User, Song, Playlist
from app.auth import get_current_user
router = APIRouter()
class RoomCreate(BaseModel):
name: str
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, List[dict]] = {}
async def connect(self, websocket: WebSocket, room_code: str, username: str):
await websocket.accept()
if room_code not in self.active_connections:
self.active_connections[room_code] = []
# Проверяем, не подключен ли уже этот пользователь
existing = [conn for conn in self.active_connections[room_code] if conn["username"] == username]
if not existing:
self.active_connections[room_code].append({"ws": websocket, "username": username})
await self.broadcast({"type": "user_count", "count": len(self.active_connections[room_code])}, room_code)
def disconnect(self, websocket: WebSocket, room_code: str):
if room_code in self.active_connections:
self.active_connections[room_code] = [
conn for conn in self.active_connections[room_code] if conn["ws"] != websocket
]
return len(self.active_connections[room_code])
return 0
async def broadcast(self, message: dict, room_code: str):
if room_code in self.active_connections:
disconnected = []
for connection in self.active_connections[room_code]:
try:
await connection["ws"].send_json(message)
except:
disconnected.append(connection)
# Удаляем отключенные соединения
for conn in disconnected:
if conn in self.active_connections[room_code]:
self.active_connections[room_code].remove(conn)
def get_user_count(self, room_code: str):
if room_code in self.active_connections:
return len(self.active_connections[room_code])
return 0
manager = ConnectionManager()
def generate_room_code():
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
@router.post("/create")
def create_room(
room: RoomCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
code = generate_room_code()
db_room = Room(name=room.name, code=code, creator_id=current_user.id)
db.add(db_room)
db.commit()
db.refresh(db_room)
return db_room
@router.get("/{room_code}")
def get_room(room_code: str, db: Session = Depends(get_db)):
room = db.query(Room).filter(Room.code == room_code).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
user_count = manager.get_user_count(room_code)
return {**room.__dict__, "user_count": user_count}
@router.post("/{room_code}/add-song/{song_id}")
def add_song_to_room(
room_code: str,
song_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
room = db.query(Room).filter(Room.code == room_code).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Song not found")
return {"message": "Song added to queue"}
@router.post("/{room_code}/add-playlist/{playlist_id}")
def add_playlist_to_room(
room_code: str,
playlist_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
room = db.query(Room).filter(Room.code == room_code).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first()
if not playlist:
raise HTTPException(status_code=404, detail="Playlist not found")
return {"message": "Playlist added to queue", "songs": [{"id": s.id, "title": s.title, "artist": s.artist} for s in playlist.songs]}
@router.websocket("/ws/{room_code}")
async def websocket_endpoint(websocket: WebSocket, room_code: str, username: str = "User"):
await manager.connect(websocket, room_code, username)
try:
while True:
data = await websocket.receive_json()
await manager.broadcast(data, room_code)
except WebSocketDisconnect:
count = manager.disconnect(websocket, room_code)
await manager.broadcast({"type": "user_count", "count": count}, room_code)