models.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import logging
  2. from uuid import uuid4
  3. from django.conf import settings
  4. from django.contrib.auth import get_user_model
  5. from django.db import models
  6. from django.urls import reverse
  7. from django_extensions.db.models import TimeStampedModel
  8. from imagekit.models import ImageSpecField
  9. from imagekit.processors import ResizeToFit
  10. from scrobbles.dataclasses import VideoGameLogData
  11. from scrobbles.mixins import LongPlayScrobblableMixin, ScrobblableConstants
  12. from scrobbles.utils import get_scrobbles_for_media
  13. from videogames.igdb import lookup_game_id_from_gdb
  14. logger = logging.getLogger(__name__)
  15. BNULL = {"blank": True, "null": True}
  16. User = get_user_model()
  17. class VideoGamePlatform(TimeStampedModel):
  18. name = models.CharField(max_length=255)
  19. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  20. logo = models.ImageField(upload_to="games/platform-logos/", **BNULL)
  21. igdb_id = models.IntegerField(**BNULL)
  22. def __str__(self):
  23. return self.name
  24. def get_absolute_url(self):
  25. return reverse(
  26. "videogames:platform_detail", kwargs={"slug": self.uuid}
  27. )
  28. class VideoGameCollection(TimeStampedModel):
  29. name = models.CharField(max_length=255)
  30. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  31. cover = models.ImageField(upload_to="games/series-covers/", **BNULL)
  32. cover_small = ImageSpecField(
  33. source="cover",
  34. processors=[ResizeToFit(100, 100)],
  35. format="JPEG",
  36. options={"quality": 60},
  37. )
  38. cover_medium = ImageSpecField(
  39. source="cover",
  40. processors=[ResizeToFit(300, 300)],
  41. format="JPEG",
  42. options={"quality": 75},
  43. )
  44. igdb_id = models.IntegerField(**BNULL)
  45. def __str__(self):
  46. return self.name
  47. def get_absolute_url(self):
  48. return reverse(
  49. "videogames:collection_detail", kwargs={"slug": self.uuid}
  50. )
  51. class VideoGame(LongPlayScrobblableMixin):
  52. COMPLETION_PERCENT = getattr(settings, "GAME_COMPLETION_PERCENT", 100)
  53. FIELDS_FROM_IGDB = [
  54. "igdb_id",
  55. "alternative_name",
  56. "rating",
  57. "rating_count",
  58. "release_date",
  59. "cover",
  60. "screenshot",
  61. ]
  62. FIELDS_FROM_HLTB = [
  63. "hltb_id",
  64. "release_year",
  65. "main_story_time",
  66. "main_extra_time",
  67. "completionist_time",
  68. "hltb_score",
  69. ]
  70. title = models.CharField(max_length=255)
  71. igdb_id = models.IntegerField(**BNULL)
  72. hltb_id = models.IntegerField(**BNULL)
  73. alternative_name = models.CharField(max_length=255, **BNULL)
  74. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  75. cover = models.ImageField(upload_to="games/covers/", **BNULL)
  76. cover_small = ImageSpecField(
  77. source="cover",
  78. processors=[ResizeToFit(100, 100)],
  79. format="JPEG",
  80. options={"quality": 60},
  81. )
  82. cover_medium = ImageSpecField(
  83. source="cover",
  84. processors=[ResizeToFit(300, 300)],
  85. format="JPEG",
  86. options={"quality": 75},
  87. )
  88. screenshot = models.ImageField(upload_to="games/screenshots/", **BNULL)
  89. screenshot_small = ImageSpecField(
  90. source="screenshot",
  91. processors=[ResizeToFit(100, 100)],
  92. format="JPEG",
  93. options={"quality": 60},
  94. )
  95. screenshot_medium = ImageSpecField(
  96. source="screenshot",
  97. processors=[ResizeToFit(300, 300)],
  98. format="JPEG",
  99. options={"quality": 75},
  100. )
  101. summary = models.TextField(**BNULL)
  102. hltb_cover = models.ImageField(upload_to="games/hltb_covers/", **BNULL)
  103. hltb_cover_small = ImageSpecField(
  104. source="hltb_cover",
  105. processors=[ResizeToFit(100, 100)],
  106. format="JPEG",
  107. options={"quality": 60},
  108. )
  109. hltb_cover_medium = ImageSpecField(
  110. source="hltb_cover",
  111. processors=[ResizeToFit(300, 300)],
  112. format="JPEG",
  113. options={"quality": 75},
  114. )
  115. rating = models.FloatField(**BNULL)
  116. rating_count = models.IntegerField(**BNULL)
  117. release_date = models.DateTimeField(**BNULL)
  118. release_year = models.IntegerField(**BNULL)
  119. main_story_time = models.IntegerField(**BNULL)
  120. main_extra_time = models.IntegerField(**BNULL)
  121. completionist_time = models.IntegerField(**BNULL)
  122. hltb_score = models.FloatField(**BNULL)
  123. platforms = models.ManyToManyField(VideoGamePlatform)
  124. retroarch_name = models.CharField(max_length=255, **BNULL)
  125. def __str__(self):
  126. return self.title
  127. @property
  128. def subtitle(self):
  129. return f" On {self.platforms.first()}"
  130. @property
  131. def strings(self) -> ScrobblableConstants:
  132. return ScrobblableConstants(verb="Sessioning", tags="joystick")
  133. @property
  134. def primary_image_url(self) -> str:
  135. url = ""
  136. if self.cover:
  137. url = self.cover_medium.url
  138. if self.hltb_cover:
  139. url = self.hltb_cover_medium.url
  140. return url
  141. def get_absolute_url(self):
  142. return reverse(
  143. "videogames:videogame_detail", kwargs={"slug": self.uuid}
  144. )
  145. def hltb_link(self):
  146. return f"https://howlongtobeat.com/game/{self.hltb_id}"
  147. def igdb_link(self):
  148. slug = self.title.lower().replace(" ", "-")
  149. return f"https://igdb.com/games/{slug}"
  150. @property
  151. def logdata_cls(self):
  152. return VideoGameLogData
  153. @property
  154. def seconds_for_completion(self) -> int:
  155. completion_time = self.run_time_ticks
  156. if not completion_time:
  157. # Default to 10 hours, why not
  158. completion_time = 10 * 60 * 60
  159. return int(completion_time * (self.COMPLETION_PERCENT / 100))
  160. def progress_for_user(self, user_id: int) -> int:
  161. """Used to keep track of whether the game is complete or not"""
  162. user = User.objects.get(id=user_id)
  163. last_scrobble = get_scrobbles_for_media(self, user).last()
  164. if not last_scrobble or not last_scrobble.playback_position:
  165. logger.warn("No total minutes in last scrobble, no progress")
  166. return 0
  167. sec_played = last_scrobble.playback_position * 60
  168. return int(sec_played / self.run_time) * 100
  169. def fix_metadata(self, force_update: bool = False):
  170. from videogames.utils import (
  171. get_or_create_videogame,
  172. load_game_data_from_igdb,
  173. )
  174. if self.hltb_id and force_update:
  175. get_or_create_videogame(str(self.hltb_id), force_update)
  176. if not self.igdb_id:
  177. # This almost never works without intervention
  178. self.igdb_id = lookup_game_id_from_gdb(self.title)
  179. if self.igdb_id:
  180. load_game_data_from_igdb(self.id, self.igdb_id)
  181. if (not self.run_time_ticks or force_update) and self.main_story_time:
  182. self.run_time_seconds = self.main_story_time
  183. self.save(update_fields=["run_time_seconds"])
  184. @classmethod
  185. def find_or_create(cls, data_dict: dict) -> "Game":
  186. from videogames.utils import get_or_create_videogame
  187. return get_or_create_videogame(data_dict.get("hltb_id"))