import logging import os from enum import Enum from shlex import quote from django.conf import settings from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.urls import reverse from django_extensions.db.fields import AutoSlugField from django_extensions.db.models import TimeStampedModel from emus.utils import ChoiceEnum logger = logging.getLogger(__name__) def get_screenshot_upload_path(instance, filename): return f"{instance.game_system.retropie_slug}/screenshots/{filename}" def get_marquee_upload_path(instance, filename): return f"{instance.game_system.retropie_slug}/marquee/{filename}" def get_video_upload_path(instance, filename): return f"{instance.game_system.retropie_slug}/videos/{filename}" def get_rom_upload_path(instance, filename): return f"{instance.game_system.retropie_slug}/{filename}" class Region(Enum): USA = "US" EUROPE = "EU" JAPAN = "JP" class BaseModel(TimeStampedModel): """A base model for providing name and slugged fields for organizational models""" name = models.CharField(max_length=255) slug = AutoSlugField(populate_from="name") class Meta: ordering = ["name"] abstract = True def slugify_function(self, content): for element in settings.REMOVE_FROM_SLUGS: content = content.replace(element, "-") return content.lower() def __str__(self): return self.name class StatisticsMixin(models.Model): class Meta: abstract = True @property def rating_avg(self): avg = self.game_set.aggregate(models.Avg("rating"))["rating__avg"] if avg: return int(100 * avg) return 0 class Genre(BaseModel, StatisticsMixin): def get_absolute_url(self): return reverse("games:genre_detail", args=[self.slug]) class Publisher(BaseModel, StatisticsMixin): def get_absolute_url(self): return reverse("games:publisher_detail", args=[self.slug]) class Developer(BaseModel, StatisticsMixin): def get_absolute_url(self): return reverse("games:developer_detail", args=[self.slug]) class GameSystem(BaseModel, StatisticsMixin): retropie_slug = models.CharField( blank=True, null=True, max_length=50, ) color = models.CharField( blank=True, null=True, max_length=6, help_text="Hex value for console badges", ) @property def defaults(self): return settings.GAME_SYSTEM_DEFAULTS.get(self.retropie_slug, None) @property def get_color(self): color = self.color if not color and self.defaults: color = self.defaults.get("color", "") return color @property def webretro_core(self): core = None if self.defaults: core = self.defaults.get("webretro_core", None) return core def get_absolute_url(self): return reverse("games:game_system_detail", args=[self.slug]) class Game(BaseModel): class Region(ChoiceEnum): US = "USA" EU = "Europe" JP = "Japan" X = "Unknown" game_system = models.ForeignKey( GameSystem, on_delete=models.SET_NULL, null=True, ) release_date = models.DateField( blank=True, null=True, ) developer = models.ForeignKey( Developer, on_delete=models.SET_NULL, blank=True, null=True, ) publisher = models.ForeignKey( Publisher, on_delete=models.SET_NULL, blank=True, null=True, ) genre = models.ManyToManyField( Genre, ) players = models.SmallIntegerField( default=1, ) kid_game = models.BooleanField( default=False, ) description = models.TextField( blank=True, null=True, ) rating = models.FloatField( blank=True, null=True, validators=[MaxValueValidator(1), MinValueValidator(0)], ) video = models.FileField( blank=True, null=True, max_length=300, upload_to=get_video_upload_path, ) marquee = models.ImageField( blank=True, null=True, max_length=300, upload_to=get_marquee_upload_path, ) screenshot = models.ImageField( blank=True, null=True, max_length=300, upload_to=get_screenshot_upload_path, ) rom_file = models.FileField( blank=True, null=True, max_length=300, upload_to=get_rom_upload_path, ) hack = models.BooleanField( default=False, ) hack_version = models.CharField( max_length=255, blank=True, null=True, ) english_patched = models.BooleanField( default=False, ) english_patched_version = models.CharField( max_length=50, blank=True, null=True, ) undub = models.BooleanField( default=False, ) featured = models.BooleanField( default=False, ) region = models.CharField( max_length=10, choices=Region.choices(), blank=True, null=True, ) class Meta: ordering = ["game_system", "name"] def __str__(self): return f"{self.name} for {self.game_system}" def get_absolute_url(self): return reverse("games:game_detail", args=[self.slug]) @property def rating_by_100(self) -> float: if self.rating: return int(100 * self.rating) return int(0) @property def retroarch_core_path(self): path = None retroarch_core = self.game_system.defaults.get("retroarch_core", None) if retroarch_core: path = quote( os.path.join( settings.ROMS_DIR, "cores", retroarch_core + "_libretro.so", ) ) return path @property def webretro_url(self): if "webretro_core" in self.game_system.defaults.keys(): return reverse("games:game_play_detail", args=[self.slug]) def retroarch_cmd(self, platform="linux"): if platform != "linux": return "" if not self.retroarch_core_path: return "" if not self.rom_file: return "" rom_file = quote(self.rom_file.path) if not os.path.exists(self.retroarch_core_path): logger.info(f"Missing libretro core file at {self.retroarch_core_path}") return f"Libretro core not found at {self.retroarch_core_path}" return f"retroarch -L {self.retroarch_core_path} {rom_file} -v" def cmd(self, platform="linux"): cmd = None if self.retroarch_cmd(platform): return self.retroarch_cmd(platform) rom_file = quote(self.rom_file.path) emulator = self.game_system.defaults.get("emulator", None) if emulator == "PCSX2": cmd = f"{emulator} --console --fullscreen --nogui {rom_file}" return cmd class GameCollection(BaseModel): games = models.ManyToManyField(Game) game_system = models.ForeignKey( GameSystem, on_delete=models.SET_NULL, blank=True, null=True, ) developer = models.ForeignKey( Developer, on_delete=models.SET_NULL, blank=True, null=True, ) publisher = models.ForeignKey( Publisher, on_delete=models.SET_NULL, blank=True, null=True, ) genre = models.ForeignKey( Genre, on_delete=models.SET_NULL, blank=True, null=True, ) def __str__(self): return f"{self.name}" def get_absolute_url(self): return reverse("games:gamecollection_detail", args=[self.slug]) @property def rating_avg(self): avg = self.games.aggregate(models.Avg("rating"))["rating__avg"] if avg: return int(100 * avg) return 0 def export_to_file(self, dryrun=True): """Will dump this collection to a .cfg file in /tmp or our COLLECTIONS_DIR configured path if dryrun=False""" file_path = f"/tmp/custom-{self.slug}.cfg" if not dryrun: file_path = os.path.join( settings.COLLECTIONS_DIR, f"custom-{self.slug}.cfg" ) with open(file_path, "w") as outfile: for game in self.games.all(): outfile.write(game.rom_file.path + "\n")