mirror of
				https://github.com/Alexander-D-Karpov/akarpov
				synced 2025-11-04 07:57:26 +03:00 
			
		
		
		
	updated gallery and music radio
This commit is contained in:
		
							parent
							
								
									4ef6021499
								
							
						
					
					
						commit
						e94f90d091
					
				| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
from rest_framework import serializers
 | 
					from rest_framework import serializers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from akarpov.blog.models import Comment, Post, Tag
 | 
					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
 | 
					from akarpov.users.api.serializers import UserPublicInfoSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from akarpov.blog.models import Post
 | 
					from akarpov.blog.models import Post
 | 
				
			||||||
from akarpov.blog.services import get_main_rating_posts
 | 
					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):
 | 
					class ListMainPostsView(generics.ListAPIView):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 | 
				
			||||||
							
								
								
									
										0
									
								
								akarpov/common/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								akarpov/common/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										19
									
								
								akarpov/common/api/pagination.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								akarpov/common/api/pagination.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
							
								
								
									
										12
									
								
								akarpov/common/api/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								akarpov/common/api/permissions.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
							
								
								
									
										14
									
								
								akarpov/common/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								akarpov/common/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
							
								
								
									
										0
									
								
								akarpov/gallery/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								akarpov/gallery/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								akarpov/gallery/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								akarpov/gallery/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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},
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
							
								
								
									
										9
									
								
								akarpov/gallery/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								akarpov/gallery/api/urls.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										21
									
								
								akarpov/gallery/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								akarpov/gallery/api/views.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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)
 | 
				
			||||||
							
								
								
									
										40
									
								
								akarpov/gallery/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								akarpov/gallery/forms.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										17
									
								
								akarpov/gallery/migrations/0004_image_public.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								akarpov/gallery/migrations/0004_image_public.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@ class Image(TimeStampedModel, ShortLinkModel, BaseImageModel, UserHistoryModel):
 | 
				
			||||||
    collection = models.ForeignKey(
 | 
					    collection = models.ForeignKey(
 | 
				
			||||||
        "Collection", related_name="images", on_delete=models.CASCADE
 | 
					        "Collection", related_name="images", on_delete=models.CASCADE
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    public = models.BooleanField(default=False)
 | 
				
			||||||
    user = models.ForeignKey(
 | 
					    user = models.ForeignKey(
 | 
				
			||||||
        "users.User", related_name="images", on_delete=models.CASCADE
 | 
					        "users.User", related_name="images", on_delete=models.CASCADE
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from akarpov.gallery.views import (
 | 
					from akarpov.gallery.views import (
 | 
				
			||||||
    collection_view,
 | 
					    collection_view,
 | 
				
			||||||
 | 
					    image_upload_view,
 | 
				
			||||||
    image_view,
 | 
					    image_view,
 | 
				
			||||||
    list_collections_view,
 | 
					    list_collections_view,
 | 
				
			||||||
    list_tag_images_view,
 | 
					    list_tag_images_view,
 | 
				
			||||||
| 
						 | 
					@ -10,6 +11,7 @@
 | 
				
			||||||
app_name = "gallery"
 | 
					app_name = "gallery"
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("", list_collections_view, name="list"),
 | 
					    path("", list_collections_view, name="list"),
 | 
				
			||||||
 | 
					    path("upload/", image_upload_view, name="upload"),
 | 
				
			||||||
    path("<str:slug>", collection_view, name="collection"),
 | 
					    path("<str:slug>", collection_view, name="collection"),
 | 
				
			||||||
    path("tag/<str:slug>", list_tag_images_view, name="tag"),
 | 
					    path("tag/<str:slug>", list_tag_images_view, name="tag"),
 | 
				
			||||||
    path("image/<str:slug>", image_view, name="view"),
 | 
					    path("image/<str:slug>", image_view, name="view"),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,9 @@
 | 
				
			||||||
 | 
					from django.contrib.auth.mixins import LoginRequiredMixin
 | 
				
			||||||
from django.shortcuts import get_object_or_404
 | 
					from django.shortcuts import get_object_or_404
 | 
				
			||||||
from django.views import generic
 | 
					from django.views import generic
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from akarpov.common.views import HasPermissions
 | 
					from akarpov.common.views import HasPermissions
 | 
				
			||||||
 | 
					from akarpov.gallery.forms import ImageUploadForm
 | 
				
			||||||
from akarpov.gallery.models import Collection, Image, Tag
 | 
					from akarpov.gallery.models import Collection, Image, Tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,3 +48,25 @@ class ImageView(generic.DetailView, HasPermissions):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
image_view = ImageView.as_view()
 | 
					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()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
from drf_spectacular.utils import extend_schema_field
 | 
					from drf_spectacular.utils import extend_schema_field
 | 
				
			||||||
from rest_framework import serializers
 | 
					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.music.models import Album, Author, Playlist, Song
 | 
				
			||||||
from akarpov.users.api.serializers import UserPublicInfoSerializer
 | 
					from akarpov.users.api.serializers import UserPublicInfoSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
from rest_framework import generics, permissions
 | 
					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 (
 | 
					from akarpov.music.api.serializers import (
 | 
				
			||||||
    FullPlaylistSerializer,
 | 
					    FullPlaylistSerializer,
 | 
				
			||||||
    ListSongSerializer,
 | 
					    ListSongSerializer,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
from rest_framework import generics, permissions
 | 
					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.models import Notification
 | 
				
			||||||
from akarpov.notifications.providers.site.api.serializers import (
 | 
					from akarpov.notifications.providers.site.api.serializers import (
 | 
				
			||||||
    SiteNotificationSerializer,
 | 
					    SiteNotificationSerializer,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										177
									
								
								akarpov/static/js/ws_script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								akarpov/static/js/ws_script.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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 = `
 | 
				
			||||||
 | 
					    <div class="card mb-3" style="max-width: 540px;">
 | 
				
			||||||
 | 
					        <div class="row g-0">
 | 
				
			||||||
 | 
					            <div class="col-md-4">
 | 
				
			||||||
 | 
					                <img src="${songData.image}" class="img-fluid rounded-start" alt="${songData.name}">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-md-8">
 | 
				
			||||||
 | 
					                <div class="card-body">
 | 
				
			||||||
 | 
					                    <h5 class="card-title">${songData.name}</h5>
 | 
				
			||||||
 | 
					                    <p class="card-text"><small class="text-muted">by ${authors}</small></p>
 | 
				
			||||||
 | 
					                    <p class="card-text">${songData.album.name}</p>
 | 
				
			||||||
 | 
					                    <!-- Admin controls would go here if needed -->
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  $('#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
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -152,11 +152,11 @@
 | 
				
			||||||
    {% endblock inline_javascript %}
 | 
					    {% endblock inline_javascript %}
 | 
				
			||||||
    {% if request.user.is_authenticated %}
 | 
					    {% if request.user.is_authenticated %}
 | 
				
			||||||
    <script>
 | 
					    <script>
 | 
				
			||||||
      {% if request.is_secure %}
 | 
					      const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
 | 
				
			||||||
      let socket = new WebSocket(`wss://{{ request.get_host }}/ws/notifications/`);
 | 
					      const host = window.location.host; // Assuming host includes the port if needed
 | 
				
			||||||
      {% else %}
 | 
					      const socketPath = `${protocol}//${host}/ws/notifications/`;
 | 
				
			||||||
      let socket = new WebSocket(`ws://{{ request.get_host }}/ws/notifications/`);
 | 
					
 | 
				
			||||||
      {% endif %}
 | 
					      let notification_socket = new WebSocket(socketPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      function sleep(ms) {
 | 
					      function sleep(ms) {
 | 
				
			||||||
          return new Promise(resolve => setTimeout(resolve, ms));
 | 
					          return new Promise(resolve => setTimeout(resolve, ms));
 | 
				
			||||||
| 
						 | 
					@ -211,18 +211,17 @@
 | 
				
			||||||
        toastBootstrap.show()
 | 
					        toastBootstrap.show()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      socket.onmessage = fn
 | 
					      notification_socket.onmessage = fn
 | 
				
			||||||
      socket.onclose = async function(event) {
 | 
					      notification_socket.onclose = async function(event) {
 | 
				
			||||||
        console.log("Notifications socket disconnected, reconnecting...")
 | 
					        console.log("Notifications socket disconnected, reconnecting...")
 | 
				
			||||||
        let socketClosed = true;
 | 
					        let socketClosed = true;
 | 
				
			||||||
        await sleep(5000)
 | 
					        await sleep(5000)
 | 
				
			||||||
        while (socketClosed) {
 | 
					        while (socketClosed) {
 | 
				
			||||||
          {# TODO: reconnect socket here #}
 | 
					 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
            let cl = socket.onclose
 | 
					            let cl = notification_socket.onclose
 | 
				
			||||||
            socket = new WebSocket(`ws://127.0.0.1:8000/ws/notifications/`);
 | 
					            notification_socket = new WebSocket(socketPath);
 | 
				
			||||||
            socket.onmessage = fn
 | 
					            notification_socket.onmessage = fn
 | 
				
			||||||
            socket.onclose = cl
 | 
					            notification_socket.onclose = cl
 | 
				
			||||||
            socketClosed = false
 | 
					            socketClosed = false
 | 
				
			||||||
          } catch (e) {
 | 
					          } catch (e) {
 | 
				
			||||||
            console.log("Can't connect to socket, reconnecting...")
 | 
					            console.log("Can't connect to socket, reconnecting...")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										43
									
								
								akarpov/templates/gallery/image_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								akarpov/templates/gallery/image_form.html
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					{% extends 'base.html' %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					{% load crispy_forms_tags %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block javascript %}
 | 
				
			||||||
 | 
					  <script src="{% static 'js/jquery.min.js' %}"></script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					<form method="post" action="{% url 'gallery:upload' %}" enctype="multipart/form-data">
 | 
				
			||||||
 | 
					    {% csrf_token %}
 | 
				
			||||||
 | 
					    {{ form.management_form }}
 | 
				
			||||||
 | 
					      {% for field in form %}
 | 
				
			||||||
 | 
					        {{ field | as_crispy_field }}
 | 
				
			||||||
 | 
					      {% endfor %}
 | 
				
			||||||
 | 
					    <button type="submit">Upload</button>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div id="progress-bar" style="width:0; height:20px; background:green;"></div>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					const form = document.querySelector('form');
 | 
				
			||||||
 | 
					form.addEventListener('submit', function(e) {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    const formData = new FormData(form);
 | 
				
			||||||
 | 
					    const xhr = new XMLHttpRequest();
 | 
				
			||||||
 | 
					    xhr.open('POST', form.action, true);
 | 
				
			||||||
 | 
					    xhr.upload.onprogress = function(e) {
 | 
				
			||||||
 | 
					        if (e.lengthComputable) {
 | 
				
			||||||
 | 
					            const percentage = (e.loaded / e.total) * 100;
 | 
				
			||||||
 | 
					            document.getElementById('progress-bar').style.width = percentage + '%';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    xhr.onload = function() {
 | 
				
			||||||
 | 
					        if (this.status === 200) {
 | 
				
			||||||
 | 
					            console.log('Upload complete!');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            console.error('Upload failed:', this.responseText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    xhr.send(formData);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					@ -1 +1,36 @@
 | 
				
			||||||
{% extends 'base.html' %}
 | 
					{% extends 'base.html' %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block css %}
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					    .gallery {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-wrap: wrap;
 | 
				
			||||||
 | 
					        justify-content: start; /* Align items to the start of the container */
 | 
				
			||||||
 | 
					        align-items: stretch; /* Stretch items to fill the container */
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .gallery-item {
 | 
				
			||||||
 | 
					        /* Adjust the margin as needed */
 | 
				
			||||||
 | 
					        margin: 5px;
 | 
				
			||||||
 | 
					        flex-grow: 1;
 | 
				
			||||||
 | 
					        flex-shrink: 1;
 | 
				
			||||||
 | 
					        flex-basis: auto; /* Automatically adjust the basis based on the content size */
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .gallery-item img {
 | 
				
			||||||
 | 
					        width: 100%; /* Make image responsive */
 | 
				
			||||||
 | 
					        height: auto; /* Maintain aspect ratio */
 | 
				
			||||||
 | 
					        display: block; /* Remove bottom space under the image */
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					<div class="gallery">
 | 
				
			||||||
 | 
					    {% for image in object_list %}
 | 
				
			||||||
 | 
					    <div class="gallery-item">
 | 
				
			||||||
 | 
					        <img src="{{ image.url }}" alt="Image">
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								akarpov/templates/gallery/upload.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								akarpov/templates/gallery/upload.html
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					{% extends 'base.html' %}
 | 
				
			||||||
| 
						 | 
					@ -1,330 +1,77 @@
 | 
				
			||||||
{% extends 'base.html' %}
 | 
					{% extends 'base.html' %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block css %}
 | 
					{% block css %}
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
body {
 | 
					body {
 | 
				
			||||||
  background-color: #EDEDED;
 | 
					    background-color: #f8f9fa;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
h1 {
 | 
					#song-info .card {
 | 
				
			||||||
  font-family: "Open Sans", sans-serif;
 | 
					    margin-bottom: 30px;
 | 
				
			||||||
  font-size: 13pt;
 | 
					 | 
				
			||||||
  font-weight: 600;
 | 
					 | 
				
			||||||
  text-transform: uppercase;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
  cursor: default;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
h4 {
 | 
					#history .card {
 | 
				
			||||||
  font-family: "Open Sans", sans-serif;
 | 
					    margin-bottom: 15px;
 | 
				
			||||||
  font-size: 8pt;
 | 
					 | 
				
			||||||
  font-weight: 400;
 | 
					 | 
				
			||||||
  cursor: default;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
h2 {
 | 
					#admin-controls {
 | 
				
			||||||
  font-family: "Open Sans", sans-serif;
 | 
					    margin-bottom: 30px;
 | 
				
			||||||
  font-size: 13pt;
 | 
					 | 
				
			||||||
  font-weight: 300;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
  cursor: default;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.player {
 | 
					.card-img-top {
 | 
				
			||||||
  height: 190px;
 | 
					    width: 100%;
 | 
				
			||||||
  width: 430px;
 | 
					    height: 200px; /* You can adjust as needed */
 | 
				
			||||||
  background-color: #1E2125;
 | 
					    object-fit: cover;
 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  -webkit-touch-callout: none;
 | 
					 | 
				
			||||||
  -webkit-user-select: none;
 | 
					 | 
				
			||||||
  -moz-user-select: none;
 | 
					 | 
				
			||||||
  -ms-user-select: none;
 | 
					 | 
				
			||||||
  user-select: none;
 | 
					 | 
				
			||||||
  top: 50%;
 | 
					 | 
				
			||||||
  left: 50%;
 | 
					 | 
				
			||||||
  transform: translate(-50%, -50%);
 | 
					 | 
				
			||||||
  -webkit-transform: translate(-50%, -50%);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.player ul {
 | 
					 | 
				
			||||||
  list-style: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.player ul li {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.cover {
 | 
					.audio-player {
 | 
				
			||||||
  position: absolute;
 | 
					    width: 100%;
 | 
				
			||||||
  top: 0;
 | 
					    margin-top: 10px;
 | 
				
			||||||
  left: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cover img {
 | 
					 | 
				
			||||||
  height: 190px;
 | 
					 | 
				
			||||||
  width: 190px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.info h1 {
 | 
					.range-slider {
 | 
				
			||||||
  margin-top: 15px;
 | 
					    width: 100%;
 | 
				
			||||||
  margin-left: 180px;
 | 
					    margin-top: 10px;
 | 
				
			||||||
  line-height: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.info h4 {
 | 
					 | 
				
			||||||
  margin-left: 180px;
 | 
					 | 
				
			||||||
  line-height: 20px;
 | 
					 | 
				
			||||||
  color: #636367;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.info h2 {
 | 
					 | 
				
			||||||
  margin-left: 180px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.button-items {
 | 
					.range-slider input[type="range"] {
 | 
				
			||||||
  margin-left: 180px;
 | 
					    width: 100%;
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#slider {
 | 
					 | 
				
			||||||
  width: 182px;
 | 
					 | 
				
			||||||
  height: 4px;
 | 
					 | 
				
			||||||
  background: #151518;
 | 
					 | 
				
			||||||
  border-radius: 2px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#slider div {
 | 
					 | 
				
			||||||
  width: 4px;
 | 
					 | 
				
			||||||
  height: 4px;
 | 
					 | 
				
			||||||
  margin-top: 1px;
 | 
					 | 
				
			||||||
  background: #EF6DBC;
 | 
					 | 
				
			||||||
  border-radius: 2px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#timer {
 | 
					 | 
				
			||||||
  color: #494B4E;
 | 
					 | 
				
			||||||
  line-height: 0;
 | 
					 | 
				
			||||||
  font-size: 9pt;
 | 
					 | 
				
			||||||
  float: right;
 | 
					 | 
				
			||||||
  font-family: Arial, Sans-Serif;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.controls {
 | 
					 | 
				
			||||||
  margin-top: 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.controls svg:nth-child(2) {
 | 
					 | 
				
			||||||
  margin-left: 5px;
 | 
					 | 
				
			||||||
  margin-right: 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#play {
 | 
					 | 
				
			||||||
  padding: 0 3px;
 | 
					 | 
				
			||||||
  width: 30px;
 | 
					 | 
				
			||||||
  height: 30px;
 | 
					 | 
				
			||||||
  x: 0px;
 | 
					 | 
				
			||||||
  y: 0px;
 | 
					 | 
				
			||||||
  enable-background: new 0 0 25 25;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#play g {
 | 
					 | 
				
			||||||
  stroke: #FEFEFE;
 | 
					 | 
				
			||||||
  stroke-width: 1;
 | 
					 | 
				
			||||||
  stroke-miterlimit: 10;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#play g path {
 | 
					 | 
				
			||||||
  fill: #FEFEFE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#play:hover {
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#play:hover g {
 | 
					 | 
				
			||||||
  stroke: #8F4DA9;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#play:hover g path {
 | 
					 | 
				
			||||||
  fill: #9b59b6;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.step-backward {
 | 
					 | 
				
			||||||
  width: 18px;
 | 
					 | 
				
			||||||
  height: 18px;
 | 
					 | 
				
			||||||
  x: 0px;
 | 
					 | 
				
			||||||
  y: 0px;
 | 
					 | 
				
			||||||
  enable-background: new 0 0 25 25;
 | 
					 | 
				
			||||||
  margin-bottom: 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.step-backward g polygon {
 | 
					 | 
				
			||||||
  fill: #FEFEFE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.step-foreward {
 | 
					 | 
				
			||||||
  width: 18px;
 | 
					 | 
				
			||||||
  height: 18px;
 | 
					 | 
				
			||||||
  x: 0px;
 | 
					 | 
				
			||||||
  y: 0px;
 | 
					 | 
				
			||||||
  enable-background: new 0 0 25 25;
 | 
					 | 
				
			||||||
  margin-bottom: 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.step-foreward g polygon {
 | 
					 | 
				
			||||||
  fill: #FEFEFE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#pause {
 | 
					 | 
				
			||||||
  x: 0px;
 | 
					 | 
				
			||||||
  y: 0px;
 | 
					 | 
				
			||||||
  enable-background: new 0 0 25 25;
 | 
					 | 
				
			||||||
  width: 30px;
 | 
					 | 
				
			||||||
  height: 30px;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  margin-left: -38px;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#pause rect {
 | 
					 | 
				
			||||||
  fill: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#pause:hover rect {
 | 
					 | 
				
			||||||
  fill: #8F4DA9;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.step-backward g polygon:hover, .step-foreward g polygon:hover {
 | 
					 | 
				
			||||||
  fill: #EF6DBC;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#skip p {
 | 
					 | 
				
			||||||
  color: #2980b9;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#skip p:hover {
 | 
					 | 
				
			||||||
  color: #e74c3c;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.expend {
 | 
					 | 
				
			||||||
  padding: 0.5px;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.expend svg:hover g polygon {
 | 
					 | 
				
			||||||
  fill: #EF6DBC;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
    <div class="player">
 | 
					    <div class="container mt-5 music-body">
 | 
				
			||||||
   <ul>
 | 
					        <!-- Music Player and Visualization -->
 | 
				
			||||||
      <li class="cover"><img id="cover" src="" alt=""/></li>
 | 
					        <div class="music-player-container">
 | 
				
			||||||
      <li class="info">
 | 
					            <canvas id="audio-visualization" width="800" height="100"></canvas>
 | 
				
			||||||
         <h1 id="artist"></h1>
 | 
					            <audio id="audio-player" controls>
 | 
				
			||||||
         <h4 id="album"></h4>
 | 
					                <source src="" type="audio/mpeg">
 | 
				
			||||||
         <h2 id="name">I Need You Back</h2>
 | 
					                Your browser does not support the audio element.
 | 
				
			||||||
 | 
					 | 
				
			||||||
         <div class="button-items">
 | 
					 | 
				
			||||||
            <audio id="music">
 | 
					 | 
				
			||||||
               <source id="music-src" type="audio/mp3">
 | 
					 | 
				
			||||||
            </audio>
 | 
					            </audio>
 | 
				
			||||||
            <div id="slider"><div id="elapsed"></div></div>
 | 
					        </div>
 | 
				
			||||||
            <p id="timer">0:00</p>
 | 
					 | 
				
			||||||
            <div class="controls">
 | 
					 | 
				
			||||||
               <span class="expend"><svg class="step-backward" viewBox="0 0 25 25" xml:space="preserve">
 | 
					 | 
				
			||||||
                  <g><polygon points="4.9,4.3 9,4.3 9,11.6 21.4,4.3 21.4,20.7 9,13.4 9,20.7 4.9,20.7"/></g>
 | 
					 | 
				
			||||||
               </svg></span>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
               <svg id="play" viewBox="0 0 25 25" xml:space="preserve">
 | 
					        <!-- Now Playing Information -->
 | 
				
			||||||
                   <defs><rect x="-49.5" y="-132.9" width="446.4" height="366.4"/></defs>
 | 
					        <div class="now-playing">
 | 
				
			||||||
                  <g><circle fill="none" cx="12.5" cy="12.5" r="10.8"/>
 | 
					            <div class="track-image">
 | 
				
			||||||
                       <path fill-rule="evenodd" clip-rule="evenodd" d="M8.7,6.9V18c0,0,0.2,1.4,1.8,0l8.1-4.8c0,0,1.2-1.1-1-2L9.8,6.5 C9.8,6.5,9.1,6,8.7,6.9z"/>
 | 
					                <img id="track-image" src="" alt="Track Image">
 | 
				
			||||||
                  </g>
 | 
					            </div>
 | 
				
			||||||
               </svg>
 | 
					            <div class="artist-info">
 | 
				
			||||||
 | 
					                <!-- Dynamically populate artist info -->
 | 
				
			||||||
 | 
					 | 
				
			||||||
               <svg id="pause" viewBox="0 0 25 25" xml:space="preserve">
 | 
					 | 
				
			||||||
                  <g>
 | 
					 | 
				
			||||||
                     <rect x="6" y="4.6" width="3.8" height="15.7"/>
 | 
					 | 
				
			||||||
                     <rect x="14" y="4.6" width="3.9" height="15.7"/>
 | 
					 | 
				
			||||||
                  </g>
 | 
					 | 
				
			||||||
               </svg>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
               <span class="expend"><svg class="step-foreward" viewBox="0 0 25 25" xml:space="preserve">
 | 
					 | 
				
			||||||
                  <g><polygon points="20.7,4.3 16.6,4.3 16.6,11.6 4.3,4.3 4.3,20.7 16.7,13.4 16.6,20.7 20.7,20.7"/></g>
 | 
					 | 
				
			||||||
                  </svg></span>
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        </li>
 | 
					
 | 
				
			||||||
     </ul>
 | 
					        <div id="admin-controls" class="mb-3" style="display: none;">
 | 
				
			||||||
 | 
					            <!-- Admin Controls will be inserted here -->
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
{% endblock %}
 | 
					
 | 
				
			||||||
 | 
					        <h2>History</h2>
 | 
				
			||||||
{% block inline_javascript %}
 | 
					        <div id="history" class="mb-3">
 | 
				
			||||||
 | 
					            <!-- Song History will be inserted here -->
 | 
				
			||||||
    <script>
 | 
					        </div>
 | 
				
			||||||
        const music = document.getElementById("music");
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async function playAudio() {
 | 
					    <script src="{% static 'js/jquery.js' %}"></script>
 | 
				
			||||||
            try {
 | 
					    <script src="{% static 'js/ws_script.js' %}"></script>
 | 
				
			||||||
              await music.play();
 | 
					 | 
				
			||||||
              playButton.style.visibility = "hidden";
 | 
					 | 
				
			||||||
              pause.style.visibility = "visible";
 | 
					 | 
				
			||||||
            } catch (err) {
 | 
					 | 
				
			||||||
              playButton.style.visibility = "visible";
 | 
					 | 
				
			||||||
              pause.style.visibility = "hidden";
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const music_src = document.getElementById("music-src")
 | 
					 | 
				
			||||||
        const cover_src = document.getElementById("cover")
 | 
					 | 
				
			||||||
        const name = document.getElementById("name")
 | 
					 | 
				
			||||||
        const album = document.getElementById("album")
 | 
					 | 
				
			||||||
        const artist = document.getElementById("artist")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const socket = new WebSocket(
 | 
					 | 
				
			||||||
            'ws://'
 | 
					 | 
				
			||||||
            + window.location.host
 | 
					 | 
				
			||||||
            + '/ws/radio/'
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        socket.onmessage = function(e) {
 | 
					 | 
				
			||||||
            const data = JSON.parse(e.data);
 | 
					 | 
				
			||||||
            console.log(data)
 | 
					 | 
				
			||||||
            music_src.src = data.file
 | 
					 | 
				
			||||||
            if (data.image !== null) {
 | 
					 | 
				
			||||||
              cover_src.src = data.image
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            name.innerText = data.name
 | 
					 | 
				
			||||||
            playAudio()
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        socket.onclose = function(e) {
 | 
					 | 
				
			||||||
            console.error('Chat socket closed unexpectedly');
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        const playButton = document.getElementById("play");
 | 
					 | 
				
			||||||
        const pauseButton = document.getElementById("pause");
 | 
					 | 
				
			||||||
        const playhead = document.getElementById("elapsed");
 | 
					 | 
				
			||||||
        const timeline = document.getElementById("slider");
 | 
					 | 
				
			||||||
        const timer = document.getElementById("timer");
 | 
					 | 
				
			||||||
        let duration;
 | 
					 | 
				
			||||||
        pauseButton.style.visibility = "hidden";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
 | 
					 | 
				
			||||||
        music.addEventListener("timeupdate", timeUpdate, false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        function timeUpdate() {
 | 
					 | 
				
			||||||
          const playPercent = timelineWidth * (music.currentTime / duration);
 | 
					 | 
				
			||||||
          playhead.style.width = playPercent + "px";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const secondsIn = Math.floor(((music.currentTime / duration) / 3.5) * 100);
 | 
					 | 
				
			||||||
          if (secondsIn <= 9) {
 | 
					 | 
				
			||||||
            timer.innerHTML = "0:0" + secondsIn;
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            timer.innerHTML = "0:" + secondsIn;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        playButton.onclick = function() {
 | 
					 | 
				
			||||||
            playAudio()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        pauseButton.onclick = function() {
 | 
					 | 
				
			||||||
          music.pause();
 | 
					 | 
				
			||||||
          playButton.style.visibility = "visible";
 | 
					 | 
				
			||||||
          pause.style.visibility = "hidden";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        music.addEventListener("canplaythrough", function () {
 | 
					 | 
				
			||||||
          duration = music.duration;
 | 
					 | 
				
			||||||
        }, false);
 | 
					 | 
				
			||||||
    </script>
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
from rest_framework import generics, permissions, status, views
 | 
					from rest_framework import generics, permissions, status, views
 | 
				
			||||||
from rest_framework.response import Response
 | 
					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.common.jwt import sign_jwt
 | 
				
			||||||
from akarpov.users.api.serializers import (
 | 
					from akarpov.users.api.serializers import (
 | 
				
			||||||
    UserEmailVerification,
 | 
					    UserEmailVerification,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,10 @@
 | 
				
			||||||
        "users/",
 | 
					        "users/",
 | 
				
			||||||
        include("akarpov.users.api.urls", namespace="users"),
 | 
					        include("akarpov.users.api.urls", namespace="users"),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "gallery/",
 | 
				
			||||||
 | 
					        include("akarpov.gallery.api.urls", namespace="gallery"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "notifications/",
 | 
					        "notifications/",
 | 
				
			||||||
        include("akarpov.notifications.providers.urls", namespace="notifications"),
 | 
					        include("akarpov.notifications.providers.urls", namespace="notifications"),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user