@@ -1,14 +1,18 @@
|
||||
"""All database models are defined here."""
|
||||
|
||||
import enum
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy import Enum
|
||||
from sqlalchemy import Float
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Mapped
|
||||
@@ -17,21 +21,12 @@ from sqlalchemy.orm import registry
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
# Registry and Base initialization using SQLAlchemy's new ORM 2.0 style
|
||||
# Registry initialization using SQLAlchemy's new ORM 2.0 style
|
||||
mapper_registry = registry()
|
||||
Base = mapper_registry.generate_base()
|
||||
|
||||
# Association tables for many-to-many relationships
|
||||
playlist_track_association = Table(
|
||||
"playlist_track_association",
|
||||
Base.metadata,
|
||||
Column("playlist_id", ForeignKey("playlists.id"), primary_key=True),
|
||||
Column("track_id", ForeignKey("tracks.id"), primary_key=True),
|
||||
)
|
||||
|
||||
holiday_track_association = Table(
|
||||
"holiday_track_association",
|
||||
Base.metadata,
|
||||
mapper_registry.metadata,
|
||||
Column("holiday_id", ForeignKey("holidays.id"), primary_key=True),
|
||||
Column("track_id", ForeignKey("tracks.id"), primary_key=True),
|
||||
)
|
||||
@@ -73,8 +68,71 @@ class Playlist:
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||
owner: Mapped[User] = relationship("User", back_populates="playlist")
|
||||
tracks: Mapped[list["Track"]] = relationship(
|
||||
"Track", secondary=playlist_track_association, back_populates="playlists"
|
||||
playlist_tracks: Mapped[list["PlaylistTrack"]] = relationship(
|
||||
"PlaylistTrack", back_populates="playlist"
|
||||
)
|
||||
|
||||
|
||||
@mapper_registry.mapped_as_dataclass
|
||||
class PlaylistTrack:
|
||||
"""Represents an entry in a playlist that associates a track with additional context.
|
||||
|
||||
Each playlist track is tied to exactly one track and optionally linked to a category,
|
||||
a holiday, or a podcast episode. The playlist track can also categorize the track
|
||||
as part of a specific sublist based on how the track was added to the playlist.
|
||||
|
||||
Attributes:
|
||||
id (int): The primary key for the playlist track.
|
||||
track_id (int): The foreign key linked to the track's primary key.
|
||||
category_id (int, optional): The foreign key linked to the category's primary key, nullable.
|
||||
holiday_id (int, optional): The foreign key linked to the holiday's primary key, nullable.
|
||||
episode_id (int, optional): The foreign key linked to the podcast episode's primary
|
||||
key, nullable.
|
||||
favorite (bool, optional): Flag to indicate if the track is marked as a favorite, nullable.
|
||||
sublist (SublistType, optional): The type of sublist the track belongs to, nullable.
|
||||
|
||||
Relationships:
|
||||
track (Track): The track associated with this playlist track.
|
||||
category (Category, optional): The category associated with this playlist track, nullable.
|
||||
holiday (Holiday, optional): The holiday associated with this playlist track, nullable.
|
||||
episode (Episode, optional): The podcast episode associated with this playlist
|
||||
track, nullable.
|
||||
|
||||
Constraints:
|
||||
At least one of `category_id`, `holiday_id`, or `episode_id` must be non-null.
|
||||
Fields `favorite` and `sublist` must be null if `episode_id` is not null.
|
||||
"""
|
||||
|
||||
__tablename__ = "playlist_tracks"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"), nullable=False)
|
||||
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id"), nullable=False)
|
||||
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"), nullable=True)
|
||||
holiday_id: Mapped[int] = mapped_column(ForeignKey("holidays.id"), nullable=True)
|
||||
episode_id: Mapped[int] = mapped_column(ForeignKey("episodes.id"), nullable=True)
|
||||
favorite: Mapped[bool] = mapped_column(Boolean, nullable=True)
|
||||
sublist: Mapped[str] = mapped_column(Enum(enum.Sublist), nullable=True)
|
||||
|
||||
playlist: Mapped["Playlist"] = relationship("Playlist")
|
||||
track: Mapped["Track"] = relationship("Track")
|
||||
category: Mapped["Category"] = relationship("Category")
|
||||
holiday: Mapped["Holiday"] = relationship("Holiday")
|
||||
episode: Mapped["Episode"] = relationship("Episode")
|
||||
|
||||
__table_args__ = (
|
||||
# Check constraint to ensure one of the category, holiday, or episode must be not null
|
||||
{
|
||||
"sqlite_with_rowid": False,
|
||||
"postgresql_where": (
|
||||
(
|
||||
category_id.is_not(None)
|
||||
| holiday_id.is_not(None)
|
||||
| episode_id.is_not(None)
|
||||
)
|
||||
& (episode_id.is_not(None) & favorite.is_(None) & sublist.is_(None))
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -111,26 +169,60 @@ class Track:
|
||||
rating: Mapped[float] = mapped_column(Float)
|
||||
genre_id: Mapped[int] = mapped_column(ForeignKey("genres.id"))
|
||||
genre: Mapped["Genre"] = relationship("Genre", back_populates="tracks")
|
||||
playlists: Mapped[list["Playlist"]] = relationship(
|
||||
"Playlist", secondary=playlist_track_association, back_populates="tracks"
|
||||
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"))
|
||||
category: Mapped["Category"] = relationship("Category", back_populates="tracks")
|
||||
playlist_tracks: Mapped[list["PlaylistTrack"]] = relationship(
|
||||
"PlaylistTrack", back_populates="track"
|
||||
)
|
||||
|
||||
|
||||
@mapper_registry.mapped_as_dataclass
|
||||
class Genre:
|
||||
"""Represents a musical genre.
|
||||
"""Represents a music genre.
|
||||
|
||||
Each genre can be associated with multiple tracks and belongs to exactly one category.
|
||||
|
||||
Attributes:
|
||||
id (int): Unique identifier for the genre.
|
||||
name (str): Name of the genre.
|
||||
tracks (list[Track]): List of tracks belonging to this genre.
|
||||
id (int): Primary key.
|
||||
name (str): Name of the genre, which is unique within the same category.
|
||||
category_id (int): Foreign key linking to the category the genre belongs to.
|
||||
category (Category): The category this genre is part of.
|
||||
tracks (list[Track]): List of tracks associated with this genre.
|
||||
"""
|
||||
|
||||
__tablename__ = "genres"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"))
|
||||
category: Mapped["Category"] = relationship("Category", back_populates="genres")
|
||||
tracks: Mapped[list["Track"]] = relationship("Track", back_populates="genre")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("name", "category_id", name="uix_genre_name_category_id"),
|
||||
)
|
||||
|
||||
|
||||
@mapper_registry.mapped_as_dataclass
|
||||
class Category:
|
||||
"""Represents a category of music that can encompass multiple genres.
|
||||
|
||||
Each category can include various genres, providing a way to classify tracks into different
|
||||
musical styles or themes.
|
||||
|
||||
Attributes:
|
||||
id (int): Primary key.
|
||||
name (str): Unique name of the category.
|
||||
genres (list[Genre]): List of genres associated with this category.
|
||||
"""
|
||||
|
||||
__tablename__ = "categories"
|
||||
__sa_dataclass_metadata_key__ = "sa"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
||||
tracks: Mapped[list[Track]] = relationship("Track", back_populates="genre")
|
||||
genres: Mapped[list["Genre"]] = relationship("Genre", back_populates="category")
|
||||
tracks: Mapped[list["Track"]] = relationship("Track", back_populates="category")
|
||||
|
||||
|
||||
@mapper_registry.mapped_as_dataclass
|
||||
@@ -218,8 +310,21 @@ class Statistics:
|
||||
max_playtime (Mapped[float]): The maximum playtime after accounting for podcasts, in seconds.
|
||||
max_tracks_per_day (Mapped[int]): The maximum number of tracks that can be played in a day.
|
||||
active_holiday_count (Mapped[int]): The number of active holidays affecting today's playlist.
|
||||
category_count (Mapped[int]): The number of categories affecting today's playlist.
|
||||
max_holiday_tracks (Mapped[int]): The maximum number of tracks that can be allocated to holidays.
|
||||
max_regular_tracks (Mapped[int]): The maximum number of regular tracks per day.
|
||||
max_regular_favorite_tracks (Mapped[int]): The maximum number of favorite tracks per day.
|
||||
max_holiday_favorite_tracks (Mapped[int]): The maximum number of favorite tracks that can be
|
||||
allocated to holidays.
|
||||
max_regular_general_tracks (Mapped[int]): The maximum number of general tracks per day.
|
||||
max_holiday_general_tracks (Mapped[int]): The maximum number of general tracks that can be
|
||||
allocated to holidays.
|
||||
max_regular_favorite_base (Mapped[int]): The maximum number of favorite tracks in one sublist.
|
||||
max_regular_general_base (Mapped[int]): The maximum number of general tracks in one sublist.
|
||||
max_holiday_favorite_base (Mapped[int]): The maximum number of favorite tracks in one sublist
|
||||
for holidays.
|
||||
max_holiday_general_base (Mapped[int]): The maximum number of general tracks in one sublist for
|
||||
holidays.
|
||||
track_update_start_timestamp (Mapped[DateTime]): The timestamp when track updates begin.
|
||||
track_update_end_timestamp (Mapped[DateTime]): The timestamp when track updates end.
|
||||
"""
|
||||
@@ -232,8 +337,17 @@ class Statistics:
|
||||
max_playtime: Mapped[float] = mapped_column(Float)
|
||||
max_tracks_per_day: Mapped[int] = mapped_column(Integer)
|
||||
active_holiday_count: Mapped[int] = mapped_column(Integer)
|
||||
category_count: Mapped[int] = mapped_column(Integer)
|
||||
max_holiday_tracks: Mapped[int] = mapped_column(Integer)
|
||||
max_regular_tracks: Mapped[int] = mapped_column(Integer)
|
||||
max_regular_favorite_tracks: Mapped[int] = mapped_column(Integer)
|
||||
max_regular_general_tracks: Mapped[int] = mapped_column(Integer)
|
||||
max_holiday_favorite_tracks: Mapped[int] = mapped_column(Integer)
|
||||
max_holiday_general_tracks: Mapped[int] = mapped_column(Integer)
|
||||
max_regular_favorite_base: Mapped[int] = mapped_column(Integer)
|
||||
max_regular_general_base: Mapped[int] = mapped_column(Integer)
|
||||
max_holiday_favorite_base: Mapped[int] = mapped_column(Integer)
|
||||
max_holiday_general_base: Mapped[int] = mapped_column(Integer)
|
||||
track_update_start_timestamp: Mapped[DateTime] = mapped_column(
|
||||
DateTime, server_default=text("CURRENT_TIMESTAMP")
|
||||
)
|
||||
|
||||
@@ -244,10 +244,19 @@ async def update_statistics(
|
||||
new_average_duration: float,
|
||||
new_podcast_length: float,
|
||||
new_max_playtime: float,
|
||||
new_category_count: int,
|
||||
new_active_holidays: int,
|
||||
new_max_tracks: int,
|
||||
new_max_regular: int,
|
||||
new_max_holiday: int,
|
||||
new_max_regular_favorite: int,
|
||||
new_max_regular_general: int,
|
||||
new_max_holiday_favorite: int,
|
||||
new_max_holiday_general: int,
|
||||
new_max_regular_favorite_base: int,
|
||||
new_max_regular_general_base: int,
|
||||
new_max_holiday_favorite_base: int,
|
||||
new_max_holiday_general_base: int,
|
||||
) -> None:
|
||||
"""Update the statistics model.
|
||||
|
||||
@@ -256,10 +265,19 @@ async def update_statistics(
|
||||
of all track durations as a float.
|
||||
new_podcast_length (float): The new total podcast length of all current episodes as a float.
|
||||
new_max_playtime (float): The new max playtime as a float.
|
||||
new_category_count (int): The new count of categories as an int.
|
||||
new_active_holidays (int): The new active holidays as an int.
|
||||
new_max_tracks (int): The new max tracks per day as an int.
|
||||
new_max_regular (int): The new max regular tracks as an int.
|
||||
new_max_holiday (int): The new max holiday tracks as an int.
|
||||
new_max_regular_favorite (int): The new max regular favorite tracks as an int.
|
||||
new_max_regular_general (int): The new max regular general tracks as an int.
|
||||
new_max_holiday_favorite (int): The new max holiday favorite tracks as an int.
|
||||
new_max_holiday_general (int): The new max holiday general tracks as an int.
|
||||
new_max_regular_favorite_base (int): The new max regular favorite base unit as an int.
|
||||
new_max_regular_general_base (int): The new max regular general base unit as an int.
|
||||
new_max_holiday_favorite_base (int): The new max holiday favorite base unit as an int.
|
||||
new_max_holiday_general_base (int): The new max holiday general base unit as an int.
|
||||
"""
|
||||
async with async_session_maker() as session:
|
||||
try:
|
||||
@@ -272,10 +290,19 @@ async def update_statistics(
|
||||
stats.average_track_length = new_average_duration
|
||||
stats.total_podcast_length = new_podcast_length
|
||||
stats.max_playtime = new_max_playtime
|
||||
stats.category_count = new_category_count
|
||||
stats.active_holiday_count = new_active_holidays
|
||||
stats.max_tracks_per_day = new_max_tracks
|
||||
stats.max_regular_tracks = new_max_regular
|
||||
stats.max_holiday_tracks = new_max_holiday
|
||||
stats.max_regular_favorite_tracks = new_max_regular_favorite
|
||||
stats.max_regular_general_tracks = new_max_regular_general
|
||||
stats.max_holiday_favorite_tracks = new_max_holiday_favorite
|
||||
stats.max_holiday_general_tracks = new_max_holiday_general
|
||||
stats.max_regular_favorite_base = new_max_regular_favorite_base
|
||||
stats.max_regular_general_base = new_max_regular_general_base
|
||||
stats.max_holiday_favorite_base = new_max_holiday_favorite_base
|
||||
stats.max_holiday_general_base = new_max_holiday_general_base
|
||||
await session.commit()
|
||||
else:
|
||||
# If no statistics entry exists, create one
|
||||
@@ -287,6 +314,14 @@ async def update_statistics(
|
||||
max_tracks_per_day=new_max_tracks,
|
||||
max_regular_tracks=new_max_regular,
|
||||
max_holiday_tracks=new_max_holiday,
|
||||
max_regular_favorite_tracks=new_max_regular_favorite,
|
||||
max_regular_general_tracks=new_max_regular_general,
|
||||
max_holiday_favorite_tracks=new_max_holiday_favorite,
|
||||
max_holiday_general_tracks=new_max_holiday_general,
|
||||
max_regular_favorite_base=new_max_regular_favorite_base,
|
||||
max_regular_general_base=new_max_regular_general_base,
|
||||
max_holiday_favorite_base=new_max_holiday_favorite_base,
|
||||
max_holiday_general_base=new_max_holiday_general_base,
|
||||
)
|
||||
session.add(new_stats)
|
||||
await session.commit()
|
||||
|
||||
@@ -26,6 +26,7 @@ async def process_tracks(server_url: str, token: str) -> None:
|
||||
|
||||
async for track_data in plex.fetch_tracks_from_plex(server_url, token):
|
||||
await sql.insert_or_update_track(track_data, found_genres)
|
||||
# TODO: Exclude podcast episodes from the list of durations.
|
||||
if track_data["duration"] > 0: # Ensure only positive durations are added
|
||||
durations.append(track_data["duration"])
|
||||
|
||||
@@ -56,25 +57,49 @@ async def calculate_statistics(durations: list[float]) -> None:
|
||||
max_playtime = env.MAX_PLAYTIME - total_podcast_length
|
||||
max_tracks_per_day = int(max_playtime / average_duration)
|
||||
|
||||
# TODO: Calculate the number of categories in the database.abs
|
||||
category_count = 1
|
||||
|
||||
# TODO: Calculate the amount of the playlist that will be for active holidays.
|
||||
active_holidays = 0
|
||||
|
||||
ratio_denom = active_holidays * 2 + 1
|
||||
|
||||
max_regular_percentage = (active_holidays + 1) / ratio_denom / category_count
|
||||
max_holiday_percentage = 1 / ratio_denom if active_holidays else 0
|
||||
max_regular_percentage = (active_holidays + 1) / ratio_denom
|
||||
|
||||
max_holiday_tracks = int(max_tracks_per_day * max_holiday_percentage)
|
||||
max_regular_tracks = int(max_tracks_per_day * max_regular_percentage)
|
||||
max_holiday_tracks = int(max_tracks_per_day * max_holiday_percentage)
|
||||
|
||||
max_regular_favorite_tracks = int(max_regular_tracks / 3)
|
||||
max_regular_general_tracks = int(2 * max_regular_tracks / 3)
|
||||
|
||||
max_holiday_favorite_tracks = int(max_holiday_tracks / 3)
|
||||
max_holiday_general_tracks = int(2 * max_holiday_tracks / 3)
|
||||
|
||||
max_regular_favorite_base = int(max_regular_favorite_tracks / 6)
|
||||
max_regular_general_base = int(max_regular_general_tracks / 6)
|
||||
|
||||
max_holiday_favorite_base = int(max_holiday_favorite_tracks / 6)
|
||||
max_holiday_general_base = int(max_holiday_general_tracks / 6)
|
||||
|
||||
await sql.update_statistics(
|
||||
average_duration,
|
||||
total_podcast_length,
|
||||
max_playtime,
|
||||
category_count,
|
||||
active_holidays,
|
||||
max_tracks_per_day,
|
||||
max_regular_tracks,
|
||||
max_holiday_tracks,
|
||||
max_regular_favorite_tracks,
|
||||
max_regular_general_tracks,
|
||||
max_regular_favorite_base,
|
||||
max_regular_general_base,
|
||||
max_holiday_favorite_tracks,
|
||||
max_holiday_general_tracks,
|
||||
max_holiday_favorite_base,
|
||||
max_holiday_general_base,
|
||||
)
|
||||
else:
|
||||
logger.warning("No valid track durations retrieved.")
|
||||
logger.warning("No valid track durations retrieved, statistics not updated.")
|
||||
|
||||
Reference in New Issue
Block a user