diff --git a/alembic/env.py b/alembic/env.py index cc121f2..be993d2 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -11,12 +11,13 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) # Import models from app.db.session import Base # Import all models here so they are registered with Base.metadata -# When models are defined, uncomment the specific imports -# from app.models.user import User -# from app.models.song import Song -# from app.models.album import Album -# from app.models.artist import Artist -# from app.models.playlist import Playlist +# These imports are needed to register the models with SQLAlchemy +# noqa: F401 means "No Quality Assurance - ignore rule F401" (unused imports) +from app.models.user import User # noqa: F401 +from app.models.song import Song, song_playlist # noqa: F401 +from app.models.album import Album # noqa: F401 +from app.models.artist import Artist # noqa: F401 +from app.models.playlist import Playlist # noqa: F401 # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/alembic/versions/001_initial_migration.py b/alembic/versions/001_initial_migration.py new file mode 100644 index 0000000..a57b842 --- /dev/null +++ b/alembic/versions/001_initial_migration.py @@ -0,0 +1,134 @@ +"""Initial database schema + +Revision ID: 001 +Revises: +Create Date: 2023-08-07 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '001' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Create users table + op.create_table( + 'users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('username', sa.String(), nullable=False), + sa.Column('hashed_password', sa.String(), nullable=False), + sa.Column('full_name', sa.String(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, default=True), + sa.Column('is_superuser', sa.Boolean(), nullable=False, default=False), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) + + # Create artists table + op.create_table( + 'artists', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('bio', sa.Text(), nullable=True), + sa.Column('image_path', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_artists_id'), 'artists', ['id'], unique=False) + op.create_index(op.f('ix_artists_name'), 'artists', ['name'], unique=False) + + # Create albums table + op.create_table( + 'albums', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('release_date', sa.Date(), nullable=True), + sa.Column('cover_image_path', sa.String(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('artist_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['artist_id'], ['artists.id'], ), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_albums_id'), 'albums', ['id'], unique=False) + op.create_index(op.f('ix_albums_title'), 'albums', ['title'], unique=False) + + # Create songs table + op.create_table( + 'songs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('duration', sa.Float(), nullable=True), + sa.Column('file_path', sa.String(), nullable=False), + sa.Column('track_number', sa.Integer(), nullable=True), + sa.Column('artist_id', sa.Integer(), nullable=False), + sa.Column('album_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['album_id'], ['albums.id'], ), + sa.ForeignKeyConstraint(['artist_id'], ['artists.id'], ), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_songs_id'), 'songs', ['id'], unique=False) + op.create_index(op.f('ix_songs_title'), 'songs', ['title'], unique=False) + + # Create playlists table + op.create_table( + 'playlists', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_public', sa.Boolean(), nullable=False, default=True), + sa.Column('cover_image_path', sa.String(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_playlists_id'), 'playlists', ['id'], unique=False) + op.create_index(op.f('ix_playlists_name'), 'playlists', ['name'], unique=False) + + # Create song_playlist association table + op.create_table( + 'song_playlist', + sa.Column('song_id', sa.Integer(), nullable=False), + sa.Column('playlist_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['playlist_id'], ['playlists.id'], ), + sa.ForeignKeyConstraint(['song_id'], ['songs.id'], ), + sa.PrimaryKeyConstraint('song_id', 'playlist_id'), + ) + + +def downgrade() -> None: + # Drop tables in reverse order + op.drop_table('song_playlist') + op.drop_index(op.f('ix_playlists_name'), table_name='playlists') + op.drop_index(op.f('ix_playlists_id'), table_name='playlists') + op.drop_table('playlists') + op.drop_index(op.f('ix_songs_title'), table_name='songs') + op.drop_index(op.f('ix_songs_id'), table_name='songs') + op.drop_table('songs') + op.drop_index(op.f('ix_albums_title'), table_name='albums') + op.drop_index(op.f('ix_albums_id'), table_name='albums') + op.drop_table('albums') + op.drop_index(op.f('ix_artists_name'), table_name='artists') + op.drop_index(op.f('ix_artists_id'), table_name='artists') + op.drop_table('artists') + op.drop_index(op.f('ix_users_username'), table_name='users') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py index e69de29..6eed00a 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -0,0 +1,9 @@ +# Define __all__ to explicitly export these models +__all__ = ["User", "Artist", "Album", "Song", "song_playlist", "Playlist"] + +# Import models to make them available when importing from app.models +from app.models.user import User # noqa: F401 +from app.models.artist import Artist # noqa: F401 +from app.models.album import Album # noqa: F401 +from app.models.song import Song, song_playlist # noqa: F401 +from app.models.playlist import Playlist # noqa: F401 \ No newline at end of file diff --git a/app/models/album.py b/app/models/album.py new file mode 100644 index 0000000..79ee259 --- /dev/null +++ b/app/models/album.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, String, Date, Integer, ForeignKey, Text +from sqlalchemy.orm import relationship + +from app.models.base import BaseModel + + +class Album(BaseModel): + """Album model representing a collection of songs.""" + + __tablename__ = "albums" + + title = Column(String, nullable=False, index=True) + release_date = Column(Date, nullable=True) + cover_image_path = Column(String, nullable=True) # Path to album cover image + description = Column(Text, nullable=True) + + # Foreign keys + artist_id = Column(Integer, ForeignKey("artists.id"), nullable=False) + + # Relationships + artist = relationship("Artist", back_populates="albums") + songs = relationship("Song", back_populates="album", cascade="all, delete-orphan") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/app/models/artist.py b/app/models/artist.py new file mode 100644 index 0000000..e776447 --- /dev/null +++ b/app/models/artist.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, String, Text +from sqlalchemy.orm import relationship + +from app.models.base import BaseModel + + +class Artist(BaseModel): + """Artist model representing a musician or band.""" + + __tablename__ = "artists" + + name = Column(String, nullable=False, index=True) + bio = Column(Text, nullable=True) + image_path = Column(String, nullable=True) # Path to artist image + + # Relationships + albums = relationship("Album", back_populates="artist", cascade="all, delete-orphan") + songs = relationship("Song", back_populates="artist", cascade="all, delete-orphan") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..bb65907 --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,20 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer +from sqlalchemy.ext.declarative import declared_attr + +from app.db.session import Base + + +class BaseModel(Base): + """Base model class that includes common fields and methods.""" + + __abstract__ = True + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + @declared_attr + def __tablename__(cls) -> str: + """Generate __tablename__ automatically from the class name.""" + return cls.__name__.lower() \ No newline at end of file diff --git a/app/models/playlist.py b/app/models/playlist.py new file mode 100644 index 0000000..d42d1b3 --- /dev/null +++ b/app/models/playlist.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, String, Integer, ForeignKey, Text, Boolean +from sqlalchemy.orm import relationship + +from app.models.base import BaseModel +from app.models.song import song_playlist + + +class Playlist(BaseModel): + """Playlist model representing a collection of songs created by a user.""" + + __tablename__ = "playlists" + + name = Column(String, nullable=False, index=True) + description = Column(Text, nullable=True) + is_public = Column(Boolean, default=True, nullable=False) + cover_image_path = Column(String, nullable=True) # Path to playlist cover image + + # Foreign keys + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + + # Relationships + user = relationship("User", back_populates="playlists") + songs = relationship("Song", secondary=song_playlist, back_populates="playlists") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/app/models/song.py b/app/models/song.py new file mode 100644 index 0000000..6920536 --- /dev/null +++ b/app/models/song.py @@ -0,0 +1,37 @@ +from sqlalchemy import Column, String, Integer, ForeignKey, Float, Table +from sqlalchemy.orm import relationship + +from app.db.session import Base +from app.models.base import BaseModel + + +# Association table for the many-to-many relationship between songs and playlists +song_playlist = Table( + "song_playlist", + Base.metadata, + Column("song_id", Integer, ForeignKey("songs.id"), primary_key=True), + Column("playlist_id", Integer, ForeignKey("playlists.id"), primary_key=True), +) + + +class Song(BaseModel): + """Song model representing an audio track.""" + + __tablename__ = "songs" + + title = Column(String, nullable=False, index=True) + duration = Column(Float, nullable=True) # Duration in seconds + file_path = Column(String, nullable=False) # Path to audio file + track_number = Column(Integer, nullable=True) # Track number in the album + + # Foreign keys + artist_id = Column(Integer, ForeignKey("artists.id"), nullable=False) + album_id = Column(Integer, ForeignKey("albums.id"), nullable=True) + + # Relationships + artist = relationship("Artist", back_populates="songs") + album = relationship("Album", back_populates="songs") + playlists = relationship("Playlist", secondary=song_playlist, back_populates="songs") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..e69d22d --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,23 @@ +from sqlalchemy import Boolean, Column, String +from sqlalchemy.orm import relationship + +from app.models.base import BaseModel + + +class User(BaseModel): + """User model for authentication and profile information.""" + + __tablename__ = "users" + + email = Column(String, unique=True, index=True, nullable=False) + username = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + full_name = Column(String, nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + is_superuser = Column(Boolean, default=False, nullable=False) + + # Relationships + playlists = relationship("Playlist", back_populates="user", cascade="all, delete-orphan") + + def __repr__(self): + return f"" \ No newline at end of file