diff --git a/api/serializers.py b/api/serializers.py index 1477bb2..dd51523 100755 --- a/api/serializers.py +++ b/api/serializers.py @@ -4,7 +4,7 @@ from core.utils.html import strip_tags from dss import settings from spa import models -from spa.models import Activity, Message +from spa.models import Activity, Message, Playlist from spa.models.activity import ActivityDownload, ActivityPlay from spa.models.blog import Blog from spa.models.genre import Genre @@ -340,10 +340,10 @@ class UserProfileSerializer(serializers.ModelSerializer): def get_top_tags(self, obj): return list( - Genre.objects.filter(mix__user__slug='fergalmoran'). - annotate(total=Count('mix')). - order_by('-total'). - values('total', 'description', 'slug')[0:3]) + Genre.objects.filter(mix__user__slug='fergalmoran'). + annotate(total=Count('mix')). + order_by('-total'). + values('total', 'description', 'slug')[0:3]) def get_profile_image_small(self, obj): return obj.get_sized_avatar_image(64, 64) @@ -519,3 +519,8 @@ class BlogSerializer(serializers.ModelSerializer): class Meta: model = Blog + + +class PlaylistSerializer(serializers.ModelSerializer): + class Meta: + model = Playlist diff --git a/api/urls.py b/api/urls.py index 8d6c68b..cb0bbb6 100755 --- a/api/urls.py +++ b/api/urls.py @@ -24,6 +24,7 @@ router.register(r'genre', views.GenreViewSet, base_name='genre') router.register(r'messages', views.MessageViewSet, base_name='messages') router.register(r'shows', views.ShowViewSet, base_name='shows') router.register(r'blog', views.BlogViewSet, base_name='shows') +router.register(r'playlist', views.PlaylistViewSet, base_name='playlists') class DebugView(APIView): diff --git a/api/views.py b/api/views.py index fcd8dd5..a1ff69c 100755 --- a/api/views.py +++ b/api/views.py @@ -17,7 +17,7 @@ from rest_framework.status import HTTP_202_ACCEPTED, HTTP_401_UNAUTHORIZED, HTTP from api import serializers from dss import settings from spa import tasks -from spa.models import Message +from spa.models import Message, Playlist from spa.models.blog import Blog from spa.models.genre import Genre from spa.models.activity import ActivityPlay @@ -56,8 +56,8 @@ class CommentViewSet(viewsets.ModelViewSet): mix = Mix.objects.get(pk=self.request.data['mix_id']) if mix is not None: serializer.save( - mix=mix, - user=self.request.user if self.request.user.is_authenticated() else None + mix=mix, + user=self.request.user if self.request.user.is_authenticated() else None ) except Mix.DoesNotExist: pass @@ -107,7 +107,7 @@ class MixViewSet(viewsets.ModelViewSet): 'id', 'play_count' ) - + @detail_route() def stream_url(self, request, **kwargs): mix = self.get_object() @@ -169,9 +169,9 @@ class SearchResultsView(views.APIView): 'url': user.get_absolute_url(), 'description': user.description } for user in UserProfile.objects.filter( - Q(user__first_name__icontains=q) | - Q(user__last_name__icontains=q) | - Q(display_name__icontains=q)).exclude(slug__isnull=True).exclude(slug__exact='')[0:10] + Q(user__first_name__icontains=q) | + Q(user__last_name__icontains=q) | + Q(display_name__icontains=q)).exclude(slug__isnull=True).exclude(slug__exact='')[0:10] ] else: r_s = [ @@ -367,3 +367,15 @@ class BlogViewSet(viewsets.ModelViewSet): def perform_create(self, serializer): serializer.save(user=self.request.user.userprofile) + + +class PlaylistViewSet(viewsets.ModelViewSet): + queryset = Playlist.objects.all() + serializer_class = serializers.PlaylistSerializer + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + if self.request.user.is_authenticated: + return self.queryset.get(user=self.request.user.userprofile) + + return Response(status=HTTP_401_UNAUTHORIZED) diff --git a/spa/management/commands/mix_mp3_tags.py b/spa/management/commands/mix_mp3_tags.py new file mode 100644 index 0000000..3b3c65e --- /dev/null +++ b/spa/management/commands/mix_mp3_tags.py @@ -0,0 +1,16 @@ +from django.core.management.base import NoArgsCommand + +from core.utils.audio.mp3 import mp3_length +from spa.models import Mix + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + mix = Mix.objects.get(duration=None) + if mix is not None: + mix.waveform_generated = True + mix.duration = mp3_length(path) + mix.save(update_fields=["waveform_generated", "duration"]) + except Exception as ex: + print("Debug exception: %s" % ex) \ No newline at end of file diff --git a/spa/tasks.py b/spa/tasks.py index 65bd081..d98688a 100755 --- a/spa/tasks.py +++ b/spa/tasks.py @@ -19,6 +19,11 @@ from dss import settings logger = logging.getLogger('dss') +@task(time_limit=3600) +def send_db_signals(uid): + waveform_generated_signal.send(sender=None, uid=uid) + + @task(time_limit=3600) def create_waveform_task(in_file, uid): out_file = os.path.join(settings.CACHE_ROOT, 'waveforms/%s.png' % uid) diff --git a/static/js/embedding/jplayer.cleanskin.js b/static/js/embedding/jplayer.cleanskin.js index c366ce7..b3d1eea 100644 --- a/static/js/embedding/jplayer.cleanskin.js +++ b/static/js/embedding/jplayer.cleanskin.js @@ -1,21 +1,257 @@ -(function ($) { $.fn.slider=function(options,flag){var EVENT=window.navigator.msPointerEnabled?2:"ontouchend" in document?3:1;if(window.debug&&console){console.log(EVENT)}function call(f,scope,args){if(typeof f==="function"){f.call(scope,args)}}var percentage={to:function(range,value){value=range[0]<0?value+Math.abs(range[0]):value-range[0];return(value*100)/this._length(range)},from:function(range,value){return(value*100)/this._length(range)},is:function(range,value){return((value*this._length(range))/100)+range[0]},_length:function(range){return(range[0]>range[1]?range[0]-range[1]:range[1]-range[0])}};function correct(proposal,slider,handle){var setup=slider.data("setup"),handles=setup.handles,settings=setup.settings,pos=setup.pos;proposal=proposal<0?0:proposal>100?100:proposal;if(settings.handles==2){if(handle.is(":first-child")){var other=parseFloat(handles[1][0].style[pos])-settings.margin;proposal=proposal>other?other:proposal}else{var other=parseFloat(handles[0][0].style[pos])+settings.margin;proposal=proposal
",slider=$(this).data("_isnS_",true),handles=[],pos,orientation,classes="",num=function(e){return !isNaN(parseFloat(e))&&isFinite(e)},split=(settings.serialization.resolution=settings.serialization.resolution||0.01).toString().split("."),res=split[0]==1?0:split[1].length;settings.start=num(settings.start)?[settings.start,0]:settings.start;$.each(settings,function(a,b){if(num(b)){settings[a]=parseFloat(b)}else{if(typeof b=="object"&&num(b[0])){b[0]=parseFloat(b[0]);if(num(b[1])){b[1]=parseFloat(b[1])}}}var e=false;b=typeof b=="undefined"?"x":b;switch(a){case"range":case"start":e=b.length!=2||!num(b[0])||!num(b[1]);break;case"handles":e=(b<1||b>2||!num(b));break;case"connect":e=b!="lower"&&b!="upper"&&typeof b!="boolean";break;case"orientation":e=(b!="vertical"&&b!="horizontal");break;case"margin":case"step":e=typeof b!="undefined"&&!num(b);break;case"serialization":e=typeof b!="object"||!num(b.resolution)||(typeof b.to=="object"&&b.to.length1?(currentClick[orientation]<(handles[0].offset()[pos]+handles[1].offset()[pos])/2?handles[0]:handles[1]):handles[0];setHandle(handle,correct(proposal,slider,handle),slider);call(settings.slide,slider);slider.change()}})}for(var i=0;i').find("input:last").val(val).change(function(a){a.stopPropagation()}))}else{if(settings.serialization.to[i]==false){handles[i].data("input",{val:function(a){if(typeof a!="undefined"){this.handle.data("noUiVal",a)}else{return this.handle.data("noUiVal")}},handle:handles[i]})}else{handles[i].data("input",settings.serialization.to[i].data("handleNR",i).val(val).change(function(){var arr=[null,null];arr[$(this).data("handleNR")]=$(this).val();slider.val(arr)}))}}}$(this).data("setup",{settings:settings,handles:handles,pos:pos,res:res})})},val:function(){if(typeof arguments[0]!=="undefined"){var val=typeof arguments[0]=="number"?[arguments[0]]:arguments[0];return this.each(function(){var setup=$(this).data("setup");for(var i=0;i range[1] ? range[0] - range[1] : range[1] - range[0]) + } + }; + + function correct(proposal, slider, handle) { + var setup = slider.data("setup"), handles = setup.handles, settings = setup.settings, pos = setup.pos; + proposal = proposal < 0 ? 0 : proposal > 100 ? 100 : proposal; + if (settings.handles == 2) { + if (handle.is(":first-child")) { + var other = parseFloat(handles[1][0].style[pos]) - settings.margin; + proposal = proposal > other ? other : proposal + } else { + var other = parseFloat(handles[0][0].style[pos]) + settings.margin; + proposal = proposal < other ? other : proposal + } + } + if (settings.step) { + var per = percentage.from(settings.range, settings.step); + proposal = Math.round(proposal / per) * per + } + return proposal + } + + function client(f) { + try { + return [(f.clientX || f.originalEvent.clientX || f.originalEvent.touches[0].clientX), (f.clientY || f.originalEvent.clientY || f.originalEvent.touches[0].clientY)] + } catch (e) { + return ["x", "y"] + } + } + + function place(handle, pos) { + return parseFloat(handle[0].style[pos]) + } + + var defaults = {handles: 1, serialization: {to: ["", ""], resolution: 0.01}}; + methods = { + create: function () { + return this.each(function () { + function setHandle(handle, to, slider) { + handle.css(pos, to + "%").data("input").val(percentage.is(settings.range, to).toFixed(res)) + } + + var settings = $.extend(defaults, options), handlehtml = "
", slider = $(this).data("_isnS_", true), handles = [], pos, orientation, classes = "", num = function (e) { + return !isNaN(parseFloat(e)) && isFinite(e) + }, split = (settings.serialization.resolution = settings.serialization.resolution || 0.01).toString().split("."), res = split[0] == 1 ? 0 : split[1].length; + settings.start = num(settings.start) ? [settings.start, 0] : settings.start; + $.each(settings, function (a, b) { + if (num(b)) { + settings[a] = parseFloat(b) + } else { + if (typeof b == "object" && num(b[0])) { + b[0] = parseFloat(b[0]); + if (num(b[1])) { + b[1] = parseFloat(b[1]) + } + } + } + var e = false; + b = typeof b == "undefined" ? "x" : b; + switch (a) { + case"range": + case"start": + e = b.length != 2 || !num(b[0]) || !num(b[1]); + break; + case"handles": + e = (b < 1 || b > 2 || !num(b)); + break; + case"connect": + e = b != "lower" && b != "upper" && typeof b != "boolean"; + break; + case"orientation": + e = (b != "vertical" && b != "horizontal"); + break; + case"margin": + case"step": + e = typeof b != "undefined" && !num(b); + break; + case"serialization": + e = typeof b != "object" || !num(b.resolution) || (typeof b.to == "object" && b.to.length < settings.handles); + break; + case"slide": + e = typeof b != "function"; + break + } + if (e && console) { + console.error("Bad input for " + a + " on slider:", slider) + } + }); + settings.margin = settings.margin ? percentage.from(settings.range, settings.margin) : 0; + if (settings.serialization.to instanceof jQuery || typeof settings.serialization.to == "string" || settings.serialization.to === false) { + settings.serialization.to = [settings.serialization.to] + } + if (settings.orientation == "vertical") { + classes += "vertical"; + pos = "top"; + orientation = 1 + } else { + classes += "horizontal"; + pos = "left"; + orientation = 0 + } + classes += settings.connect ? settings.connect == "lower" ? " connect lower" : " connect" : ""; + slider.addClass(classes); + for (var i = 0; i < settings.handles; i++) { + handles[i] = slider.append(handlehtml).children(":last"); + var setTo = percentage.to(settings.range, settings.start[i]); + handles[i].css(pos, setTo + "%"); + if (setTo == 100 && handles[i].is(":first-child")) { + handles[i].css("z-index", 2) + } + var bind = ".slider", onEvent = (EVENT === 1 ? "mousedown" : EVENT === 2 ? "MSPointerDown" : "touchstart") + bind + "X", moveEvent = (EVENT === 1 ? "mousemove" : EVENT === 2 ? "MSPointerMove" : "touchmove") + bind, offEvent = (EVENT === 1 ? "mouseup" : EVENT === 2 ? "MSPointerUp" : "touchend") + bind; + handles[i].find("div").on(onEvent, function (e) { + $("body").bind("selectstart" + bind, function () { + return false + }); + if (!slider.hasClass("disabled")) { + $("body").addClass("TOUCH"); + var handle = $(this).addClass("active").parent(), unbind = handle.add($(document)).add("body"), originalPosition = parseFloat(handle[0].style[pos]), originalClick = client(e), previousClick = originalClick, previousProposal = false; + $(document).on(moveEvent, function (f) { + f.preventDefault(); + var currentClick = client(f); + if (currentClick[0] == "x") { + return + } + currentClick[0] -= originalClick[0]; + currentClick[1] -= originalClick[1]; + var movement = [previousClick[0] != currentClick[0], previousClick[1] != currentClick[1]], proposal = originalPosition + ((currentClick[orientation] * 100) / (orientation ? slider.height() : slider.width())); + proposal = correct(proposal, slider, handle); + if (movement[orientation] && proposal != previousProposal) { + handle.css(pos, proposal + "%").data("input").val(percentage.is(settings.range, proposal).toFixed(res)); + call(settings.slide, slider.data("_n", true)); + previousProposal = proposal; + handle.css("z-index", handles.length == 2 && proposal == 100 && handle.is(":first-child") ? 2 : 1) + } + previousClick = currentClick + }).on(offEvent, function () { + unbind.off(bind); + $("body").removeClass("TOUCH"); + if (slider.find(".active").removeClass("active").end().data("_n")) { + slider.data("_n", false).change() + } + }) + } + }).on("click", function (e) { + e.stopPropagation() + }) + } + if (EVENT == 1) { + slider.on("click", function (f) { + if (!slider.hasClass("disabled")) { + var currentClick = client(f), proposal = ((currentClick[orientation] - slider.offset()[pos]) * 100) / (orientation ? slider.height() : slider.width()), handle = handles.length > 1 ? (currentClick[orientation] < (handles[0].offset()[pos] + handles[1].offset()[pos]) / 2 ? handles[0] : handles[1]) : handles[0]; + setHandle(handle, correct(proposal, slider, handle), slider); + call(settings.slide, slider); + slider.change() + } + }) + } + for (var i = 0; i < handles.length; i++) { + var val = percentage.is(settings.range, place(handles[i], pos)).toFixed(res); + if (typeof settings.serialization.to[i] == "string") { + handles[i].data("input", slider.append('').find("input:last").val(val).change(function (a) { + a.stopPropagation() + })) + } else { + if (settings.serialization.to[i] == false) { + handles[i].data("input", { + val: function (a) { + if (typeof a != "undefined") { + this.handle.data("noUiVal", a) + } else { + return this.handle.data("noUiVal") + } + }, handle: handles[i] + }) + } else { + handles[i].data("input", settings.serialization.to[i].data("handleNR", i).val(val).change(function () { + var arr = [null, null]; + arr[$(this).data("handleNR")] = $(this).val(); + slider.val(arr) + })) + } + } + } + $(this).data("setup", {settings: settings, handles: handles, pos: pos, res: res}) + }) + }, val: function () { + if (typeof arguments[0] !== "undefined") { + var val = typeof arguments[0] == "number" ? [arguments[0]] : arguments[0]; + return this.each(function () { + var setup = $(this).data("setup"); + for (var i = 0; i < setup.handles.length; i++) { + if (val[i] != null) { + var proposal = correct(percentage.to(setup.settings.range, val[i]), $(this), setup.handles[i]); + setup.handles[i].css(setup.pos, proposal + "%").data("input").val(percentage.is(setup.settings.range, proposal).toFixed(setup.res)) + } + } + }) + } else { + var handles = $(this).data("setup").handles, re = []; + for (var i = 0; i < handles.length; i++) { + re.push(parseFloat(handles[i].data("input").val())) + } + return re.length == 1 ? re[0] : re + } + }, disabled: function () { + return flag ? $(this).addClass("disabled") : $(this).removeClass("disabled") + } + }; + var $_val = jQuery.fn.val; + jQuery.fn.val = function () { + return this.data("_isnS_") ? methods.val.apply(this, arguments) : $_val.apply(this, arguments) + }; + return options == "disabled" ? methods.disabled.apply(this) : methods.create.apply(this) + } +})(jQuery); (function ($) { - - $.fn.videoPlayer = function(extras) { - - var playerGUI = "#" + $(this).attr('id'); - var playerID = "#" + $(this).find('.videoPlayer').attr('id'); - - try { - var settings = $.parseJSON($(this).find('.playerData').text()); - } catch (err) { - console.log('JSON parse ERROR, fall back to JS!'); - var settings = extras; - } - - $(this).find('.playerData').remove(); - $(this).append('
\ + + $.fn.videoPlayer = function (extras) { + + var playerGUI = "#" + $(this).attr('id'); + var playerID = "#" + $(this).find('.videoPlayer').attr('id'); + + try { + var settings = $.parseJSON($(this).find('.playerData').text()); + } catch (err) { + console.log('JSON parse ERROR, fall back to JS!'); + var settings = extras; + } + + $(this).find('.playerData').remove(); + $(this).append('
\ \
\
\ @@ -41,132 +277,138 @@ \ \
'); - - if ($(this).hasClass('audioPlayer')) { - $(this).find('.fullScreen').remove(); - $(this).find('.fullScreenOFF').remove(); - } - - createPlayer(playerGUI, playerID, settings, extras); - - } - - - function createPlayer(playerGUI, mainPlayer, settings, extras) { - - // Get supplied media from MEDIA array - var supplied = new Array; - $.each(settings.media, function(key, value) { if (key != 'poster') {supplied.push(key);}}); - formats = supplied.join(', '); - - var options = { - - ready: function () { - $(this).jPlayer("setMedia", settings.media); - if (settings.autoplay != null) { - $(mainPlayer).jPlayer('play'); - } - }, - - // Extra Settings - swfPath: "/projects/clean-jplayer-skin/Jplayer.swf", - supplied: formats, - solution: 'html, flash', - volume: 0.5, - size: settings.size, - smoothPlayBar: false, - keyEnabled: true, - - // CSS Selectors - cssSelectorAncestor: playerGUI, - cssSelector: { - videoPlay: ".video-play", - play: ".play", - pause: ".pause", - seekBar: ".seekBar", - playBar: ".playBar", - volumeBar: ".currentVolume", - volumeBarValue: ".currentVolume .curvol", - currentTime: ".time.current", - duration: ".time.duration", - fullScreen: ".fullScreen", - restoreScreen: ".fullScreenOFF", - gui: ".controls", - noSolution: ".noSolution" - }, - - error: function(event) { - if(event.jPlayer.error.type === $.jPlayer.error.URL_NOT_SET) { - // Setup the media stream again and play it. - $(this).jPlayer("setMedia", settings.media).jPlayer('play'); - } - }, - - play: function() { - $(playerGUI + ' .video-play').fadeOut(); - $(this).on('click', function() { $(mainPlayer).jPlayer('pause');}); - $(this).jPlayer("pauseOthers"); - }, - - pause: function() { - $(playerGUI + ' .video-play').fadeIn(); - $(playerGUI + ' .playerScreen').unbind('click'); - }, - - volumechange: function(event) { - if(event.jPlayer.options.muted) { - $(playerGUI + ' .currentVolume').val(0); - } else { - $(playerGUI + ' .currentVolume').val(event.jPlayer.options.volume); - } - }, - - timeupdate: function(event) { - $(playerGUI + ' .seekBar').val(event.jPlayer.status.currentPercentRelative); - }, - - progress: function(event) { - $(playerGUI + ' .seekBar').val(event.jPlayer.status.currentPercentRelative); - }, - - ended: function() { - $(this).jPlayer("setMedia", settings.media); - } - - - }; - - // Create the volume slider control - $(playerGUI + ' .currentVolume').slider({ - range: [0, 1], - step: 0.01, - start : 0.5, - handles: 1, - slide: function() { - var value = $(this).val(); - $(mainPlayer).jPlayer("option", "muted", false); - $(mainPlayer).jPlayer("option", "volume", value); - $(playerGUI + ' .volumeText').html('Volume: ' + (value * 100).toFixed(0) + ''); - } - }); - - $(playerGUI + ' .seekBar').slider({ - range: [0,100], - step: 0.01, - start: 0, - handles: 1, - slide: function() { - var value = $(this).val(); - $(mainPlayer).jPlayer("playHead", value); - } - - }); - - // Initialize Player - $.extend(options, extras); - $(mainPlayer).jPlayer(options); - - } - - + + if ($(this).hasClass('audioPlayer')) { + $(this).find('.fullScreen').remove(); + $(this).find('.fullScreenOFF').remove(); + } + + createPlayer(playerGUI, playerID, settings, extras); + + } + + + function createPlayer(playerGUI, mainPlayer, settings, extras) { + + // Get supplied media from MEDIA array + var supplied = new Array; + $.each(settings.media, function (key, value) { + if (key != 'poster') { + supplied.push(key); + } + }); + formats = supplied.join(', '); + + var options = { + + ready: function () { + $(this).jPlayer("setMedia", settings.media); + if (settings.autoplay != null) { + $(mainPlayer).jPlayer('play'); + } + }, + + // Extra Settings + swfPath: "/projects/clean-jplayer-skin/Jplayer.swf", + supplied: formats, + solution: 'html, flash', + volume: 0.5, + size: settings.size, + smoothPlayBar: false, + keyEnabled: true, + + // CSS Selectors + cssSelectorAncestor: playerGUI, + cssSelector: { + videoPlay: ".video-play", + play: ".play", + pause: ".pause", + seekBar: ".seekBar", + playBar: ".playBar", + volumeBar: ".currentVolume", + volumeBarValue: ".currentVolume .curvol", + currentTime: ".time.current", + duration: ".time.duration", + fullScreen: ".fullScreen", + restoreScreen: ".fullScreenOFF", + gui: ".controls", + noSolution: ".noSolution" + }, + + error: function (event) { + if (event.jPlayer.error.type === $.jPlayer.error.URL_NOT_SET) { + // Setup the media stream again and play it. + $(this).jPlayer("setMedia", settings.media).jPlayer('play'); + } + }, + + play: function () { + $(playerGUI + ' .video-play').fadeOut(); + $(this).on('click', function () { + $(mainPlayer).jPlayer('pause'); + }); + $(this).jPlayer("pauseOthers"); + }, + + pause: function () { + $(playerGUI + ' .video-play').fadeIn(); + $(playerGUI + ' .playerScreen').unbind('click'); + }, + + volumechange: function (event) { + if (event.jPlayer.options.muted) { + $(playerGUI + ' .currentVolume').val(0); + } else { + $(playerGUI + ' .currentVolume').val(event.jPlayer.options.volume); + } + }, + + timeupdate: function (event) { + $(playerGUI + ' .seekBar').val(event.jPlayer.status.currentPercentRelative); + }, + + progress: function (event) { + $(playerGUI + ' .seekBar').val(event.jPlayer.status.currentPercentRelative); + }, + + ended: function () { + $(this).jPlayer("setMedia", settings.media); + } + + + }; + + // Create the volume slider control + $(playerGUI + ' .currentVolume').slider({ + range: [0, 1], + step: 0.01, + start: 0.5, + handles: 1, + slide: function () { + var value = $(this).val(); + $(mainPlayer).jPlayer("option", "muted", false); + $(mainPlayer).jPlayer("option", "volume", value); + $(playerGUI + ' .volumeText').html('Volume: ' + (value * 100).toFixed(0) + ''); + } + }); + + $(playerGUI + ' .seekBar').slider({ + range: [0, 100], + step: 0.01, + start: 0, + handles: 1, + slide: function () { + var value = $(this).val(); + $(mainPlayer).jPlayer("playHead", value); + } + + }); + + // Initialize Player + $.extend(options, extras); + $(mainPlayer).jPlayer(options); + + } + + })(jQuery); \ No newline at end of file