xing things.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2024-04-23 13:33:49 -04:00
parent 7af96ca7b9
commit 8f882659c2
3 changed files with 198 additions and 24 deletions

View File

@@ -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")
)

View File

@@ -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()

View File

@@ -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.")