import os import re from django.contrib.auth.models import User from django.db import models from django.db.models.fields.related import ForeignKey from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ ############################# Helper Functions ####################################################### def space_to_camelcase(value): """Convert a space-separated string to camelCase.""" def camelcase(): yield str.lower while True: yield str.capitalize c = camelcase() return "".join(next(c)(x) if x else "_" for x in value.split()) def score_filename(score, original_name): """Generate filename for uploaded score files.""" file_extension = os.path.splitext(original_name)[1] filename = "scores/" filename += space_to_camelcase(score.piece.title) + "/" filename += space_to_camelcase(score.score_type) filename += file_extension return filename def recording_filename(recording, original_name): """Generate filename for uploaded recording files.""" file_extension = os.path.splitext(original_name)[1] filename = "recordings/" filename += space_to_camelcase(recording.piece.title) + "/" filename += space_to_camelcase(recording.artist) filename += file_extension return filename ####################################################################################################### class Piece(models.Model): """A musical piece in the repertoire.""" title = models.CharField(max_length=255, verbose_name=_("title"), unique=True) composer = models.CharField(max_length=255, blank=True, verbose_name=_("composer")) repertoire_nr = models.IntegerField( null=True, blank=True, unique=True, default=None ) class Meta: ordering = ["title"] permissions = (("manage_scores", "Administrate and manage scores"),) def __str__(self): return self.title def is_in_repertoire(self): return self.repertoire_nr is not None # Backwards compatibility isInRepertoire = is_in_repertoire def get_score_for_user(self, user): """Get the preferred score for a user, or the first available score.""" try: return ScoreUserMapping.objects.get( user=user, score__in=self.scores.all() ).score except ScoreUserMapping.DoesNotExist: if self.scores.exists(): return self.scores.first() return None @staticmethod def get_repertoire(): """Get all pieces that are in the repertoire, ordered by repertoire number.""" return Piece.objects.filter(repertoire_nr__isnull=False).order_by( "repertoire_nr" ) # Backwards compatibility getRepertoire = get_repertoire class BookLocation(models.Model): """Location of a piece in a physical book.""" piece = models.ForeignKey("Piece", on_delete=models.CASCADE) book = models.CharField(max_length=100, blank=False, verbose_name=_("Buch")) page = models.IntegerField(verbose_name=_("page")) class Meta: ordering = ["book", "page"] def __str__(self): return f"{self.book}, {self.page}" class Score(models.Model): """A score file for a piece (e.g., full score, parts, etc.).""" piece = ForeignKey("Piece", on_delete=models.CASCADE, related_name="scores") score_type = models.CharField(max_length=100, verbose_name="score type") file = models.FileField(upload_to=score_filename, verbose_name=_("file")) uploaded_by = ForeignKey( User, on_delete=models.SET_NULL, null=True, verbose_name=_("uploaded_by") ) class Meta: unique_together = (("piece", "score_type"),) ordering = ["score_type"] def __str__(self): return f"{self.piece.title} - {self.score_type}" @property def image_file_name(self): """Get the path for the cached image preview of this score.""" return os.path.splitext("image_cache/" + str(self.file))[0] + ".jpg" @staticmethod def pdf2jpg(source_file, target_file, resolution=100, crop=15): """Convert a PDF to a JPG preview image.""" try: from wand.image import Image with Image( filename=source_file, resolution=(resolution, resolution) ) as img: img.crop( crop, crop, width=img.width - 2 * crop, height=int(0.5 * img.height) - 2 * crop, ) img.format = "jpeg" img.save(filename=target_file) return True except Exception as e: print(f"Error converting PDF to JPG: {e}") return False def get_image_file(self): """Get or create a preview image for this score.""" from django.conf import settings input_file = settings.MEDIA_ROOT + "/" + str(self.file) cache_file = settings.MEDIA_ROOT + "/" + str(self.image_file_name) # Create a jpg for this score, if it does not exist yet if not os.path.exists(cache_file): cache_dir = os.path.dirname(cache_file) if not os.path.exists(cache_dir): os.makedirs(cache_dir) Score.pdf2jpg(input_file, cache_file) return self.image_file_name def is_active_score(self, user): """Check if this is the active score for a user.""" return ScoreUserMapping.objects.filter(score=self, user=user).exists() class Recording(models.Model): """An audio recording of a piece.""" piece = ForeignKey("Piece", on_delete=models.CASCADE, related_name="recordings") artist = models.CharField(max_length=100, verbose_name=_("Artist")) file = models.FileField(upload_to=recording_filename, verbose_name=_("file")) uploaded_by = ForeignKey( User, on_delete=models.SET_NULL, null=True, verbose_name=_("uploaded_by") ) class Meta: unique_together = (("piece", "artist"),) ordering = ["artist"] def __str__(self): return f"{self.piece.title} - {self.artist}" class YoutubeRecording(models.Model): """A YouTube link for a piece.""" piece = models.ForeignKey( "Piece", on_delete=models.CASCADE, related_name="youtubeLinks" ) link = models.CharField(max_length=300, blank=False) uploaded_by = ForeignKey( User, on_delete=models.SET_NULL, null=True, verbose_name=_("uploaded_by") ) youtube_regex = re.compile( r"(?:https://)?(?:http://)?www\.youtube\.(?:com|de)/watch\?v=(?P[-\w]*)" ) class Meta: unique_together = (("link", "piece"),) def __str__(self): return f"{self.piece.title} - YouTube" @property def embed_html(self): """Generate embeddable HTML for this YouTube video.""" replacement = """
""" return mark_safe(self.youtube_regex.sub(replacement, self.link)) class ScoreUserMapping(models.Model): """Maps a user's preferred score for each piece.""" score = ForeignKey("Score", on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) piece = ForeignKey("Piece", on_delete=models.CASCADE) class Meta: unique_together = (("piece", "user"),) def __str__(self): return f"{self.user.username} -> {self.score}" @staticmethod def add_user_score_mapping(score, user): """Set the preferred score for a user for a given piece.""" piece = score.piece ScoreUserMapping.objects.filter(user=user, piece=piece).delete() ScoreUserMapping.objects.create(score=score, user=user, piece=piece)