Files
blechreiz-website/scoremanager/models.py

241 lines
7.8 KiB
Python

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<videoID>[-\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 = """
<div class="embed-container">
<iframe src="//www.youtube.com/embed/\\g<videoID>"
frameborder="0" allowfullscreen></iframe>
</div>
"""
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)