diff --git a/akarpov/blog/api/serializers.py b/akarpov/blog/api/serializers.py index c1d3f9f..8b5fd29 100644 --- a/akarpov/blog/api/serializers.py +++ b/akarpov/blog/api/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from akarpov.blog.models import Comment, Post, Tag -from akarpov.common.api import RecursiveField +from akarpov.common.api.serializers import RecursiveField from akarpov.users.api.serializers import UserPublicInfoSerializer diff --git a/akarpov/blog/api/views.py b/akarpov/blog/api/views.py index 5fe4f5a..3ddc921 100644 --- a/akarpov/blog/api/views.py +++ b/akarpov/blog/api/views.py @@ -9,7 +9,7 @@ ) from akarpov.blog.models import Post from akarpov.blog.services import get_main_rating_posts -from akarpov.common.api import StandardResultsSetPagination +from akarpov.common.api.pagination import StandardResultsSetPagination class ListMainPostsView(generics.ListAPIView): diff --git a/akarpov/common/api.py b/akarpov/common/api.py deleted file mode 100644 index 45e2ea1..0000000 --- a/akarpov/common/api.py +++ /dev/null @@ -1,45 +0,0 @@ -from rest_framework import serializers -from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import SAFE_METHODS, BasePermission - -from akarpov.utils.models import get_object_user - - -class SmallResultsSetPagination(PageNumberPagination): - page_size = 10 - page_size_query_param = "page_size" - max_page_size = 100 - - -class StandardResultsSetPagination(PageNumberPagination): - page_size = 50 - page_size_query_param = "page_size" - max_page_size = 200 - - -class BigResultsSetPagination(PageNumberPagination): - page_size = 100 - page_size_query_param = "page_size" - max_page_size = 1000 - - -class RecursiveField(serializers.Serializer): - def to_representation(self, value): - serializer = self.parent.parent.__class__(value, context=self.context) - return serializer.data - - -class IsCreatorOrReadOnly(BasePermission): - def has_permission(self, request, view): - return bool( - request.method in SAFE_METHODS - or request.user - and get_object_user(view.get_object()) == request.user - ) - - -class SetUserModelSerializer(serializers.ModelSerializer): - def create(self, validated_data): - creator = self.context["request"].user - obj = self.Meta.model.objects.create(creator=creator, **validated_data) - return obj diff --git a/akarpov/common/api/__init__.py b/akarpov/common/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/akarpov/common/api/pagination.py b/akarpov/common/api/pagination.py new file mode 100644 index 0000000..b5fd290 --- /dev/null +++ b/akarpov/common/api/pagination.py @@ -0,0 +1,19 @@ +from rest_framework.pagination import PageNumberPagination + + +class SmallResultsSetPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 100 + + +class StandardResultsSetPagination(PageNumberPagination): + page_size = 50 + page_size_query_param = "page_size" + max_page_size = 200 + + +class BigResultsSetPagination(PageNumberPagination): + page_size = 100 + page_size_query_param = "page_size" + max_page_size = 1000 diff --git a/akarpov/common/api/permissions.py b/akarpov/common/api/permissions.py new file mode 100644 index 0000000..31c34ff --- /dev/null +++ b/akarpov/common/api/permissions.py @@ -0,0 +1,12 @@ +from rest_framework.permissions import SAFE_METHODS, BasePermission + +from akarpov.utils.models import get_object_user + + +class IsCreatorOrReadOnly(BasePermission): + def has_permission(self, request, view): + return bool( + request.method in SAFE_METHODS + or request.user + and get_object_user(view.get_object()) == request.user + ) diff --git a/akarpov/common/api/serializers.py b/akarpov/common/api/serializers.py new file mode 100644 index 0000000..7839939 --- /dev/null +++ b/akarpov/common/api/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers + + +class RecursiveField(serializers.Serializer): + def to_representation(self, value): + serializer = self.parent.parent.__class__(value, context=self.context) + return serializer.data + + +class SetUserModelSerializer(serializers.ModelSerializer): + def create(self, validated_data): + creator = self.context["request"].user + obj = self.Meta.model.objects.create(creator=creator, **validated_data) + return obj diff --git a/akarpov/gallery/api/__init__.py b/akarpov/gallery/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/akarpov/gallery/api/serializers.py b/akarpov/gallery/api/serializers.py new file mode 100644 index 0000000..58c8b62 --- /dev/null +++ b/akarpov/gallery/api/serializers.py @@ -0,0 +1,24 @@ +from rest_framework import serializers + +from akarpov.gallery.models import Image + + +class ImageSerializer(serializers.ModelSerializer): + class Meta: + model = Image + fields = ( + "slug", + "image", + "collection", + "public", + "image_cropped", + "created", + "modified", + ) + extra_kwargs = { + "slug": {"read_only": True}, + "collection": {"write_only": True}, + "image_cropped": {"read_only": True}, + "created": {"read_only": True}, + "modified": {"read_only": True}, + } diff --git a/akarpov/gallery/api/urls.py b/akarpov/gallery/api/urls.py new file mode 100644 index 0000000..35bc52b --- /dev/null +++ b/akarpov/gallery/api/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + +app_name = "gallery" + +urlpatterns = [ + path("", views.ListCreateImageAPIView.as_view(), name="list-create-all"), +] diff --git a/akarpov/gallery/api/views.py b/akarpov/gallery/api/views.py new file mode 100644 index 0000000..ee2f4c2 --- /dev/null +++ b/akarpov/gallery/api/views.py @@ -0,0 +1,21 @@ +from rest_framework import generics, parsers +from rest_framework.permissions import IsAuthenticatedOrReadOnly + +from akarpov.common.api.pagination import StandardResultsSetPagination +from akarpov.gallery.api.serializers import ImageSerializer +from akarpov.gallery.models import Image + + +class ListCreateImageAPIView(generics.ListCreateAPIView): + serializer_class = ImageSerializer + permission_classes = [IsAuthenticatedOrReadOnly] + parser_classes = [parsers.MultiPartParser, parsers.FormParser] + pagination_class = StandardResultsSetPagination + + def get_queryset(self): + if self.request.user.is_authenticated: + return self.request.user.images.all() + return Image.objects.filter(public=True) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) diff --git a/akarpov/gallery/forms.py b/akarpov/gallery/forms.py new file mode 100644 index 0000000..af761db --- /dev/null +++ b/akarpov/gallery/forms.py @@ -0,0 +1,40 @@ +from django import forms + +from akarpov.common.forms import MultipleFileField +from akarpov.gallery.models import Collection, Image + + +class ImageUploadForm(forms.ModelForm): + image = MultipleFileField + + class Meta: + model = Image + fields = ( + "image", + "collection", + "public", + ) + + def __init__(self, *args, **kwargs): + user = kwargs.pop("user", None) + super().__init__(*args, **kwargs) + if user is not None: + self.fields["collection"].queryset = Collection.objects.filter(user=user) + + def save(self, commit=True): + files = self.files.getlist("image") + instances = [] + for file in files: + instance = self.instance + instance.image = file + if commit: + instance.save() + instances.append(instance) + return instances + + +ImageFormSet = forms.modelformset_factory( + Image, + form=ImageUploadForm, + extra=3, # Number of images to upload at once; adjust as needed +) diff --git a/akarpov/gallery/migrations/0004_image_public.py b/akarpov/gallery/migrations/0004_image_public.py new file mode 100644 index 0000000..3f23993 --- /dev/null +++ b/akarpov/gallery/migrations/0004_image_public.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.6 on 2023-11-17 20:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("gallery", "0003_alter_collection_options_alter_image_options"), + ] + + operations = [ + migrations.AddField( + model_name="image", + name="public", + field=models.BooleanField(default=False), + ), + ] diff --git a/akarpov/gallery/models.py b/akarpov/gallery/models.py index 41e8292..fbfdf75 100644 --- a/akarpov/gallery/models.py +++ b/akarpov/gallery/models.py @@ -31,6 +31,7 @@ class Image(TimeStampedModel, ShortLinkModel, BaseImageModel, UserHistoryModel): collection = models.ForeignKey( "Collection", related_name="images", on_delete=models.CASCADE ) + public = models.BooleanField(default=False) user = models.ForeignKey( "users.User", related_name="images", on_delete=models.CASCADE ) diff --git a/akarpov/gallery/urls.py b/akarpov/gallery/urls.py index efcbf63..8c33714 100644 --- a/akarpov/gallery/urls.py +++ b/akarpov/gallery/urls.py @@ -2,6 +2,7 @@ from akarpov.gallery.views import ( collection_view, + image_upload_view, image_view, list_collections_view, list_tag_images_view, @@ -10,6 +11,7 @@ app_name = "gallery" urlpatterns = [ path("", list_collections_view, name="list"), + path("upload/", image_upload_view, name="upload"), path("", collection_view, name="collection"), path("tag/", list_tag_images_view, name="tag"), path("image/", image_view, name="view"), diff --git a/akarpov/gallery/views.py b/akarpov/gallery/views.py index 817d25f..652e955 100644 --- a/akarpov/gallery/views.py +++ b/akarpov/gallery/views.py @@ -1,7 +1,9 @@ +from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404 from django.views import generic from akarpov.common.views import HasPermissions +from akarpov.gallery.forms import ImageUploadForm from akarpov.gallery.models import Collection, Image, Tag @@ -46,3 +48,25 @@ class ImageView(generic.DetailView, HasPermissions): image_view = ImageView.as_view() + + +class ImageUploadView(LoginRequiredMixin, generic.CreateView): + model = Image + form_class = ImageUploadForm + success_url = "" # Replace with your success URL + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["user"] = self.request.user + return kwargs + + def post(self, request, *args, **kwargs): + form = self.get_form() + if form.is_valid(): + form.save() + return self.form_valid(form) + else: + return self.form_invalid(form) + + +image_upload_view = ImageUploadView.as_view() diff --git a/akarpov/music/api/serializers.py b/akarpov/music/api/serializers.py index 5230fe5..95548bd 100644 --- a/akarpov/music/api/serializers.py +++ b/akarpov/music/api/serializers.py @@ -1,7 +1,7 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from akarpov.common.api import SetUserModelSerializer +from akarpov.common.api.serializers import SetUserModelSerializer from akarpov.music.models import Album, Author, Playlist, Song from akarpov.users.api.serializers import UserPublicInfoSerializer diff --git a/akarpov/music/api/views.py b/akarpov/music/api/views.py index ed6842e..a3f0f50 100644 --- a/akarpov/music/api/views.py +++ b/akarpov/music/api/views.py @@ -1,6 +1,6 @@ from rest_framework import generics, permissions -from akarpov.common.api import IsCreatorOrReadOnly +from akarpov.common.api.permissions import IsCreatorOrReadOnly from akarpov.music.api.serializers import ( FullPlaylistSerializer, ListSongSerializer, diff --git a/akarpov/notifications/providers/site/api/views.py b/akarpov/notifications/providers/site/api/views.py index bded5bf..ee2392b 100644 --- a/akarpov/notifications/providers/site/api/views.py +++ b/akarpov/notifications/providers/site/api/views.py @@ -1,6 +1,6 @@ from rest_framework import generics, permissions -from akarpov.common.api import StandardResultsSetPagination +from akarpov.common.api.pagination import StandardResultsSetPagination from akarpov.notifications.models import Notification from akarpov.notifications.providers.site.api.serializers import ( SiteNotificationSerializer, diff --git a/akarpov/static/js/ws_script.js b/akarpov/static/js/ws_script.js new file mode 100644 index 0000000..abfdb4b --- /dev/null +++ b/akarpov/static/js/ws_script.js @@ -0,0 +1,177 @@ +// Determine the correct WebSocket protocol to use +const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; +const host = window.location.host; // Assuming host includes the port if needed +const socketPath = `${protocol}//${host}/ws/radio/`; + +let socket = new WebSocket(socketPath); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// Your existing timeSince function can remain unchanged + +// Update the onmessage handler here as needed for the music player +socket.onmessage = function(event) { + let data = JSON.parse(event.data); + // Process the data +}; + +// This function will attempt to reconnect the WebSocket after a 5-second delay +async function reconnectSocket() { + console.log("Radio socket disconnected, reconnecting..."); + let socketClosed = true; + await sleep(5000); // Wait 5 seconds before attempting to reconnect + while (socketClosed) { + try { + socket = new WebSocket(socketPath); + socket.onmessage = onMessageHandler; // Reassign your onmessage handler + socket.onclose = onSocketCloseHandler; // Reassign the onclose handler to this function + socketClosed = false; // Exit the loop if the connection is successful + } catch (e) { + console.log("Can't connect to socket, retrying in 1 second..."); + await sleep(1000); // Wait 1 second before retrying + } + } +} + +// Define a separate function for the onclose event +const onSocketCloseHandler = async function(event) { + if (!event.wasClean) { + // Only attempt to reconnect if the socket did not close cleanly + await reconnectSocket(); + } +}; + +// Assign the onclose handler to the socket +socket.onclose = onSocketCloseHandler; +let isAdmin = false; // This will be set based on socket connect data for admin TODO: retrieve from server + + + +function displaySong(data) { + // Display song details using jQuery to manipulate the DOM + $('#song-info').html(/* HTML structure for song info */); + // Update admin controls if isAdmin is true + if (isAdmin) { + $('#admin-controls').show(); + // Add slider and other controls + } + // Add song to history + addToHistory(data); +} + +function displayAdminControls(data) { + // Display admin controls if user is admin + if (isAdmin) { + $('#admin-controls').html(/* HTML structure for admin controls */); + } +} + +function updateSong(data) { + // Update the song information on the page + displaySong(data); +} + +function addToHistory(songData) { + var authors = songData.authors.map(author => author.name).join(", "); + var historyItemHtml = ` +
+
+
+ ${songData.name} +
+
+
+
${songData.name}
+

by ${authors}

+

${songData.album.name}

+ +
+
+
+
+ `; + $('#history').prepend(historyItemHtml); // prepend to add it to the top of the list +} + +// Functionality to handle sliding track length and selecting track to play +// ... + +// ws_script.js + +$(document).ready(function() { + var audioContext; + var analyser; + var started = false; // Flag to indicate if the audio context has started + var audio = document.getElementById('audio-player'); + var canvas = document.getElementById('audio-visualization'); + var ctx = canvas.getContext('2d'); + + // Function to initialize the AudioContext and analyser + function initAudioContext() { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + analyser = audioContext.createAnalyser(); + var audioSrc = audioContext.createMediaElementSource(audio); + audioSrc.connect(analyser); + analyser.connect(audioContext.destination); + } + + // Function to start or resume the audio context on user interaction + function startOrResumeContext() { + if (audioContext.state === 'suspended') { + audioContext.resume().then(() => { + console.log('Playback resumed successfully'); + }); + } + } + + // Volume control listener + $('#volume-control').on('input change', function() { + if (audio) { + audio.volume = $(this).val() / 100; + } + }); + + // Listener for play/pause button + $('#play-pause-button').on('click', function() { + if (!started) { + initAudioContext(); + started = true; + } + + startOrResumeContext(); // Resume the audio context if needed + + if (audio.paused) { + audio.play().then(() => { + console.log('Audio playing'); + }).catch((e) => { + console.error('Error playing audio:', e); + }); + } else { + audio.pause(); + console.log('Audio paused'); + } + }); + + function renderFrame() { + if (!analyser) return; + + requestAnimationFrame(renderFrame); + var fbc_array = new Uint8Array(analyser.frequencyBinCount); + analyser.getByteFrequencyData(fbc_array); + ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear the canvas + // Visualization code... + } + + // Error event listener for the audio element + audio.addEventListener('error', function(e) { + console.error('Error with audio playback:', e); + }); + + // Attempt to play the audio and start visualization when the audio can play through + audio.addEventListener('canplaythrough', function() { + startOrResumeContext(); + renderFrame(); // Start the visualizer + }); +}); diff --git a/akarpov/templates/base.html b/akarpov/templates/base.html index 0eb87f3..da3e5c8 100644 --- a/akarpov/templates/base.html +++ b/akarpov/templates/base.html @@ -152,11 +152,11 @@ {% endblock inline_javascript %} {% if request.user.is_authenticated %} +{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ form.management_form }} + {% for field in form %} + {{ field | as_crispy_field }} + {% endfor %} + +
+ +
+ +{% endblock %} diff --git a/akarpov/templates/gallery/list.html b/akarpov/templates/gallery/list.html index 21f5da2..5f863e8 100644 --- a/akarpov/templates/gallery/list.html +++ b/akarpov/templates/gallery/list.html @@ -1 +1,36 @@ {% extends 'base.html' %} + +{% block css %} + +{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/akarpov/templates/gallery/upload.html b/akarpov/templates/gallery/upload.html new file mode 100644 index 0000000..21f5da2 --- /dev/null +++ b/akarpov/templates/gallery/upload.html @@ -0,0 +1 @@ +{% extends 'base.html' %} diff --git a/akarpov/templates/music/radio.html b/akarpov/templates/music/radio.html index 02d4764..212363f 100644 --- a/akarpov/templates/music/radio.html +++ b/akarpov/templates/music/radio.html @@ -1,330 +1,77 @@ {% extends 'base.html' %} +{% load static %} {% block css %} {% endblock %} {% block content %} -
-
    -
  • -
  • -

    -

    -

    I Need You Back

    - -
    -
    -
  • -
-
-{% endblock %} - -{% block inline_javascript %} - - + + {% endblock %} diff --git a/akarpov/users/api/views.py b/akarpov/users/api/views.py index b3aacaa..de91283 100644 --- a/akarpov/users/api/views.py +++ b/akarpov/users/api/views.py @@ -3,7 +3,7 @@ from rest_framework import generics, permissions, status, views from rest_framework.response import Response -from akarpov.common.api import SmallResultsSetPagination +from akarpov.common.api.pagination import SmallResultsSetPagination from akarpov.common.jwt import sign_jwt from akarpov.users.api.serializers import ( UserEmailVerification, diff --git a/config/api_router.py b/config/api_router.py index 0f23f2b..8a63b4e 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -24,6 +24,10 @@ "users/", include("akarpov.users.api.urls", namespace="users"), ), + path( + "gallery/", + include("akarpov.gallery.api.urls", namespace="gallery"), + ), path( "notifications/", include("akarpov.notifications.providers.urls", namespace="notifications"),