diff --git a/.gitignore b/.gitignore index 6b75577..3c7f005 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ tv/* log.txt res/* media_manager/indexer/indexers/prowlarr.http +*.egg-info +.env web/cache/ diff --git a/README.md b/README.md index 1b15e65..30c3339 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,23 @@ docker compose up db -d uv run alembic upgrade head ``` +## Get the frontend up and running + +TODO: provide an env.example to copy + +```bash +cd /web && npm install +``` + +## Now start the backend and frontend +```bash +fastapi dev /media_manager/main.py --reload --host +``` + +```bash +cd /web && npm run dev +``` + ### [View the docs for installation instructions and more](https://maxdorninger.github.io/MediaManager/configuration-overview.html#configuration-overview) diff --git a/alembic/env.py b/alembic/env.py index b15956f..0428fc1 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -28,6 +28,8 @@ from media_manager.auth.db import User, OAuthAccount # noqa: E402 from media_manager.indexer.models import IndexerQueryResult # noqa: E402 from media_manager.torrent.models import Torrent # noqa: E402 from media_manager.tv.models import Show, Season, Episode, SeasonFile, SeasonRequest # noqa: E402 +from media_manager.movies.models import Movie, MovieFile, MovieRequest # noqa: E402 +from media_manager.notification.models import Notification # noqa: E402 from media_manager.database import Base # noqa: E402 @@ -45,6 +47,10 @@ target_metadata = Base.metadata Episode, SeasonFile, SeasonRequest, + Movie, + MovieFile, + MovieRequest, + Notification, ) diff --git a/alembic/versions/93fb07842385_initial_migration.py b/alembic/versions/93fb07842385_initial_migration.py index f35ac16..7f374fc 100644 --- a/alembic/versions/93fb07842385_initial_migration.py +++ b/alembic/versions/93fb07842385_initial_migration.py @@ -8,6 +8,9 @@ Create Date: 2025-05-27 21:36:18.532068 from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = "93fb07842385" @@ -19,12 +22,191 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - + + # Create user table + op.create_table('user', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('email', sa.String(length=320), nullable=False), + sa.Column('hashed_password', sa.String(length=1024), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_superuser', sa.Boolean(), nullable=False), + sa.Column('is_verified', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) + + # Create oauth account table + op.create_table('oauthaccount', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('oauth_name', sa.String(length=100), nullable=False), + sa.Column('access_token', sa.String(length=1024), nullable=False), + sa.Column('expires_at', sa.Integer(), nullable=True), + sa.Column('refresh_token', sa.String(length=1024), nullable=True), + sa.Column('account_id', sa.String(length=320), nullable=False), + sa.Column('account_email', sa.String(length=320), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_oauthaccount_account_id'), 'oauthaccount', ['account_id'], unique=False) + op.create_index(op.f('ix_oauthaccount_oauth_name'), 'oauthaccount', ['oauth_name'], unique=False) + + # Create torrent table + op.create_table('torrent', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('status', sa.Enum('finished', 'downloading', 'error', 'unknown', name='torrentstatus'), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.Column('imported', sa.Boolean(), nullable=False), + sa.Column('hash', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexer query result table + op.create_table('indexer_query_result', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('download_url', sa.String(), nullable=False), + sa.Column('seeders', sa.Integer(), nullable=False), + sa.Column('flags', postgresql.ARRAY(sa.String()), nullable=True), + sa.Column('quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.Column('season', postgresql.ARRAY(sa.Integer()), nullable=True), + sa.Column('size', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + + # Create notification table + op.create_table('notification', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('message', sa.String(), nullable=False), + sa.Column('read', sa.Boolean(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + + # Create show table + op.create_table('show', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('external_id', sa.Integer(), nullable=False), + sa.Column('metadata_provider', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('overview', sa.String(), nullable=False), + sa.Column('year', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('external_id', 'metadata_provider') + ) + + # Create movie table + op.create_table('movie', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('external_id', sa.Integer(), nullable=False), + sa.Column('metadata_provider', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('overview', sa.String(), nullable=False), + sa.Column('year', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('external_id', 'metadata_provider') + ) + + # Create season table + op.create_table('season', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('show_id', sa.UUID(), nullable=False), + sa.Column('number', sa.Integer(), nullable=False), + sa.Column('external_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('overview', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['show_id'], ['show.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('show_id', 'number') + ) + + # Create movie file table + op.create_table('movie_file', + sa.Column('movie_id', sa.UUID(), nullable=False), + sa.Column('file_path_suffix', sa.String(), nullable=False), + sa.Column('quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.Column('torrent_id', sa.UUID(), nullable=True), + sa.ForeignKeyConstraint(['movie_id'], ['movie.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['torrent_id'], ['torrent.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('movie_id', 'file_path_suffix') + ) + + # Create movie request table + op.create_table('movie_request', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('movie_id', sa.UUID(), nullable=False), + sa.Column('wanted_quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.Column('min_quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.Column('authorized', sa.Boolean(), nullable=False), + sa.Column('requested_by_id', sa.UUID(), nullable=True), + sa.Column('authorized_by_id', sa.UUID(), nullable=True), + sa.ForeignKeyConstraint(['authorized_by_id'], ['user.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['movie_id'], ['movie.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['requested_by_id'], ['user.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('movie_id', 'wanted_quality') + ) + + # Create episode table + op.create_table('episode', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('season_id', sa.UUID(), nullable=False), + sa.Column('number', sa.Integer(), nullable=False), + sa.Column('external_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['season_id'], ['season.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('season_id', 'number') + ) + + # Create season file table + op.create_table('season_file', + sa.Column('season_id', sa.UUID(), nullable=False), + sa.Column('torrent_id', sa.UUID(), nullable=True), + sa.Column('file_path_suffix', sa.String(), nullable=False), + sa.Column('quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.ForeignKeyConstraint(['season_id'], ['season.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['torrent_id'], ['torrent.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('season_id', 'file_path_suffix') + ) + + # Create season request table + op.create_table('season_request', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('season_id', sa.UUID(), nullable=False), + sa.Column('wanted_quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.Column('min_quality', sa.Enum('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), nullable=False), + sa.Column('requested_by_id', sa.UUID(), nullable=True), + sa.Column('authorized', sa.Boolean(), nullable=False), + sa.Column('authorized_by_id', sa.UUID(), nullable=True), + sa.ForeignKeyConstraint(['authorized_by_id'], ['user.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['requested_by_id'], ['user.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['season_id'], ['season.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('season_id', 'wanted_quality') + ) + # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - + op.drop_table('season_request') + op.drop_table('season_file') + op.drop_table('episode') + op.drop_table('movie_request') + op.drop_table('movie_file') + op.drop_table('season') + op.drop_table('movie') + op.drop_table('show') + op.drop_table('notification') + op.drop_table('indexer_query_result') + op.drop_table('torrent') + op.drop_index(op.f('ix_oauthaccount_oauth_name'), table_name='oauthaccount') + op.drop_index(op.f('ix_oauthaccount_account_id'), table_name='oauthaccount') + op.drop_table('oauthaccount') + op.drop_index(op.f('ix_user_email'), table_name='user') + op.drop_table('user') # ### end Alembic commands ### diff --git a/media_manager/config.py b/media_manager/config.py index 05f1913..ff67526 100644 --- a/media_manager/config.py +++ b/media_manager/config.py @@ -5,10 +5,10 @@ from pydantic_settings import BaseSettings class BasicConfig(BaseSettings): - image_directory: Path = "/data/images" - tv_directory: Path = "/data/tv" - movie_directory: Path = "/data/movies" - torrent_directory: Path = "/data/torrents" + image_directory: Path = Path(__file__).parent.parent / "data" / "images" + tv_directory: Path = Path(__file__).parent.parent / "data" / "tv" + movie_directory: Path = Path(__file__).parent.parent / "data" / "movies" + torrent_directory: Path = Path(__file__).parent.parent / "data" / "torrents" FRONTEND_URL: AnyHttpUrl = "http://localhost:3000/" CORS_URLS: list[str] = [] DEVELOPMENT: bool = False