Files
vrobbler/vrobbler/apps/charts/models.py
Colin Powell 033239260f
All checks were successful
build / test (push) Successful in 1m57s
[perf] Add caching and lock protections
2026-06-19 13:37:30 -04:00

235 lines
9.5 KiB
Python

import calendar
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
User = get_user_model()
BNULL = {"blank": True, "null": True}
class ChartRecord(TimeStampedModel):
"""Stores rankings for a user/media/time period combination.
Example entries:
- Bjork was #3 artist in 2022
- Homogenic was #10 album in April 2025
- The Office was #1 TV series in Week #34 2024
"""
user = models.ForeignKey(User, on_delete=models.CASCADE, **BNULL)
year = models.IntegerField()
month = models.IntegerField(**BNULL)
week = models.IntegerField(**BNULL)
day = models.IntegerField(**BNULL)
artist = models.ForeignKey("music.Artist", on_delete=models.CASCADE, **BNULL)
album = models.ForeignKey("music.Album", on_delete=models.CASCADE, **BNULL)
track = models.ForeignKey("music.Track", on_delete=models.CASCADE, **BNULL)
tv_series = models.ForeignKey("videos.Series", on_delete=models.CASCADE, **BNULL)
video = models.ForeignKey("videos.Video", on_delete=models.CASCADE, **BNULL)
podcast = models.ForeignKey("podcasts.Podcast", on_delete=models.CASCADE, **BNULL)
podcast_episode = models.ForeignKey(
"podcasts.PodcastEpisode", on_delete=models.CASCADE, **BNULL
)
board_game = models.ForeignKey(
"boardgames.BoardGame", on_delete=models.CASCADE, **BNULL
)
trail = models.ForeignKey("trails.Trail", on_delete=models.CASCADE, **BNULL)
geo_location = models.ForeignKey(
"locations.GeoLocation", on_delete=models.CASCADE, **BNULL
)
food = models.ForeignKey("foods.Food", on_delete=models.CASCADE, **BNULL)
book = models.ForeignKey("books.Book", on_delete=models.CASCADE, **BNULL)
rank = models.IntegerField()
count = models.IntegerField(default=0)
class Meta:
indexes = [
models.Index(fields=["user", "year", "artist", "rank"]),
models.Index(fields=["user", "year", "album", "rank"]),
models.Index(fields=["user", "year", "track", "rank"]),
models.Index(fields=["user", "year", "tv_series", "rank"]),
models.Index(fields=["user", "year", "video", "rank"]),
models.Index(fields=["user", "year", "podcast", "rank"]),
models.Index(fields=["user", "year", "board_game", "rank"]),
models.Index(fields=["user", "year", "trail", "rank"]),
models.Index(fields=["user", "year", "geo_location", "rank"]),
models.Index(fields=["user", "year", "food", "rank"]),
models.Index(fields=["user", "year", "book", "rank"]),
models.Index(fields=["user", "year", "month", "artist", "rank"]),
models.Index(fields=["user", "year", "month", "album", "rank"]),
models.Index(fields=["user", "year", "month", "track", "rank"]),
models.Index(fields=["user", "year", "month", "tv_series", "rank"]),
models.Index(fields=["user", "year", "month", "video", "rank"]),
models.Index(fields=["user", "year", "month", "board_game", "rank"]),
models.Index(fields=["user", "year", "month", "book", "rank"]),
models.Index(fields=["user", "year", "month", "food", "rank"]),
models.Index(fields=["user", "year", "month", "podcast", "rank"]),
models.Index(fields=["user", "year", "month", "trail", "rank"]),
models.Index(fields=["user", "year", "week", "artist", "rank"]),
models.Index(fields=["user", "year", "week", "album", "rank"]),
models.Index(fields=["user", "year", "week", "track", "rank"]),
models.Index(fields=["user", "year", "week", "tv_series", "rank"]),
models.Index(fields=["user", "year", "week", "video", "rank"]),
models.Index(fields=["user", "year", "week", "board_game", "rank"]),
models.Index(fields=["user", "year", "week", "book", "rank"]),
models.Index(fields=["user", "year", "week", "food", "rank"]),
models.Index(fields=["user", "year", "week", "podcast", "rank"]),
models.Index(fields=["user", "year", "week", "trail", "rank"]),
models.Index(fields=["user", "year", "month", "day", "artist", "rank"]),
models.Index(fields=["user", "year", "month", "day", "album", "rank"]),
models.Index(fields=["user", "year", "month", "day", "tv_series", "rank"]),
]
constraints = [
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "artist"],
condition=Q(artist__isnull=False),
name="unique_chart_artist_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "album"],
condition=Q(album__isnull=False),
name="unique_chart_album_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "track"],
condition=Q(track__isnull=False),
name="unique_chart_track_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "tv_series"],
condition=Q(tv_series__isnull=False),
name="unique_chart_tv_series_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "video"],
condition=Q(video__isnull=False),
name="unique_chart_video_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "podcast"],
condition=Q(podcast__isnull=False),
name="unique_chart_podcast_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "podcast_episode"],
condition=Q(podcast_episode__isnull=False),
name="unique_chart_podcast_episode_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "board_game"],
condition=Q(board_game__isnull=False),
name="unique_chart_board_game_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "trail"],
condition=Q(trail__isnull=False),
name="unique_chart_trail_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "geo_location"],
condition=Q(geo_location__isnull=False),
name="unique_chart_geo_location_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "food"],
condition=Q(food__isnull=False),
name="unique_chart_food_period",
),
models.UniqueConstraint(
fields=["user", "year", "month", "week", "day", "book"],
condition=Q(book__isnull=False),
name="unique_chart_book_period",
),
]
@property
def media_obj(self):
for attr in [
"artist",
"album",
"track",
"tv_series",
"video",
"podcast",
"podcast_episode",
"board_game",
"trail",
"geo_location",
"food",
"book",
]:
obj = getattr(self, attr)
if obj:
return obj
return None
@property
def media_type(self):
mapping = {
"artist": "Artist",
"album": "Album",
"track": "Track",
"tv_series": "TV Series",
"video": "Video",
"podcast": "Podcast",
"podcast_episode": "Podcast Episode",
"board_game": "Board Game",
"trail": "Trail",
"geo_location": "Location",
"food": "Food",
"book": "Book",
}
for attr, name in mapping.items():
if getattr(self, attr) is not None:
return name
return None
@property
def period_str(self) -> str:
if self.day:
return f"{calendar.month_name[self.month]} {self.day}, {self.year}"
if self.week:
return f"Week {self.week}, {self.year}"
if self.month:
return f"{calendar.month_name[self.month]} {self.year}"
return str(self.year)
@property
def period_type(self) -> str:
if self.day:
return "day"
if self.week:
return "week"
if self.month:
return "month"
return "year"
def __str__(self):
obj = self.media_obj
obj_name = str(obj) if obj else "Unknown"
return f"#{self.rank} {obj_name} in {self.period_str}"
def get_absolute_url(self):
return reverse("charts:charts-home")
@property
def chart_url(self) -> str:
url = reverse("charts:charts-home")
date_str = str(self.year)
if self.week:
date_str = f"{self.year}-W{self.week}"
elif self.month:
date_str = f"{self.year}-{self.month:02d}"
return f"{url}?date={date_str}"
@property
def rank_emoji(self) -> str:
emojis = {1: "🥇", 2: "🥈", 3: "🥉"}
return emojis.get(self.rank, f"#{self.rank}")