models.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import logging
  2. import os
  3. import uuid
  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_web.utils import ChoiceEnum
  12. from taggit.managers import TaggableManager
  13. logger = logging.getLogger(__name__)
  14. def get_screenshot_upload_path(instance, filename):
  15. return f"{instance.game_system.retropie_slug}/screenshots/{filename}"
  16. def get_marquee_upload_path(instance, filename):
  17. return f"{instance.game_system.retropie_slug}/marquee/{filename}"
  18. def get_video_upload_path(instance, filename):
  19. return f"{instance.game_system.retropie_slug}/videos/{filename}"
  20. def get_rom_upload_path(instance, filename):
  21. return f"{instance.game_system.retropie_slug}/{filename}"
  22. class BaseModel(TimeStampedModel):
  23. """A base model for providing name and slugged fields for organizational models"""
  24. uuid = models.UUIDField(default=uuid.uuid4, editable=False)
  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. region = models.CharField(
  172. max_length=10,
  173. choices=Region.choices(),
  174. blank=True,
  175. null=True,
  176. )
  177. source = models.CharField(
  178. max_length=500,
  179. blank=True,
  180. null=True,
  181. )
  182. featured_on = models.DateField(
  183. blank=True,
  184. null=True,
  185. )
  186. tags = TaggableManager(blank=True)
  187. class Meta:
  188. ordering = ["game_system", "name"]
  189. def __str__(self):
  190. return f"{self.name} for {self.game_system}"
  191. def get_absolute_url(self):
  192. return reverse("games:game_detail", args=[self.slug])
  193. @property
  194. def rating_by_100(self) -> float:
  195. if self.rating:
  196. return int(100 * self.rating)
  197. return int(0)
  198. @property
  199. def rating_class(self):
  200. if self.rating_by_100 > 70:
  201. return "high"
  202. if self.rating_by_100 > 50:
  203. return "medium"
  204. return "low"
  205. @property
  206. def in_progress(self):
  207. return self.started_on and not self.finished_on
  208. @property
  209. def retroarch_core_path(self):
  210. path = None
  211. retroarch_core = self.game_system.defaults.get("retroarch_core", None)
  212. if retroarch_core:
  213. path = quote(
  214. os.path.join(
  215. settings.ROMS_DIR,
  216. "cores",
  217. retroarch_core + "_libretro.so",
  218. )
  219. )
  220. return path
  221. @property
  222. def webretro_url(self):
  223. if "webretro_core" in self.game_system.defaults.keys():
  224. return reverse("games:game_play_detail", args=[self.slug])
  225. def retroarch_cmd(self, platform="linux"):
  226. if platform != "linux":
  227. return ""
  228. if not self.retroarch_core_path:
  229. return ""
  230. if not self.rom_file:
  231. return ""
  232. rom_file = quote(self.rom_file.path)
  233. if self.game_system.slug == "scummvm":
  234. new_path = list()
  235. try:
  236. split_path = self.rom_file.path.split("/")
  237. folder_name = self.rom_file.path.split("/")[-1].split(".")[0]
  238. for i in split_path:
  239. if i == "scummvm":
  240. new_path.append(f"scummvm/{folder_name}")
  241. else:
  242. new_path.append(i)
  243. except IndexError:
  244. pass
  245. if new_path:
  246. rom_file = quote("/".join(new_path))
  247. if not os.path.exists(self.retroarch_core_path):
  248. logger.info(
  249. f"Missing libretro core file at {self.retroarch_core_path}"
  250. )
  251. return f"Libretro core not found at {self.retroarch_core_path}"
  252. return f"retroarch -L {self.retroarch_core_path} {rom_file} -v"
  253. def cmd(self, platform="linux"):
  254. cmd = None
  255. if self.retroarch_cmd(platform):
  256. return self.retroarch_cmd(platform)
  257. rom_file = quote(self.rom_file.path)
  258. emulator = self.game_system.defaults.get("emulator", None)
  259. if emulator == "PCSX2":
  260. cmd = f"{emulator} --console --fullscreen --nogui {rom_file}"
  261. return cmd
  262. class GameCollection(BaseModel):
  263. games = models.ManyToManyField(Game)
  264. game_system = models.ForeignKey(
  265. GameSystem,
  266. on_delete=models.SET_NULL,
  267. blank=True,
  268. null=True,
  269. )
  270. developer = models.ForeignKey(
  271. Developer,
  272. on_delete=models.SET_NULL,
  273. blank=True,
  274. null=True,
  275. )
  276. publisher = models.ForeignKey(
  277. Publisher,
  278. on_delete=models.SET_NULL,
  279. blank=True,
  280. null=True,
  281. )
  282. genre = models.ForeignKey(
  283. Genre,
  284. on_delete=models.SET_NULL,
  285. blank=True,
  286. null=True,
  287. )
  288. def __str__(self):
  289. return f"{self.name}"
  290. def get_absolute_url(self):
  291. return reverse("games:gamecollection_detail", args=[self.slug])
  292. @property
  293. def rating_avg(self):
  294. avg = self.games.aggregate(models.Avg("rating"))["rating__avg"]
  295. if avg:
  296. return int(100 * avg)
  297. return 0
  298. def export_to_file(self, dryrun=True):
  299. """Will dump this collection to a .cfg file in /tmp or
  300. our COLLECTIONS_DIR configured path if dryrun=False"""
  301. collection_slug = self.slug.replace("-", "")
  302. file_path = f"/tmp/custom-{collection_slug}.cfg"
  303. if not dryrun:
  304. file_path = os.path.join(
  305. settings.COLLECTIONS_DIR, f"custom-{collection_slug}.cfg"
  306. )
  307. with open(file_path, "w") as outfile:
  308. for game in self.games.all():
  309. outfile.write(game.rom_file.path + "\n")