from os import SCHED_OTHER from django.conf import settings from django.db import models from django.utils.text import slugify from django.utils.timezone import now from django.utils.functional import cached_property from django.core.files.storage import get_storage_class from django.db.models import Q import re import logging logger = logging.getLogger(__name__) try: library_storage = get_storage_class(settings.LIBRARY_STORAGE)() except (ImportError, AttributeError): library_storage = get_storage_class()() logger.info("Library storage: %s", library_storage.__class__.__name__) # taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_Instruments ABBREVIATIONS = """ score Score acc Accordion afl Alto flute alt Alto (voice) (contralto) arp Arpeggione bag Bagpipe bar Baritone (voice) bass Bass (voice) bbar Bass baritone (voice) bc Continuo (Basso continuo) bcl Bass clarinet bell Bell (Chimes) bfl Bass flute bgtr Bass guitar bjo Banjo bn Bassoon bob Bass oboe (Baritone oboe) br Brass instruments bryt Baryton bstcl Basset clarinet bsthn Basset horn bug Bugle cbcl Contrabass clarinet cbn Contrabassoon cch Children's chorus cel Celesta ch Mixed chorus cimb Cimbalom cit Cittern cl Clarinet clvd Clavichord cm Chalumeau conc Concertina crh Crumhorn crt Cornet crtt Cornett (Zink) cv Child's voice db Double Bass dlcn Dulcian dom Domra dulc Dulcimer egtr Electric guitar eh English horn (Cor anglais) elec Electronic Instruments epf Electric piano eq Equal voices erhu Erhu euph Euphonium fch Female chorus fda Flute d'amore (Tenor flute) fgh Flugelhorn fife Fife fl Flute flag Flageolet ghca Glass harmonica (Bowl organ) gl Glockenspiel gtr Guitar harm Harmonium hca Harmonica (Mouth Organ) heck Heckelphone hn Horn hp Harp hpd Harpsichord kbd Keyboard instrument lute Lute lyre Lyre mand Mandolin mar Marimba mch Male chorus mez Mezzo-soprano mus Musette nar Narrator (Reciter) ob Oboe oca Ocarina oda Oboe d'amore om Ondes Martenot oph Ophicleide orch Orchestra org Organ oud Oud pan Pan flute (Pan-pipes) perc Percussion pf Piano pf3h Piano 3 hands pf4h Piano 4 hands pf5h Piano 5 hands pf6h Piano 6 hands pflh Piano left hand pfped Pedal piano pfrh Piano right hand picc Piccolo pipa Pipa pk Timpani ptpt Piccolo trumpet reb Rebec rec Recorder sar Sarrusophone sax Saxophone sheng Sheng shw Shawm sit Sitar skbt Sackbut sop Soprano (voice) srp Serpent stpt Slide trumpet str String instruments sxh Saxhorn syn Synthesizer tba Tuba tbn Trombone ten Tenor thrm Theremin timp Timpani tpt Trumpet uch Unison chorus uke Ukelele (Ukulele) v Voice (solo) va Viola vap Viola pomposa vc Cello vda Viola d'amore vib Vibraphone vie Vielle (Hurdy-Gurdy) viol Viol (Viola da gamba) vlne Violone vn Violin vuv Vuvuzela vv Voices (multiple soloists) wag Wagner tuba ww Woodwind instruments xiao Xiao xyl Xylophone zith Zither """ INSTRUMENTS = [] for line in ABBREVIATIONS.split('\n'): parts = line.strip().split(maxsplit=1) if len(parts) < 2: continue name, _, _ = parts[1].partition('(') INSTRUMENTS.append((parts[0], name)) ''' ORCHESTRATIONS = { 'SATB': ('S', 'A', 'T', 'B'), 'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'), 'String Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb'), 'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb', 'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Hn1', 'Hn2', 'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba', 'Timp', 'Drum', 'Perc'), 'Custom': (), } ''' DOCTYPES = [ (1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source'), ] LICENCE_TYPES = [ (2, 'Public Domain'), (4, 'Copyright Expired'), (6, 'Copyrighted'), (10, 'Internal use only'), ] ACCESS_TYPES = [ (1, 'Unlimited'), (2, 'Approval required'), ] def tag_to_instrument(tag): m = re.match(r'([A-Za-z]+)(\d*)', tag) if not m: return tag l = m.groups() return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip() ''' class Orchestration(models.Model): """ Stores a list of instrument codes as a single entry (space delimited). Can be global or ensemble specific """ ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="orchestrations", null=True, blank=True) name = models.CharField(max_length=100) instruments = models.TextField() def as_list(self): tags = [ t.strip() for t in self.instruments.split(' ') ] return [ (t, tag_to_instrument(t)) for t in tags if t ] def save(self): self.as_list() super(Orchestration, self).save() def __str__(self): return self.name ''' class Item(models.Model): """ Item represents a specic version of a Work in a Project e.g. item in set list or programme It also allows works to be shared from one ensemble to another on a per-project basis. """ project = models.ForeignKey('interface.Project', on_delete=models.CASCADE) work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name='project_items') checkout = models.DateTimeField() due = models.DateTimeField(null=True, blank=True) returned = models.DateTimeField(null=True, blank=True) approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE) order = models.SmallIntegerField(default=0) version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag") class Meta: ordering = ['order', 'work'] def __str__(self): return f"<{self.project.slug}:{self.work.slug}>" class Collection(models.Model): """ Storage location for works (physical or virtual) """ name = models.CharField(max_length=255, help_text="Name of the collection") administrators = models.ManyToManyField('auth.User', related_name="collections", help_text="Administrators for this collection") location = models.CharField(max_length=100, help_text="Physical location (institution, town...)") storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True, help_text="Storage for documents") notes = models.TextField(blank=True, help_text="Publicly visible notes about collection and loans policy") #ensembles = models.ManyToManyField('interface.Ensemble', related_name="collections", through='EnsembleAccess') def __str__(self): return self.name class EnsembleAccess(models.Model): """ Can have different access levels to a collection """ ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="allowed_collections") collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="allowed_ensembles") access_type = models.PositiveSmallIntegerField(choices=ACCESS_TYPES, default=2) class Meta: verbose_name_plural = "Ensemble access" class Work(models.Model): """ A musical work 'owned' by a collection from a licencing perspective. """ collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works", help_text="Owner") slug = models.SlugField(max_length=100, editable=False) name = models.CharField(max_length=255) edition = models.CharField(max_length=100, blank=True, help_text="Edition details") parent = models.ForeignKey('Work', null=True, blank=True, on_delete=models.SET_NULL, related_name="related_works", help_text="Arrangement of another work or part of an anthology") composer = models.CharField(max_length=255, blank=True, help_text="Use Composer / Arranger format") #orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works', blank=True) orchestration = models.CharField(max_length=255, blank=True, help_text="IMDB format instrumentation") parts = models.JSONField(null=True, blank=True) # Collection details collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works") code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number") licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status") max_loans = models.IntegerField(default=1, help_text="How many projects can this work be attached to") # Extra info running_time = models.IntegerField(null=True, blank=True, help_text="Running time in seconds") notes = models.TextField(blank=True) tag_list = models.CharField(max_length=255, blank=True, help_text="Multiple tags for the work") projects = models.ManyToManyField('interface.Project', through='Item', related_name="works", help_text="Current usage") @property def duration(self): if self.running_time is None: return "-:--" return "{0:d}:{1:02d}".format(int(self.running_time / 60), self.running_time % 60) @property def tags(self): return self.tag_list.split(';') if self.tag_list else [] @tags.setter def set_tags(self, tags): self.tag_list = ";".join(tags) @property def digital_parts(self): return Part.objects.filter(doc__work=self.pk) @property def physical_parts(self): if not self.parts: return [] return [ (tag_to_instrument(k), v) for (k, v) in self.parts.items() ] #@property #def instruments(self): # return self.orchestration.as_list() def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super(Work, self).save(*args, **kwargs) @property def active_projects(self): return self.projects.filter(active=True) @property def current_loans(self): return self.project_items.filter(checkout__lte=now(), returned=None).select_related('project') @cached_property def loans(self): try: return self.loan_count except AttributeError: return self.project_items.filter(checkout__lte=now(), returned=None).count() @property def is_available(self): if self.max_loans < 0: return True return self.max_loans > self.loans @property def available(self): if self.max_loans < 0: return 'Unlimited' a = self.max_loans - self.loans return '{0} of {1}'.format(max(a, 0), self.max_loans) @property def identifier(self): return f"{self.collection.pk:03d}-{self.pk:03d}" def __str__(self): return f"{self.name} ({self.composer})" def doc_upload_filename(doc, filename): storage = doc.work.collection.storage if not storage: raise RuntimeError("Collection has no storage attached") return f'{storage}:works/{doc.work.slug}-{doc.work.pk}/{filename}' class Document(models.Model): """ Document represents a single file stored in the storage backend. """ work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs") doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1) upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage) created = models.DateTimeField(auto_now_add=True) version = models.CharField(max_length=30, blank=True) def __str__(self): return self.upload.name class Part(models.Model): """ Part is a tagged portion of a Document """ doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="parts") tag = models.SlugField(max_length=20) start = models.SmallIntegerField(null=True, blank=True) end = models.SmallIntegerField(null=True, blank=True) notes = models.TextField(blank=True) class Meta: ordering = ['doc', 'start', 'pk'] @property def instrument(self): return tag_to_instrument(self.tag) @property def filename(self): return slugify(f'{self.doc.work.name}_{self.instrument}') + '.pdf' @property def pagerange(self): if self.start: if self.end: return f"{self.start}-{self.end}" return str(self.start) return "all" def __str__(self): return f'{self.doc.upload} [{self.pagerange}]'