417 lines
12 KiB
Python
417 lines
12 KiB
Python
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}]' |