models.py 8.7 KB

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