models.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import os
  2. from shlex import quote
  3. from enum import Enum
  4. from django.conf import settings
  5. from django.core.validators import MaxValueValidator, MinValueValidator
  6. from django.db import models
  7. from django.urls import reverse
  8. from django_extensions.db.models import TimeStampedModel
  9. from django_extensions.db.fields import AutoSlugField
  10. from emus.utils import ChoiceEnum
  11. def get_screenshot_upload_path(instance, filename):
  12. return f"{instance.game_system.retropie_slug}/screenshots/{filename}"
  13. def get_marquee_upload_path(instance, filename):
  14. return f"{instance.game_system.retropie_slug}/marquee/{filename}"
  15. def get_video_upload_path(instance, filename):
  16. return f"{instance.game_system.retropie_slug}/videos/{filename}"
  17. def get_rom_upload_path(instance, filename):
  18. return f"{instance.game_system.retropie_slug}/{filename}"
  19. class Region(Enum):
  20. USA = "US"
  21. EUROPE = "EU"
  22. JAPAN = "JP"
  23. class BaseModel(TimeStampedModel):
  24. """A base model for providing name and slugged fields for organizational models"""
  25. name = models.CharField(max_length=255)
  26. slug = AutoSlugField(populate_from="name")
  27. class Meta:
  28. ordering = ["name"]
  29. abstract = True
  30. def slugify_function(self, content):
  31. for element in settings.REMOVE_FROM_SLUGS:
  32. content = content.replace(element, "-")
  33. return content.lower()
  34. def __str__(self):
  35. return self.name
  36. class StatisticsMixin(models.Model):
  37. class Meta:
  38. abstract = True
  39. @property
  40. def rating_avg(self):
  41. avg = self.game_set.aggregate(models.Avg("rating"))["rating__avg"]
  42. if avg:
  43. return int(100 * avg)
  44. return 0
  45. class Genre(BaseModel, StatisticsMixin):
  46. def get_absolute_url(self):
  47. return reverse("games:genre_detail", args=[self.slug])
  48. class Publisher(BaseModel, StatisticsMixin):
  49. def get_absolute_url(self):
  50. return reverse("games:publisher_detail", args=[self.slug])
  51. class Developer(BaseModel, StatisticsMixin):
  52. def get_absolute_url(self):
  53. return reverse("games:developer_detail", args=[self.slug])
  54. class GameSystem(BaseModel, StatisticsMixin):
  55. retropie_slug = models.CharField(
  56. blank=True,
  57. null=True,
  58. max_length=50,
  59. )
  60. color = models.CharField(
  61. blank=True,
  62. null=True,
  63. max_length=6,
  64. help_text="Hex value for console badges",
  65. )
  66. @property
  67. def defaults(self):
  68. return settings.GAME_SYSTEM_DEFAULTS.get(self.retropie_slug, None)
  69. @property
  70. def get_color(self):
  71. color = self.color
  72. if not color and self.defaults:
  73. color = self.defaults.get("color", "")
  74. return color
  75. @property
  76. def webretro_core(self):
  77. core = None
  78. if self.defaults:
  79. core = self.defaults.get("webretro_core", None)
  80. return core
  81. def get_absolute_url(self):
  82. return reverse("games:game_system_detail", args=[self.slug])
  83. class Game(BaseModel):
  84. class Region(ChoiceEnum):
  85. US = "USA"
  86. EU = "Europe"
  87. JP = "Japan"
  88. X = "Unknown"
  89. game_system = models.ForeignKey(
  90. GameSystem,
  91. on_delete=models.SET_NULL,
  92. null=True,
  93. )
  94. release_date = models.DateField(
  95. blank=True,
  96. null=True,
  97. )
  98. developer = models.ForeignKey(
  99. Developer,
  100. on_delete=models.SET_NULL,
  101. blank=True,
  102. null=True,
  103. )
  104. publisher = models.ForeignKey(
  105. Publisher,
  106. on_delete=models.SET_NULL,
  107. blank=True,
  108. null=True,
  109. )
  110. genre = models.ManyToManyField(
  111. Genre,
  112. )
  113. players = models.SmallIntegerField(
  114. default=1,
  115. )
  116. kid_game = models.BooleanField(
  117. default=False,
  118. )
  119. description = models.TextField(
  120. blank=True,
  121. null=True,
  122. )
  123. rating = models.FloatField(
  124. blank=True,
  125. null=True,
  126. validators=[MaxValueValidator(1), MinValueValidator(0)],
  127. )
  128. video = models.FileField(
  129. blank=True,
  130. null=True,
  131. max_length=300,
  132. upload_to=get_video_upload_path,
  133. )
  134. marquee = models.ImageField(
  135. blank=True,
  136. null=True,
  137. max_length=300,
  138. upload_to=get_marquee_upload_path,
  139. )
  140. screenshot = models.ImageField(
  141. blank=True,
  142. null=True,
  143. max_length=300,
  144. upload_to=get_screenshot_upload_path,
  145. )
  146. rom_file = models.FileField(
  147. blank=True,
  148. null=True,
  149. max_length=300,
  150. upload_to=get_rom_upload_path,
  151. )
  152. hack = models.BooleanField(
  153. default=False,
  154. )
  155. hack_version = models.CharField(
  156. max_length=255,
  157. blank=True,
  158. null=True,
  159. )
  160. english_patched = models.BooleanField(
  161. default=False,
  162. )
  163. english_patched_version = models.CharField(
  164. max_length=50,
  165. blank=True,
  166. null=True,
  167. )
  168. undub = models.BooleanField(
  169. default=False,
  170. )
  171. featured = models.BooleanField(
  172. default=False,
  173. )
  174. region = models.CharField(
  175. max_length=10,
  176. choices=Region.choices(),
  177. blank=True,
  178. null=True,
  179. )
  180. class Meta:
  181. ordering = ["game_system", "name"]
  182. def __str__(self):
  183. return f"{self.name} for {self.game_system}"
  184. def get_absolute_url(self):
  185. return reverse("games:game_detail", args=[self.slug])
  186. @property
  187. def rating_by_100(self) -> float:
  188. if self.rating:
  189. return int(100 * self.rating)
  190. return int(0)
  191. @property
  192. def retroarch_core_path(self):
  193. path = None
  194. retroarch_core = self.game_system.defaults.get("retroarch_core", None)
  195. if retroarch_core:
  196. path = quote(
  197. os.path.join(
  198. settings.ROMS_DIR,
  199. "cores",
  200. retroarch_core + "_libretro.so",
  201. )
  202. )
  203. return path
  204. @property
  205. def webretro_url(self):
  206. if "webretro_core" in self.game_system.defaults.keys():
  207. return reverse("games:game_play_detail", args=[self.slug])
  208. def retroarch_cmd(self, platform="linux"):
  209. if platform != "linux":
  210. return ""
  211. if not self.retroarch_core_path:
  212. return ""
  213. if not self.rom_file:
  214. return ""
  215. rom_file = quote(self.rom_file.path)
  216. return f"retroarch -L {self.retroarch_core_path} {rom_file}"
  217. def cmd(self, platform="linux"):
  218. cmd = None
  219. if self.retroarch_cmd(platform):
  220. return self.retroarch_cmd(platform)
  221. rom_file = quote(self.rom_file.path)
  222. emulator = self.game_system.defaults.get("emulator", None)
  223. if emulator == "PCSX2":
  224. cmd = f"{emulator} --console --fullscreen --nogui {rom_file}"
  225. return cmd