Initial changes to Song model using Stores.
This commit is contained in:
parent
7e0b2a5a45
commit
9fa3a408b1
6 changed files with 325 additions and 216 deletions
39
savepointradio/radio/fields.py
Normal file
39
savepointradio/radio/fields.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
'''
|
||||||
|
Custom model fields for the Save Point Radio project.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from django.core import validators
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .forms import ALLOWED_SCHEMES, RadioIRIFormField
|
||||||
|
|
||||||
|
|
||||||
|
class RadioIRIField(models.TextField):
|
||||||
|
'''
|
||||||
|
A custom URL model field that allows schemes that match those from
|
||||||
|
Liquidsoap. This is necessary due to a bug in how Django currently
|
||||||
|
handles custom URLFields:
|
||||||
|
|
||||||
|
https://code.djangoproject.com/ticket/25594
|
||||||
|
https://stackoverflow.com/questions/41756572/
|
||||||
|
'''
|
||||||
|
default_validators = [validators.URLValidator(schemes=ALLOWED_SCHEMES)]
|
||||||
|
description = _("Long IRI")
|
||||||
|
|
||||||
|
def __init__(self, verbose_name=None, name=None, **kwargs):
|
||||||
|
# This is a limit for Internet Explorer URLs
|
||||||
|
kwargs.setdefault('max_length', 2000)
|
||||||
|
super().__init__(verbose_name, name, **kwargs)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
if kwargs.get("max_length") == 2000:
|
||||||
|
del kwargs['max_length']
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
return super().formfield(**{
|
||||||
|
'form_class': RadioIRIFormField,
|
||||||
|
**kwargs,
|
||||||
|
})
|
21
savepointradio/radio/forms.py
Normal file
21
savepointradio/radio/forms.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
'''
|
||||||
|
Custom forms/formfields for the Save Point Radio project.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from django.core import validators
|
||||||
|
from django.forms.fields import URLField
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_SCHEMES = ['http', 'https', 'file', 'ftp', 'ftps', 's3']
|
||||||
|
|
||||||
|
|
||||||
|
class RadioIRIFormField(URLField):
|
||||||
|
'''
|
||||||
|
A custom URL form field that allows schemes that match those from
|
||||||
|
Liquidsoap. This is necessary due to a bug in how Django currently
|
||||||
|
handles custom URLFields:
|
||||||
|
|
||||||
|
https://code.djangoproject.com/ticket/25594
|
||||||
|
https://stackoverflow.com/questions/41756572/
|
||||||
|
'''
|
||||||
|
default_validators = [validators.URLValidator(schemes=ALLOWED_SCHEMES)]
|
|
@ -1,160 +1,129 @@
|
||||||
from decimal import *
|
'''
|
||||||
|
Django management command to import old playlist data. This should only be used
|
||||||
|
for seeding a newly created database.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import decimal
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
from radio.models import Album, Artist, Game, Song
|
from core.utils import path_to_iri
|
||||||
|
from radio.models import Album, Artist, Game, Store, Song
|
||||||
|
|
||||||
getcontext().prec = 8
|
decimal.getcontext().prec = 8
|
||||||
|
|
||||||
|
|
||||||
def adapt_decimal(d):
|
|
||||||
return str(d)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_decimal(s):
|
|
||||||
return Decimal(s.decode('utf8'))
|
|
||||||
|
|
||||||
|
|
||||||
sqlite3.register_adapter(Decimal, adapt_decimal)
|
|
||||||
sqlite3.register_converter("decimal", convert_decimal)
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Imports the old radio data from the original sqlite3 database'
|
'''Main "importoldreadio" command class'''
|
||||||
|
help = 'Imports the old radio data from an exported playlist'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('sqlite3_db_file', nargs=1)
|
parser.add_argument('playlist_file', nargs=1)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
if not os.path.isfile(options['sqlite3_db_file'][0]):
|
playlist_file = options['playlist_file'][0]
|
||||||
|
if not os.path.isfile(playlist_file):
|
||||||
raise CommandError('File does not exist')
|
raise CommandError('File does not exist')
|
||||||
else:
|
|
||||||
total_albums = 0
|
|
||||||
total_artists = 0
|
|
||||||
total_games = 0
|
|
||||||
total_songs = 0
|
|
||||||
total_jingles = 0
|
|
||||||
|
|
||||||
detect_types = sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
|
with open(playlist_file, 'r', encoding='utf8') as pfile:
|
||||||
con = sqlite3.connect(options['sqlite3_db_file'][0],
|
playlist = json.load(pfile, parse_float=decimal.Decimal)
|
||||||
detect_types=detect_types)
|
|
||||||
cur = con.cursor()
|
|
||||||
|
|
||||||
# Fetching albums first
|
totals = {
|
||||||
for album in con.execute('SELECT title, enabled FROM albums'):
|
'albums': 0,
|
||||||
album_disabled = not bool(album[1])
|
'artists': 0,
|
||||||
Album.objects.create(title=album[0], disabled=album_disabled)
|
'games': 0,
|
||||||
total_albums += 1
|
'songs': 0,
|
||||||
|
'jingles': 0
|
||||||
|
}
|
||||||
|
|
||||||
self.stdout.write('Imported {} albums'.format(str(total_albums)))
|
# Fetching albums first
|
||||||
|
for album in playlist['albums']:
|
||||||
|
Album.objects.create(title=album['title'],
|
||||||
|
disabled=album['disabled'])
|
||||||
|
total_albums += 1
|
||||||
|
|
||||||
# Next up, artists
|
self.stdout.write('Imported {} albums'.format(str(totals['albums'])))
|
||||||
cur.execute('''SELECT
|
|
||||||
artists_id,
|
|
||||||
alias,
|
|
||||||
firstname,
|
|
||||||
lastname,
|
|
||||||
enabled
|
|
||||||
FROM artists''')
|
|
||||||
artists = cur.fetchall()
|
|
||||||
|
|
||||||
for artist in artists:
|
# Next up, artists
|
||||||
artist_disabled = not bool(artist[4])
|
for artist in playlist['artists']:
|
||||||
Artist.objects.create(alias=artist[1] or '',
|
Artist.objects.create(alias=artist['alias'] or '',
|
||||||
first_name=artist[2] or '',
|
first_name=artist['first_name'] or '',
|
||||||
last_name=artist[3] or '',
|
last_name=artist['last_name'] or '',
|
||||||
disabled=artist_disabled)
|
disabled=artist['disabled'])
|
||||||
total_artists += 1
|
total_artists += 1
|
||||||
|
|
||||||
self.stdout.write('Imported {} artists'.format(str(total_artists)))
|
self.stdout.write('Imported {} artists'.format(str(totals['artists'])))
|
||||||
|
|
||||||
# On to games
|
# On to games
|
||||||
for game in con.execute('SELECT title, enabled FROM games'):
|
for game in playlist['games']:
|
||||||
game_disabled = not bool(game[1])
|
Game.objects.create(title=game['title'],
|
||||||
Game.objects.create(title=game[0], disabled=game_disabled)
|
disabled=game['disabled'])
|
||||||
total_games += 1
|
total_games += 1
|
||||||
|
|
||||||
self.stdout.write('Imported {} games'.format(str(total_games)))
|
self.stdout.write('Imported {} games'.format(str(totals['games'])))
|
||||||
|
|
||||||
# Followed by the songs
|
# Followed by the songs
|
||||||
cur.execute('''SELECT
|
for song in playlist['songs']:
|
||||||
songs.songs_id AS id,
|
try:
|
||||||
games.title AS game,
|
album = Album.objects.get(title__exact=song['album'])
|
||||||
albums.title AS album,
|
except Album.DoesNotExist:
|
||||||
songs.enabled AS enabled,
|
album = None
|
||||||
songs.type AS type,
|
|
||||||
songs.title AS title,
|
|
||||||
songs.length AS length,
|
|
||||||
songs.path AS path
|
|
||||||
FROM songs
|
|
||||||
LEFT JOIN games
|
|
||||||
ON (songs.game = games.games_id)
|
|
||||||
LEFT JOIN albums
|
|
||||||
ON (songs.album = albums.albums_id)''')
|
|
||||||
songs = cur.fetchall()
|
|
||||||
|
|
||||||
for song in songs:
|
try:
|
||||||
try:
|
game = Game.objects.get(title__exact=song['game'])
|
||||||
album = Album.objects.get(title__exact=song[2])
|
except Game.DoesNotExist:
|
||||||
except Album.DoesNotExist:
|
game = None
|
||||||
album = None
|
|
||||||
|
|
||||||
try:
|
new_song = Song.objects.create(album=album,
|
||||||
game = Game.objects.get(title__exact=song[1])
|
game=game,
|
||||||
except Game.DoesNotExist:
|
disabled=song['disabled'],
|
||||||
game = None
|
song_type=song['type'],
|
||||||
|
title=song['title'])
|
||||||
|
|
||||||
song_disabled = not bool(song[3])
|
for artist in song['artists']:
|
||||||
new_song = Song.objects.create(album=album,
|
new_artist = Artist.objects.get(
|
||||||
game=game,
|
alias__exact=artist['alias'] or '',
|
||||||
disabled=song_disabled,
|
first_name__exact=artist['first_name'] or '',
|
||||||
song_type=song[4],
|
last_name__exact=artist['last_name'] or ''
|
||||||
title=song[5],
|
|
||||||
length=song[6],
|
|
||||||
path=song[7])
|
|
||||||
if song[4] == 'S':
|
|
||||||
total_songs += 1
|
|
||||||
else:
|
|
||||||
total_jingles += 1
|
|
||||||
|
|
||||||
cur.execute('''SELECT
|
|
||||||
ifnull(alias, "") AS alias,
|
|
||||||
ifnull(firstname, "") AS firstname,
|
|
||||||
ifnull(lastname, "") AS lastname
|
|
||||||
FROM artists
|
|
||||||
WHERE artists_id
|
|
||||||
IN (SELECT artists_artists_id
|
|
||||||
FROM songs_have_artists
|
|
||||||
WHERE songs_songs_id = ?)''', [song[0]])
|
|
||||||
old_artists = cur.fetchall()
|
|
||||||
for old_artist in old_artists:
|
|
||||||
new_artist = Artist.objects.get(alias__exact=old_artist[0],
|
|
||||||
first_name__exact=old_artist[1],
|
|
||||||
last_name__exact=old_artist[2])
|
|
||||||
new_song.artists.add(new_artist)
|
|
||||||
|
|
||||||
self.stdout.write(
|
|
||||||
'Imported {} requestables ({} songs, {} jingles)'.format(
|
|
||||||
str(total_songs + total_jingles),
|
|
||||||
str(total_songs),
|
|
||||||
str(total_jingles)
|
|
||||||
)
|
)
|
||||||
|
new_song.artists.add(new_artist)
|
||||||
|
|
||||||
|
new_store = Store.objects.create(
|
||||||
|
iri=path_to_iri(song['store']['path']),
|
||||||
|
mime_type=song['store']['mime'],
|
||||||
|
file_size=song['store']['filesize'],
|
||||||
|
length=song['store']['length']
|
||||||
)
|
)
|
||||||
|
new_song.stores.add(new_store)
|
||||||
pub = input('Do you want to publish all imported objects as well? '
|
new_song.current_store = new_store
|
||||||
'[Y/N] ')
|
new_song.save()
|
||||||
|
if song['type'] == 'S':
|
||||||
if pub == 'Y' or pub == 'y':
|
totals['songs'] += 1
|
||||||
for al in Album.objects.all():
|
|
||||||
al.publish()
|
|
||||||
for ar in Artist.objects.all():
|
|
||||||
ar.publish()
|
|
||||||
for g in Game.objects.all():
|
|
||||||
g.publish()
|
|
||||||
for s in Song.objects.all():
|
|
||||||
s.publish()
|
|
||||||
self.stdout.write('Published imported objects successfully')
|
|
||||||
else:
|
else:
|
||||||
self.stdout.write('Skipped publishing songs')
|
totals['jingles'] += 1
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
'Imported {} requestables ({} songs, {} jingles)'.format(
|
||||||
|
str(totals['songs'] + totals['jingles']),
|
||||||
|
str(totals['songs']),
|
||||||
|
str(totals['jingles'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
pub = input('Do you want to publish all imported objects as well? '
|
||||||
|
'[Y/N] ')
|
||||||
|
|
||||||
|
if pub in ('Y', 'y'):
|
||||||
|
for album in Album.objects.all():
|
||||||
|
album.publish()
|
||||||
|
for artist in Artist.objects.all():
|
||||||
|
artist.publish()
|
||||||
|
for game in Game.objects.all():
|
||||||
|
game.publish()
|
||||||
|
for song in Song.objects.all():
|
||||||
|
song.publish()
|
||||||
|
self.stdout.write('Published imported objects successfully')
|
||||||
|
else:
|
||||||
|
self.stdout.write('Skipped publishing songs')
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
'''
|
||||||
|
Django Model Managers for the Radio application.
|
||||||
|
'''
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import getcontext, Decimal, ROUND_UP
|
from decimal import getcontext, Decimal, ROUND_UP
|
||||||
from random import randint
|
from random import randint
|
||||||
|
@ -15,123 +19,126 @@ getcontext().prec = 16
|
||||||
|
|
||||||
|
|
||||||
class RadioManager(models.Manager):
|
class RadioManager(models.Manager):
|
||||||
"""
|
'''
|
||||||
Custom object manager for filtering out common behaviors for radio
|
Custom object manager for filtering out common behaviors for radio
|
||||||
objects.
|
objects.
|
||||||
"""
|
'''
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
'''
|
||||||
Return customized default QuerySet.
|
Return customized default QuerySet.
|
||||||
"""
|
'''
|
||||||
return RadioQuerySet(self.model, using=self._db)
|
return RadioQuerySet(self.model, using=self._db)
|
||||||
|
|
||||||
def disabled(self):
|
def disabled(self):
|
||||||
"""
|
'''
|
||||||
Radio objects that are marked as disabled.
|
Radio objects that are marked as disabled.
|
||||||
"""
|
'''
|
||||||
return self.get_queryset().disabled()
|
return self.get_queryset().disabled()
|
||||||
|
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
"""
|
'''
|
||||||
Radio objects that are marked as enabled.
|
Radio objects that are marked as enabled.
|
||||||
"""
|
'''
|
||||||
return self.get_queryset().enabled()
|
return self.get_queryset().enabled()
|
||||||
|
|
||||||
def published(self):
|
def published(self):
|
||||||
"""
|
'''
|
||||||
Radio objects that are marked as published.
|
Radio objects that are marked as published.
|
||||||
"""
|
'''
|
||||||
return self.get_queryset().published()
|
return self.get_queryset().published()
|
||||||
|
|
||||||
def unpublished(self):
|
def unpublished(self):
|
||||||
"""
|
'''
|
||||||
Radio objects that are marked as unpublished.
|
Radio objects that are marked as unpublished.
|
||||||
"""
|
'''
|
||||||
return self.get_queryset().unpublished()
|
return self.get_queryset().unpublished()
|
||||||
|
|
||||||
def available(self):
|
def available(self):
|
||||||
"""
|
'''
|
||||||
Radio objects that are enabled and published.
|
Radio objects that are enabled and published.
|
||||||
"""
|
'''
|
||||||
return self.enabled().published()
|
return self.enabled().published()
|
||||||
|
|
||||||
|
|
||||||
class SongManager(RadioManager):
|
class SongManager(RadioManager):
|
||||||
"""
|
'''
|
||||||
Custom object manager for filtering out common behaviors for Song objects.
|
Custom object manager for filtering out common behaviors for Song objects.
|
||||||
"""
|
'''
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
'''
|
||||||
Return customized default QuerySet for Songs.
|
Return customized default QuerySet for Songs.
|
||||||
"""
|
'''
|
||||||
return SongQuerySet(self.model, using=self._db)
|
return SongQuerySet(self.model, using=self._db)
|
||||||
|
|
||||||
def available_jingles(self):
|
def available_jingles(self):
|
||||||
"""
|
'''
|
||||||
Jingles that are currently published and are enabled.
|
Jingles that are currently published and are enabled.
|
||||||
"""
|
'''
|
||||||
return self.available().jingles()
|
return self.available().jingles()
|
||||||
|
|
||||||
def available_songs(self):
|
def available_songs(self):
|
||||||
"""
|
'''
|
||||||
Songs that are currently published and are enabled.
|
Songs that are currently published and are enabled.
|
||||||
"""
|
'''
|
||||||
return self.available().songs()
|
return self.available().songs()
|
||||||
|
|
||||||
def playlist_length(self):
|
def playlist_length(self):
|
||||||
"""
|
'''
|
||||||
Total length of available songs in the playlist (in seconds).
|
Total length of available songs in the playlist (in seconds).
|
||||||
"""
|
'''
|
||||||
length = self.available_songs().aggregate(models.Sum('length'))
|
a_songs = self.available_songs()
|
||||||
return length['length__sum']
|
length = a_songs.aggregate(
|
||||||
|
total_time=models.Sum('current_store__length')
|
||||||
|
)
|
||||||
|
return length['total_time']
|
||||||
|
|
||||||
def wait_total(self, adjusted_ratio=0.0):
|
def wait_total(self, adjusted_ratio=0.0):
|
||||||
"""
|
'''
|
||||||
Default length in seconds before a song can be played again. This is
|
Default length in seconds before a song can be played again. This is
|
||||||
based on the replay ratio set in the application settings.
|
based on the replay ratio set in the application settings.
|
||||||
"""
|
'''
|
||||||
total_ratio = get_setting('replay_ratio') + adjusted_ratio
|
total_ratio = get_setting('replay_ratio') + adjusted_ratio
|
||||||
wait = self.playlist_length() * Decimal(total_ratio)
|
wait = self.playlist_length() * Decimal(total_ratio)
|
||||||
wait = wait.quantize(Decimal('.01'), rounding=ROUND_UP)
|
wait = wait.quantize(Decimal('.01'), rounding=ROUND_UP)
|
||||||
return timedelta(seconds=float(wait))
|
return timedelta(seconds=float(wait))
|
||||||
|
|
||||||
def datetime_from_wait(self):
|
def datetime_from_wait(self):
|
||||||
"""
|
'''
|
||||||
Datetime of now minus the default wait time for played songs.
|
Datetime of now minus the default wait time for played songs.
|
||||||
"""
|
'''
|
||||||
return timezone.now() - self.wait_total()
|
return timezone.now() - self.wait_total()
|
||||||
|
|
||||||
def playable(self):
|
def playable(self):
|
||||||
"""
|
'''
|
||||||
Songs that are playable because they are available (enabled &
|
Songs that are playable because they are available (enabled &
|
||||||
published) and they have not been played within the default wait time
|
published) and they have not been played within the default wait time
|
||||||
(or at all).
|
(or at all).
|
||||||
"""
|
'''
|
||||||
return self.available_songs().filter(
|
return self.available_songs().filter(
|
||||||
models.Q(next_play__lt=timezone.now()) |
|
models.Q(next_play__lt=timezone.now()) |
|
||||||
models.Q(next_play__isnull=True)
|
models.Q(next_play__isnull=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
def requestable(self):
|
def requestable(self):
|
||||||
"""
|
'''
|
||||||
Songs that can be placed in the request queue for playback.
|
Songs that can be placed in the request queue for playback.
|
||||||
"""
|
'''
|
||||||
# Import SongRequest here to get rid of circular dependencies
|
# Import SongRequest here to get rid of circular dependencies
|
||||||
SongRequest = apps.get_model(app_label='profiles',
|
song_request = apps.get_model(app_label='profiles',
|
||||||
model_name='SongRequest')
|
model_name='SongRequest')
|
||||||
requests = SongRequest.music.unplayed().values_list('song__id',
|
requests = song_request.music.unplayed().values_list('song__id',
|
||||||
flat=True)
|
flat=True)
|
||||||
return self.playable().exclude(id__in=requests)
|
return self.playable().exclude(id__in=requests)
|
||||||
|
|
||||||
def get_random_requestable_song(self):
|
def get_random_requestable_song(self):
|
||||||
"""
|
'''
|
||||||
Pick a random requestable song and return it.
|
Pick a random requestable song and return it.
|
||||||
"""
|
'''
|
||||||
return self.requestable()[randint(0, self.requestable().count() - 1)]
|
return self.requestable()[randint(0, self.requestable().count() - 1)]
|
||||||
|
|
||||||
def get_random_jingle(self):
|
def get_random_jingle(self):
|
||||||
"""
|
'''
|
||||||
Pick a random jingle and return it.
|
Pick a random jingle and return it.
|
||||||
"""
|
'''
|
||||||
random_index = randint(0, self.available_jingles().count() - 1)
|
random_index = randint(0, self.available_jingles().count() - 1)
|
||||||
return self.available_jingles()[random_index]
|
return self.available_jingles()[random_index]
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 2.2.1 on 2019-05-31 03:00
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import radio.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('radio', '0003_song_next_play'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Store',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_date', models.DateTimeField(auto_now_add=True, verbose_name='added on')),
|
||||||
|
('modified_date', models.DateTimeField(auto_now=True, verbose_name='last modified')),
|
||||||
|
('iri', radio.fields.RadioIRIField()),
|
||||||
|
('mime_type', models.CharField(blank=True, max_length=64, verbose_name='file MIME type')),
|
||||||
|
('file_size', models.BigIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='file size')),
|
||||||
|
('length', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='song length (in seconds)')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='song',
|
||||||
|
name='length',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='song',
|
||||||
|
name='path',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='song',
|
||||||
|
name='current_store',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='current_of', to='radio.Store'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='song',
|
||||||
|
name='stores',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='song', to='radio.Store'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,13 +1,19 @@
|
||||||
|
'''
|
||||||
|
Django Models for the Radio application.
|
||||||
|
'''
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import getcontext, Decimal, ROUND_UP
|
from decimal import getcontext, Decimal, ROUND_UP
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from core.behaviors import Disableable, Publishable, Timestampable
|
from core.behaviors import Disableable, Publishable, Timestampable
|
||||||
from core.utils import get_setting
|
from core.utils import get_setting
|
||||||
|
from .fields import RadioIRIField
|
||||||
from .managers import RadioManager, SongManager
|
from .managers import RadioManager, SongManager
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,9 +22,9 @@ getcontext().prec = 16
|
||||||
|
|
||||||
|
|
||||||
class Album(Disableable, Publishable, Timestampable, models.Model):
|
class Album(Disableable, Publishable, Timestampable, models.Model):
|
||||||
"""
|
'''
|
||||||
A model for a music album.
|
A model for a music album.
|
||||||
"""
|
'''
|
||||||
title = models.CharField(_('title'), max_length=255, unique=True)
|
title = models.CharField(_('title'), max_length=255, unique=True)
|
||||||
|
|
||||||
sorted_title = models.CharField(_('naturalized title'),
|
sorted_title = models.CharField(_('naturalized title'),
|
||||||
|
@ -37,9 +43,9 @@ class Album(Disableable, Publishable, Timestampable, models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Artist(Disableable, Publishable, Timestampable, models.Model):
|
class Artist(Disableable, Publishable, Timestampable, models.Model):
|
||||||
"""
|
'''
|
||||||
A model for a music artist.
|
A model for a music artist.
|
||||||
"""
|
'''
|
||||||
alias = models.CharField(_('alias'), max_length=127, blank=True)
|
alias = models.CharField(_('alias'), max_length=127, blank=True)
|
||||||
first_name = models.CharField(_('first name'), 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)
|
last_name = models.CharField(_('last name'), max_length=127, blank=True)
|
||||||
|
@ -57,10 +63,10 @@ class Artist(Disableable, Publishable, Timestampable, models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
"""
|
'''
|
||||||
String representing the artist's full name including an alias, if
|
String representing the artist's full name including an alias, if
|
||||||
available.
|
available.
|
||||||
"""
|
'''
|
||||||
if self.alias:
|
if self.alias:
|
||||||
if self.first_name or self.last_name:
|
if self.first_name or self.last_name:
|
||||||
return '{} "{}" {}'.format(self.first_name,
|
return '{} "{}" {}'.format(self.first_name,
|
||||||
|
@ -74,9 +80,9 @@ class Artist(Disableable, Publishable, Timestampable, models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Game(Disableable, Publishable, Timestampable, models.Model):
|
class Game(Disableable, Publishable, Timestampable, models.Model):
|
||||||
"""
|
'''
|
||||||
A model for a game.
|
A model for a game.
|
||||||
"""
|
'''
|
||||||
title = models.CharField(_('title'), max_length=255, unique=True)
|
title = models.CharField(_('title'), max_length=255, unique=True)
|
||||||
|
|
||||||
sorted_title = models.CharField(_('naturalized title'),
|
sorted_title = models.CharField(_('naturalized title'),
|
||||||
|
@ -94,10 +100,29 @@ class Game(Disableable, Publishable, Timestampable, models.Model):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
class Store(Timestampable, models.Model):
|
||||||
|
'''
|
||||||
|
A model to represent various data locations (stores) for the song.
|
||||||
|
'''
|
||||||
|
iri = RadioIRIField()
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class Song(Disableable, Publishable, Timestampable, models.Model):
|
class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
"""
|
'''
|
||||||
A model for a song.
|
A model for a song.
|
||||||
"""
|
'''
|
||||||
JINGLE = 'J'
|
JINGLE = 'J'
|
||||||
SONG = 'S'
|
SONG = 'S'
|
||||||
TYPE_CHOICES = (
|
TYPE_CHOICES = (
|
||||||
|
@ -128,13 +153,12 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
editable=False)
|
editable=False)
|
||||||
length = models.DecimalField(_('song length (in seconds)'),
|
stores = models.ManyToManyField(Store, blank=True, related_name='song')
|
||||||
max_digits=8,
|
current_store = models.ForeignKey(Store,
|
||||||
decimal_places=2,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True)
|
blank=True,
|
||||||
path = models.TextField(_('absolute path to song file'))
|
related_name='current_of')
|
||||||
|
|
||||||
sorted_title = models.CharField(_('naturalized title'),
|
sorted_title = models.CharField(_('naturalized title'),
|
||||||
db_index=True,
|
db_index=True,
|
||||||
editable=False,
|
editable=False,
|
||||||
|
@ -147,34 +171,34 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
ordering = ['sorted_title', ]
|
ordering = ['sorted_title', ]
|
||||||
|
|
||||||
def _is_jingle(self):
|
def _is_jingle(self):
|
||||||
"""
|
'''
|
||||||
Is the object a jingle?
|
Is the object a jingle?
|
||||||
"""
|
'''
|
||||||
return self.song_type == 'J'
|
return self.song_type == 'J'
|
||||||
_is_jingle.boolean = True
|
_is_jingle.boolean = True
|
||||||
is_jingle = property(_is_jingle)
|
is_jingle = property(_is_jingle)
|
||||||
|
|
||||||
def _is_song(self):
|
def _is_song(self):
|
||||||
"""
|
'''
|
||||||
Is the object a song?
|
Is the object a song?
|
||||||
"""
|
'''
|
||||||
return self.song_type == 'S'
|
return self.song_type == 'S'
|
||||||
_is_song.boolean = True
|
_is_song.boolean = True
|
||||||
is_song = property(_is_song)
|
is_song = property(_is_song)
|
||||||
|
|
||||||
def _is_available(self):
|
def _is_available(self):
|
||||||
"""
|
'''
|
||||||
Is the object both enabled and published?
|
Is the object both enabled and published?
|
||||||
"""
|
'''
|
||||||
return self._is_enabled() and self._is_published()
|
return self._is_enabled() and self._is_published()
|
||||||
_is_available.boolean = True
|
_is_available.boolean = True
|
||||||
is_available = property(_is_available)
|
is_available = property(_is_available)
|
||||||
|
|
||||||
def _full_title(self):
|
def _full_title(self):
|
||||||
"""
|
'''
|
||||||
String representing the entire song title, including the game and
|
String representing the entire song title, including the game and
|
||||||
artists involved.
|
artists involved.
|
||||||
"""
|
'''
|
||||||
if self._is_song():
|
if self._is_song():
|
||||||
enabled_artists = self.artists.all().filter(disabled=False)
|
enabled_artists = self.artists.all().filter(disabled=False)
|
||||||
all_artists = ', '.join([a.full_name for a in enabled_artists])
|
all_artists = ', '.join([a.full_name for a in enabled_artists])
|
||||||
|
@ -185,9 +209,9 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
full_title = property(_full_title)
|
full_title = property(_full_title)
|
||||||
|
|
||||||
def _average_rating(self):
|
def _average_rating(self):
|
||||||
"""
|
'''
|
||||||
Decimal number of the average rating of a song from 1 - 5.
|
Decimal number of the average rating of a song from 1 - 5.
|
||||||
"""
|
'''
|
||||||
ratings = self.rating_set.all()
|
ratings = self.rating_set.all()
|
||||||
if ratings:
|
if ratings:
|
||||||
avg = Decimal(ratings.aggregate(avg=models.Avg('value'))['avg'])
|
avg = Decimal(ratings.aggregate(avg=models.Avg('value'))['avg'])
|
||||||
|
@ -196,9 +220,9 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
average_rating = property(_average_rating)
|
average_rating = property(_average_rating)
|
||||||
|
|
||||||
def get_time_until_requestable(self):
|
def get_time_until_requestable(self):
|
||||||
"""
|
'''
|
||||||
Length of time before a song can be requested again.
|
Length of time before a song can be requested again.
|
||||||
"""
|
'''
|
||||||
if self._is_song() and self._is_available():
|
if self._is_song() and self._is_available():
|
||||||
if self.last_played:
|
if self.last_played:
|
||||||
allowed_datetime = Song.music.datetime_from_wait()
|
allowed_datetime = Song.music.datetime_from_wait()
|
||||||
|
@ -209,9 +233,9 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_date_when_requestable(self, last_play=None):
|
def get_date_when_requestable(self, last_play=None):
|
||||||
"""
|
'''
|
||||||
Datetime when a song can be requested again.
|
Datetime when a song can be requested again.
|
||||||
"""
|
'''
|
||||||
last = self.last_played if last_play is None else last_play
|
last = self.last_played if last_play is None else last_play
|
||||||
|
|
||||||
if self._is_song() and self._is_available():
|
if self._is_song() and self._is_available():
|
||||||
|
@ -232,10 +256,10 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_playable(self):
|
def _is_playable(self):
|
||||||
"""
|
'''
|
||||||
Is the song available and not been played within the default waiting
|
Is the song available and not been played within the default waiting
|
||||||
period (or at all)?
|
period (or at all)?
|
||||||
"""
|
'''
|
||||||
if self._is_song() and self._is_available():
|
if self._is_song() and self._is_available():
|
||||||
return self.get_date_when_requestable() <= timezone.now()
|
return self.get_date_when_requestable() <= timezone.now()
|
||||||
return False
|
return False
|
||||||
|
@ -243,14 +267,14 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
is_playable = property(_is_playable)
|
is_playable = property(_is_playable)
|
||||||
|
|
||||||
def _is_requestable(self):
|
def _is_requestable(self):
|
||||||
"""
|
'''
|
||||||
Is the song playable and has it not already been requested?
|
Is the song playable and has it not already been requested?
|
||||||
"""
|
'''
|
||||||
if self._is_playable():
|
if self._is_playable():
|
||||||
SongRequest = apps.get_model(app_label='profiles',
|
song_request = apps.get_model(app_label='profiles',
|
||||||
model_name='SongRequest')
|
model_name='SongRequest')
|
||||||
requests = SongRequest.music.unplayed().values_list('song__id',
|
requests = song_request.music.unplayed().values_list('song__id',
|
||||||
flat=True)
|
flat=True)
|
||||||
return self.pk not in requests
|
return self.pk not in requests
|
||||||
return False
|
return False
|
||||||
_is_requestable.boolean = True
|
_is_requestable.boolean = True
|
||||||
|
|
Loading…
Reference in a new issue