diff --git a/oliver_screen/api.py b/oliver_screen/api.py index 5d65b02..5d3bf5c 100644 --- a/oliver_screen/api.py +++ b/oliver_screen/api.py @@ -1,55 +1,14 @@ -from django.utils import timezone from rest_framework.response import Response from rest_framework.views import APIView -from django.contrib.humanize.templatetags.humanize import naturaltime -from dateutil import parser -from oliver_screen import lastfm, utils +from oliver_screen.lastfm import get_track_data, get_last_track_data class LastTrackView(APIView): - def _get_playback_date(self, playback_date_string): - playback_date = parser.parse(playback_date_string) - return timezone.make_aware(playback_date, timezone.UTC()) # Last.fm API is in UTC - - def _time_since(self, playback_date_string): - playback_date = self._get_playback_date(playback_date_string) - formatted = naturaltime(playback_date) - formatted = formatted.replace(u'\xa0', u' ') # remove non-breaking space (in latin1) - return formatted - def get(self, request, format=None): - user = lastfm.get_user() - playedtracks = user.get_recent_tracks(limit=2) - - track_data = { - 'artist': playedtracks[0].track.get_artist().get_name(), - 'title': playedtracks[0].track.get_title(), - 'playback_date': self._get_playback_date(playedtracks[0].playback_date), - 'time_since': self._time_since(playedtracks[0].playback_date) - } - return Response({'track': track_data}) + return Response({'track': get_last_track_data()}) class CurrentTrackView(APIView): def get(self, request, format=None): - user = lastfm.get_user() - track = user.get_now_playing() - - if track is None: - # No response from LastFM? - return Response({'track': utils.get_current_from_cache()}) - - artist = track.get_artist() - video = utils.get_youtubevideo(artist.get_name(), track.get_title()) - image = artist.get_cover_image(size=4) - - now_playing = { - 'artist': artist.get_name(), - 'title': track.get_title(), - 'image': image, - 'video': video - } - utils.cache_current(now_playing) - - return Response({'track': now_playing}) + return Response({'track': get_track_data()}) diff --git a/oliver_screen/consumers.py b/oliver_screen/consumers.py index 9cff600..09e21b2 100644 --- a/oliver_screen/consumers.py +++ b/oliver_screen/consumers.py @@ -1,9 +1,7 @@ from channels import Group from channels.sessions import channel_session from django.conf import settings -import json -from oliver_screen.models import ScreenConsumer from oliver_screen.tasks import update_screens_task @@ -17,21 +15,11 @@ def ws_connect(message): g = Group(settings.OLIVER_WS_GROUP, channel_layer=message.channel_layer) g.add(message.reply_channel) - active_consumers = ScreenConsumer.objects.filter(is_active=True) - # At last one - if active_consumers.exists(): - screen_consumer = active_consumers.first() - else: - screen_consumer = ScreenConsumer.objects.create() - update_screens_task() # async - - g.send({'text': json.dumps({'screen_consumer': {'uuid': str(screen_consumer.uuid)}})}) + update_screens_task() # async @channel_session def ws_disconnect(message): Group(settings.OLIVER_WS_GROUP, channel_layer=message.channel_layer).discard(message.reply_channel) - # FIXME: only set specific uuid inactive and only revoke is no more active screens - ScreenConsumer.objects.filter(is_active=True).update(is_active=False) - update_screens_task.revoke() + # update_screens_task.revoke() diff --git a/oliver_screen/lastfm.py b/oliver_screen/lastfm.py index aeece24..4e8a831 100644 --- a/oliver_screen/lastfm.py +++ b/oliver_screen/lastfm.py @@ -1,13 +1,65 @@ import pylast from django.conf import settings +from django.utils import timezone +from django.contrib.humanize.templatetags.humanize import naturaltime +from dateutil import parser + +from oliver_screen import utils from oliver_screen.models import LastFMUser def get_user(): network = pylast.LastFMNetwork(api_key=settings.LASTFM_API_KEY, api_secret=settings.LASTFM_API_SECRET) - lastfmusers = LastFMUser.objects.filter(active=True) - if len(lastfmusers) != 1: - # default - return network.get_user(settings.LASTFM_DEFAULT_USERNAME) - return network.get_user(lastfmusers[0].name) + lastfmusers = LastFMUser.objects.filter(active=True) + lastfm_username = lastfmusers[0].name if len(lastfmusers) == 1 else settings.LASTFM_DEFAULT_USERNAME + + return network.get_user(lastfm_username) + + +def _get_playback_date(playback_date_string): + playback_date = parser.parse(playback_date_string) + + return timezone.make_aware(playback_date, timezone.UTC()) # Last.fm API is in UTC + + +def _time_since(playback_date_string): + playback_date = _get_playback_date(playback_date_string) + formatted = naturaltime(playback_date) + formatted = formatted.replace(u'\xa0', u' ') # remove non-breaking space (in latin1) + + return formatted + + +def get_last_track_data(): + user = get_user() + playedtracks = user.get_recent_tracks(limit=2) + + track_data = { + 'artist': playedtracks[0].track.get_artist().get_name(), + 'title': playedtracks[0].track.get_title(), + 'playback_date': _get_playback_date(playedtracks[0].playback_date), + 'time_since': _time_since(playedtracks[0].playback_date) + } + return track_data + + +def get_track_data(): + user = get_user() + track = user.get_now_playing() + + if track is None: + return utils.get_current_from_cache() + + artist = track.get_artist() + image = artist.get_cover_image(size=4) + + track_data = { + 'artist': artist.get_name(), + 'title': track.get_title(), + 'image': image, + } + + utils.cache_current(track_data) + + return track_data diff --git a/oliver_screen/migrations/0004_delete_currenttrack.py b/oliver_screen/migrations/0004_delete_currenttrack.py new file mode 100644 index 0000000..0c2b03d --- /dev/null +++ b/oliver_screen/migrations/0004_delete_currenttrack.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oliver_screen', '0003_auto_20160403_1343'), + ] + + operations = [ + migrations.DeleteModel( + name='CurrentTrack', + ), + ] diff --git a/oliver_screen/models.py b/oliver_screen/models.py index 43a4828..6482b8d 100644 --- a/oliver_screen/models.py +++ b/oliver_screen/models.py @@ -12,16 +12,6 @@ class ScreenConsumer(models.Model): return str(self.uuid) -class CurrentTrack(models.Model): - title = models.CharField(max_length=200) - artist = models.CharField(max_length=200) - image = models.CharField(max_length=200) - retries = models.IntegerField() - - def __str__(self): - return self.title + " - " + self.artist - - class LastFMUser(models.Model): name = models.CharField(max_length=200) active = models.BooleanField(default=True) diff --git a/oliver_screen/tasks.py b/oliver_screen/tasks.py index 882323e..e6d81ce 100644 --- a/oliver_screen/tasks.py +++ b/oliver_screen/tasks.py @@ -2,45 +2,44 @@ import json import logging from django.conf import settings +from django.core.cache import cache from huey.contrib.djhuey import db_task from channels import Group -from oliver_screen import utils, lastfm -from oliver_screen.models import ScreenConsumer +from oliver_screen import lastfm logger = logging.getLogger(__name__) +LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes + + +def acquire_lock(lock_id): + # cache.add fails if the key already exists + return cache.add(lock_id, 'true', LOCK_EXPIRE) + + +def release_lock(lock_id): + # memcache delete is very slow, but we have to use it to take advantage of using add() for atomic locking + return cache.delete(lock_id) + @db_task() def update_screens_task(): screen_channel_group = Group(settings.OLIVER_WS_GROUP) + lock_id = '{0}-lock'.format(settings.OLIVER_WS_GROUP) + logger.debug('Updating screens') + if acquire_lock(lock_id): + try: + track_data = lastfm.get_track_data() + finally: + release_lock(lock_id) - user = lastfm.get_user() - track = user.get_now_playing() - if track is None: - track_data = utils.get_current_from_cache() - logger.debug("sending ", track_data, "to", screen_channel_group.name) - screen_channel_group.send({'track': json.dumps({'track': track_data})}) - return + screen_channel_group.send({'text': json.dumps({'track': track_data})}) - artist = track.get_artist() - image = artist.get_cover_image(size=4) - - track_data = { - 'artist': artist.get_name(), - 'title': track.get_title(), - 'image': image, - } - utils.cache_current(track_data) - - logger.debug("sending ", track_data, "to", screen_channel_group.name) - screen_channel_group.send({'text': json.dumps({'track': track_data})}) - - # At least one active screen - if ScreenConsumer.objects.filter(is_active=True).exists(): update_screens_task.schedule(delay=settings.OLIVER_LASTFM_POLL_INTERVAL) - else: - logger.debug('No active screens, bailing') - return track_data + return track_data + + logger.debug('Last.FM is already being polled, doing nothing.') + diff --git a/oliver_screen/utils.py b/oliver_screen/utils.py index 3e12a37..8fd014a 100644 --- a/oliver_screen/utils.py +++ b/oliver_screen/utils.py @@ -1,4 +1,5 @@ -from oliver_screen.models import CurrentTrack, YouTubeVideo +from django.core.cache import cache +from oliver_screen.models import YouTubeVideo def get_youtubevideo(artist, title): @@ -9,45 +10,12 @@ def get_youtubevideo(artist, title): return videos[0] +CACHE_KEY = 'oliver-track' -def cache_current(now_playing): - # Fetch the one CurrentTrack entry in the db and update it with the current track. - try: - current = CurrentTrack.objects.get(pk=1) - except CurrentTrack.DoesNotExist: - # update - current = CurrentTrack(artist="", title="", image="", retries=0) - current.artist = now_playing['artist'] - current.title = now_playing['title'] - current.image = now_playing['image'] - current.retries = 0 - current.save() +def cache_current(track): + return cache.set(CACHE_KEY, track, 20) # in seconds def get_current_from_cache(): - try: - current = CurrentTrack.objects.get(pk=1) - except CurrentTrack.DoesNotExist: - current = CurrentTrack(artist="", title="", image="", retries=0) - current.save() - - now_playing = {} - - # try 3 times before clearing the cache - if current.artist != "" and current.retries < 3: - now_playing = { - 'artist': current.artist, - 'title': current.title, - 'image': current.image - } - - current.retries += 1 - current.save() - else: - current.artist = "" - current.title = "" - current.image = "" - current.save() - - return now_playing \ No newline at end of file + return cache.get(CACHE_KEY) diff --git a/oliversite/settings.py b/oliversite/settings.py index d50783e..83fee60 100644 --- a/oliversite/settings.py +++ b/oliversite/settings.py @@ -1,14 +1,12 @@ -from __future__ import unicode_literals # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os - from huey import RedisHuey BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '#r65fsg)wsk&cs8m^i)@##7-wg=#z=*b_)a82#5z#4%&!j_9=6' @@ -102,6 +100,13 @@ OLIVER_LASTFM_POLL_INTERVAL = 10 # seconds HUEY = RedisHuey('oliverscreen', result_store=True) +CACHES = { + 'default': { + 'BACKEND': 'redis_cache.RedisCache', + 'LOCATION': 'localhost:6379', + } +} + # Local settings try: from .local_settings import * diff --git a/requirements.txt b/requirements.txt index f50c5ac..574bcd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ channels==0.10.3 Django>=1.8,<1.9 django-extensions==1.6.1 django-flat-theme==1.1.3 +django-redis-cache==1.6.5 djangorestframework==3.3.3 gevent==1.1.0 gunicorn==19.4.5