models.py 8.4 KB

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