spradio-server-django/savepointradio/radio/models.py

311 lines
11 KiB
Python

'''
Django Models for the Radio application.
'''
from datetime import timedelta
from decimal import getcontext, Decimal, ROUND_UP
from django.apps import apps
from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from dynamic_preferences.registries import global_preferences_registry
from core.behaviors import Disableable, Publishable, Timestampable
from .fields import RadioIRIField
from .managers import RadioManager, SongManager
# Set decimal precision
getcontext().prec = 16
radio_settings = global_preferences_registry.manager()
class Album(Disableable, Publishable, Timestampable, models.Model):
'''
A model for a music album.
'''
title = models.CharField(_('title'), max_length=255, unique=True)
sorted_title = models.CharField(_('naturalized title'),
db_index=True,
editable=False,
max_length=255)
objects = models.Manager()
music = RadioManager()
class Meta:
ordering = ['sorted_title', ]
def __str__(self):
return self.title
class Artist(Disableable, Publishable, Timestampable, models.Model):
'''
A model for a music artist.
'''
alias = models.CharField(_('alias'), max_length=127, blank=True)
first_name = models.CharField(_('first name'), max_length=127, blank=True)
last_name = models.CharField(_('last name'), max_length=127, blank=True)
sorted_full_name = models.CharField(_('naturalized full name'),
db_index=True,
editable=False,
max_length=255)
objects = models.Manager()
music = RadioManager()
class Meta:
ordering = ['sorted_full_name', ]
@property
def full_name(self):
'''
String representing the artist's full name including an alias, if
available.
'''
if self.alias:
if self.first_name or self.last_name:
return '{} "{}" {}'.format(self.first_name,
self.alias,
self.last_name)
return self.alias
return '{} {}'.format(self.first_name, self.last_name)
def __str__(self):
return self.full_name
class Game(Disableable, Publishable, Timestampable, models.Model):
'''
A model for a game.
'''
title = models.CharField(_('title'), max_length=255, unique=True)
sorted_title = models.CharField(_('naturalized title'),
db_index=True,
editable=False,
max_length=255)
objects = models.Manager()
music = RadioManager()
class Meta:
ordering = ['sorted_title', ]
def __str__(self):
return self.title
class Store(Timestampable, models.Model):
'''
A model to represent various data locations (stores) for the song.
'''
iri = RadioIRIField(_('IRI path to song file'))
mime_type = models.CharField(_('file MIME type'),
max_length=64,
blank=True)
file_size = models.BigIntegerField(_('file size'),
validators=[MinValueValidator(0)],
blank=True,
null=True)
length = models.DecimalField(_('song length (in seconds)'),
max_digits=8,
decimal_places=2,
null=True,
blank=True)
track_gain = models.DecimalField(_('recommended replaygain adjustment'),
max_digits=6,
decimal_places=2,
null=True,
blank=True)
track_peak = models.DecimalField(_('highest volume level in the track'),
max_digits=10,
decimal_places=6,
null=True,
blank=True)
def _replaygain(self):
'''
String representation of the recommended amplitude adjustment.
'''
if self.track_gain is None:
return '+0.00 dB'
if self.track_gain > 0:
return '+{} dB'.format(str(self.track_gain))
return '{} dB'.format(str(self.track_gain))
replaygain = property(_replaygain)
def __str__(self):
return self.iri
class Song(Disableable, Publishable, Timestampable, models.Model):
'''
A model for a song.
'''
JINGLE = 'J'
SONG = 'S'
TYPE_CHOICES = (
(JINGLE, 'Jingle'),
(SONG, 'Song'),
)
album = models.ForeignKey(Album,
on_delete=models.SET_NULL,
null=True,
blank=True)
artists = models.ManyToManyField(Artist, blank=True, related_name='songs')
game = models.ForeignKey(Game,
on_delete=models.SET_NULL,
null=True,
blank=True)
song_type = models.CharField(_('song type'),
max_length=1,
choices=TYPE_CHOICES,
default=SONG)
title = models.CharField(_('title'), max_length=255)
num_played = models.PositiveIntegerField(_('number of times played'),
default=0)
last_played = models.DateTimeField(_('was last played'),
null=True,
blank=True,
editable=False)
next_play = models.DateTimeField(_('can be played again'),
null=True,
blank=True,
editable=False)
stores = models.ManyToManyField(Store, blank=True, related_name='song')
active_store = models.ForeignKey(Store,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='active_for')
sorted_title = models.CharField(_('naturalized title'),
db_index=True,
editable=False,
max_length=255)
objects = models.Manager()
music = SongManager()
class Meta:
ordering = ['sorted_title', ]
def _is_jingle(self):
'''
Is the object a jingle?
'''
return self.song_type == 'J'
_is_jingle.boolean = True
is_jingle = property(_is_jingle)
def _is_song(self):
'''
Is the object a song?
'''
return self.song_type == 'S'
_is_song.boolean = True
is_song = property(_is_song)
def _is_available(self):
'''
Is the object both enabled and published?
'''
return self._is_enabled() and self._is_published()
_is_available.boolean = True
is_available = property(_is_available)
def _full_title(self):
'''
String representing the entire song title, including the game and
artists involved.
'''
if self._is_song():
enabled_artists = self.artists.all().filter(disabled=False)
all_artists = ', '.join([a.full_name for a in enabled_artists])
return '{} - {} [{}]'.format(self.game.title,
self.title,
all_artists)
return self.title
full_title = property(_full_title)
def _average_rating(self):
'''
Decimal number of the average rating of a song from 1 - 5.
'''
ratings = self.rating_set.all()
if ratings:
avg = Decimal(ratings.aggregate(avg=models.Avg('value'))['avg'])
return avg.quantize(Decimal('.01'), rounding=ROUND_UP)
return None
average_rating = property(_average_rating)
def get_time_until_requestable(self):
'''
Length of time before a song can be requested again.
'''
if self._is_song() and self._is_available():
if self.last_played:
allowed_datetime = Song.music.datetime_from_wait()
remaining_wait = self.last_played - allowed_datetime
if remaining_wait.total_seconds() > 0:
return remaining_wait
return timedelta(seconds=0)
return None
def get_date_when_requestable(self, last_play=None):
'''
Datetime when a song can be requested again.
'''
last = self.last_played if last_play is None else last_play
if self._is_song() and self._is_available():
if last:
# Check if we have enough ratings to change ratio
min_rate = radio_settings['replay__min_ratings_for_variance']
if self.rating_set.count() >= min_rate:
ratio = radio_settings['replay__rating_variance_ratio']
# -((average - 1)/(highest_rating - 1)) * ratio
base = -((self._average_rating() - 1) / 4) * ratio
adjusted_ratio = float(base + (ratio * 0.5))
else:
adjusted_ratio = float(0.0)
return last + Song.music.wait_total(adjusted_ratio)
return timezone.now()
return None
def _is_playable(self):
'''
Is the song available and not been played within the default waiting
period (or at all)?
'''
if self._is_song() and self._is_available():
return self.next_play is None or self.next_play <= timezone.now()
return False
_is_playable.boolean = True
is_playable = property(_is_playable)
def _is_requestable(self):
'''
Is the song playable and has it not already been requested?
'''
if self._is_playable():
song_request = apps.get_model(app_label='profiles',
model_name='SongRequest')
requests = song_request.music.unplayed().values_list('song__id',
flat=True)
return self.pk not in requests
return False
_is_requestable.boolean = True
is_requestable = property(_is_requestable)
def __str__(self):
return self.title