Forráskód Böngészése

added tag playlists functionality

sleepytaco 3 éve
szülő
commit
b96bc7c4be

+ 15 - 1
apps/main/models.py

@@ -3,6 +3,7 @@ import time
 
 import googleapiclient.errors
 import humanize
+from django.contrib.auth.models import User
 from django.db import models
 from django.db.models import Q
 from google.oauth2.credentials import Credentials
@@ -1073,11 +1074,12 @@ class PlaylistManager(models.Manager):
 
             try:
                 pl_response = pl_request.execute()
-            except googleapiclient.errors.HttpError:  # failed to update playlist details
+            except googleapiclient.errors.HttpError as e:  # failed to update playlist details
                 # possible causes:
                 # playlistItemsNotAccessible (403)
                 # playlistItemNotFound (404)
                 # playlistOperationUnsupported (400)
+                print("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
                 return -1
 
             print(pl_response)
@@ -1089,7 +1091,19 @@ class PlaylistManager(models.Manager):
             return 0
 
 
+class Tag(models.Model):
+    name = models.CharField(max_length=69)
+    created_by = models.ForeignKey(User, related_name="playlist_tags", on_delete=models.CASCADE)
+
+    # type = models.CharField(max_length=10)  # either 'playlist' or 'video'
+
+    created_at = models.DateTimeField(auto_now_add=True)
+    updated_at = models.DateTimeField(auto_now=True)
+
+
 class Playlist(models.Model):
+    tags = models.ManyToManyField(Tag, related_name="playlists")
+
     # playlist details
     playlist_id = models.CharField(max_length=150)
     name = models.CharField(max_length=150, blank=True)  # YT PLAYLIST NAMES CAN ONLY HAVE MAX OF 150 CHARS

+ 10 - 0
apps/main/templates/intercooler/playlist_tags.html

@@ -0,0 +1,10 @@
+{% for tag in playlist_tags %}
+<span id="tag-{{ tag.name|slugify }}">
+    <span class="badge rounded-pill bg-info mb-lg-1">
+        {{ tag.name }}
+        <a hx-post="{% url 'remove_playlist_tag' playlist_id tag.name %}" hx-trigger="click" hx-target="#tag-{{ tag.name|slugify }}">
+            <i class="fas fa-times-circle"></i>
+        </a>
+    </span>
+</span>
+{% endfor %}

+ 3 - 0
apps/main/templates/intercooler/playlist_tags_unused.html

@@ -0,0 +1,3 @@
+{% for tag in unused_tags %}
+  <option value="{{ tag.name }}">{{ tag.name }}</option>
+{% endfor %}

+ 4 - 2
apps/main/templates/intercooler/videos.html

@@ -4,8 +4,10 @@
 {% endif %}
 <div class="list-group" id="video-checkboxes">
     {% if videos %}
-      {% for video in videos %}
-                <li class="list-group-item d-flex justify-content-between align-items-center bg-transparent" style="background-color: #40B3A2">
+      {% for video in videos|slice:"0:50" %}
+                <li {% if forloop.last %}hx-get="{% url 'load_more_videos' playlist.playlist_id page|default:"1" %}"
+    hx-trigger="revealed"
+    hx-swap="afterend" hx-indicator="#load-more-videos-spinner"{% endif %} class="list-group-item d-flex justify-content-between align-items-center bg-transparent" style="background-color: #40B3A2">
 
             {% if video.is_unavailable_on_yt and not video.was_deleted_on_yt %}
 

+ 10 - 0
apps/main/templates/manage_playlists.html

@@ -38,6 +38,16 @@
         </div>
         </a>
     </div>
+    <div id="manage-add-videos-btn" class="col">
+        <a  hx-get="{% url 'manage_view_page' 'add-videos-to-playlists' %}" hx-trigger="click" hx-target="#manage-pl-div" class="text-decoration-none text-white">
+        <div class="card" style="background-color: #a75e27;">
+            <div class="card-body">
+                <h4 class="card-title">Add Videos to Existing Playlists</h4>
+                <p class="card-text">Search for YouTube videos in your UnTube collection or just input YouTube video links to add new videos to your existing YouTube playlists.</p>
+            </div>
+        </div>
+        </a>
+    </div>
     <div id="manage-delete-playlists-btn" class="col">
         <a  hx-get="{% url 'manage_view_page' 'nuke-playlists' %}" hx-trigger="click" hx-target="#manage-pl-div" class="text-decoration-none text-white">
         <div class="card" style="background-color: #a72744;">

+ 61 - 5
apps/main/templates/view_playlist.html

@@ -24,7 +24,7 @@
     <div class="list-group-item list-group-item-action active">
         <div class="d-flex w-100 justify-content-between">
             <span>
-                <h2 class="mb-1">{{ playlist.name }} <small>{% if playlist.user_label %}<span class="h3" style="border-bottom: 3px #ffffff dashed;">a.k.a {{ playlist.user_label }}</span>{% endif %}</small>
+                <h2 class="mb-1"><a href="https://www.youtube.com/playlist?list={{ playlist.playlist_id }}" target="_blank" style="color: white; text-decoration: none">{{ playlist.name }}</a> <small>{% if playlist.user_label %}<span class="h3" style="border-bottom: 3px #ffffff dashed;">a.k.a {{ playlist.user_label }}</span>{% endif %}</small>
                     <small>
 
                         <input class="form-control me-1 visually-hidden" id="pl-{{ playlist.playlist_id }}" value="{{ playlist.playlist_id }}">
@@ -49,7 +49,7 @@
             <h5>No description</h5>
             {% endif %}
         </p>
-        <h6 class="">
+        <h6 class="h6 text-uppercase">
             <span class="badge bg-light text-black-50">{% if playlist.is_user_owned %}OWNED{% else %}IMPORTED{% endif %}</span>
 
             <span class="badge bg-light text-black-50">{{ playlist.video_count }} VIDEOS</span>
@@ -62,8 +62,59 @@
                     <span class="badge bg-light text-black-50">DUPLICATE VIDEOS</span>
             {% endif %}
         </h6>
+        <h6 class="h6 pt-1">
+
+            Tags:
+            <span class="text-uppercase">
+                <span id="playlist-tags">
+                {% for tag in playlist_tags %}
+                    <span id="tag-{{ tag.name|slugify }}">
+                    <span class="badge rounded-pill bg-info mb-lg-1">{{ tag.name }} <a hx-post="{% url 'remove_playlist_tag' playlist.playlist_id tag.name %}" hx-trigger="click" hx-target="#tag-{{ tag.name|slugify }}"><i class="fas fa-times-circle"></i></a></span>
+                    </span>
+                {% endfor %}
+                </span>
+            <a data-bs-toggle="collapse" href="#addTagsCollapse" role="button" aria-expanded="false" aria-controls="addTagsCollapse">
+              <span class="badge rounded-pill bg-warning mb-lg-2{% if not playlist_tags %} ms-1{% endif %}"><i class="fas fa-plus"></i>{% if playlist_tags.count == 0 %} add a tag{% endif %}</span>
+            </a>
+            </span>
+            <div class="collapse" id="addTagsCollapse">
+                <div class="card card-body bg-dark text-white">
+
+      <h5>Add a tag to this playlist</h5>
+
+        {% if unused_tags %}
+      <div class="d-flex justify-content-start mt-2">
+              <select class="form-select w-50 bg-dark text-white border border-secondary" name="playlistTag" hx-get="{% url 'get_unused_playlist_tags' playlist.playlist_id %}" hx-trigger="click" hx-target="#unused-playlist-tags">
+                  <option selected>Pick from existing unused tags</option>
+                    <span id="unused-playlist-tags">
+                        {% for tag in unused_tags %}
+                          <option value="{{ tag.name }}">{{ tag.name }}</option>
+                        {% endfor %}
+                    </span>
+              </select>
+            <div class="btn-group ms-2">
+                <button type="button" class="btn btn-warning" hx-post="{% url 'add_playlist_tag' playlist.playlist_id %}" hx-trigger="click" hx-include="[name='playlistTag']" hx-target="this">Add Tag</button>
+            </div>
+      </div>
+    <div class="d-flex justify-content-start mt-3">
+          - OR -
+      </div>
+        {% endif %}
+
+      <div class="d-flex justify-content-start mt-2">
+           <input class="form-control w-50 bg-dark text-white border border-secondary" placeholder="Enter a new tag name here" name="createTagField">
+          <div class="btn-group ms-2">
+            <button type="button" class="btn btn-warning" hx-post="{% url 'create_playlist_tag' playlist.playlist_id %}" hx-trigger="click" hx-include="[name='createTagField']" hx-target="this">Create & Add Tag</button>
+          </div>
+      </div>
+
+    </div>
+
+            </div>
+        </h6>
     </div>
 
+
     <br>
 
     <div id="row1">
@@ -264,9 +315,11 @@
         <br>
         {% if videos %}
         <div class="list-group" id="video-checkboxes">
-          {% for video in videos %}
+          {% for video in videos|slice:"0:50" %}
 
-            <li class="list-group-item d-flex justify-content-between align-items-center bg-transparent" style="background-color: #40B3A2">
+            <li {% if forloop.last %}hx-get="{% url 'load_more_videos' playlist.playlist_id page|default:"1" %}"
+    hx-trigger="revealed"
+    hx-swap="afterend" hx-indicator="#load-more-videos-spinner" {% endif %} class="list-group-item d-flex justify-content-between align-items-center bg-transparent" style="background-color: #40B3A2">
 
         {% if video.is_unavailable_on_yt and not video.was_deleted_on_yt %}
 
@@ -384,9 +437,12 @@
             </div>
 
         {% endif %}
+
     </div>
     {% endif %}
-
+    <div class="d-flex justify-content-center">
+        <img id="load-more-videos-spinner" class="htmx-indicator mt-4" src="{% static 'svg-loaders/spinning-circles.svg' %}" width="40" height="40">
+    </div>
 
     </div>
 

+ 6 - 0
apps/main/urls.py

@@ -26,6 +26,12 @@ urlpatterns = [
          name='mark_playlist_as'),
     path("playlist/<slug:playlist_id>/update/<slug:type>", views.update_playlist, name="update_playlist"),
     path("playlist/<slug:playlist_id>/update-settings", views.update_playlist_settings, name="update_playlist_settings"),
+    path("playlist/<slug:playlist_id>/load-more-videos/<int:page>", views.load_more_videos, name="load_more_videos"),
+    path("playlist/<slug:playlist_id>/create-tag", views.create_playlist_tag, name="create_playlist_tag"),
+    path("playlist/<slug:playlist_id>/add-tag", views.add_playlist_tag, name="add_playlist_tag"),
+    path("playlist/<slug:playlist_id>/remove-tag/<str:tag_name>", views.remove_playlist_tag, name="remove_playlist_tag"),
+    path("playlist/<slug:playlist_id>/get-tags", views.get_playlist_tags, name="get_playlist_tags"),
+    path("playlist/<slug:playlist_id>/get-unused-tags", views.get_unused_playlist_tags, name="get_unused_playlist_tags"),
 
     ### STUFF RELATED TO PLAYLISTS IN BULK
     path("search/playlists/<slug:playlist_type>", views.search_playlists, name="search_playlists"),

+ 139 - 20
apps/main/views.py

@@ -2,9 +2,9 @@ import datetime
 
 import pytz
 from django.db.models import Q
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseRedirect
 from django.shortcuts import render, redirect
-from apps.main.models import Playlist
+from apps.main.models import Playlist, Tag
 from django.contrib.auth.decorators import login_required  # redirects user to settings.LOGIN_URL
 from allauth.socialaccount.models import SocialToken
 from django.views.decorators.http import require_POST
@@ -132,7 +132,14 @@ def view_playlist(request, playlist_id):
 
     videos = playlist.videos.order_by("video_position")
 
+    user_created_tags = Tag.objects.filter(created_by=request.user)
+    playlist_tags = playlist.tags.all()
+
+    unused_tags = user_created_tags.difference(playlist_tags)
+
     return render(request, 'view_playlist.html', {"playlist": playlist,
+                                                  "playlist_tags": playlist_tags,
+                                                  "unused_tags": unused_tags,
                                                   "videos": videos})
 
 
@@ -313,9 +320,9 @@ def delete_videos(request, playlist_id, command):
             f'<div class="spinner-border text-light" role="status" hx-post="/from/{playlist_id}/delete-videos/start" hx-trigger="load" hx-swap="outerHTML"></div>')
     elif command == "start":
         Playlist.objects.deletePlaylistItems(request.user, playlist_id, video_ids)
-        #playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
-        #playlist.has_playlist_changed = True
-        #playlist.save(update_fields=['has_playlist_changed'])
+        # playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+        # playlist.has_playlist_changed = True
+        # playlist.save(update_fields=['has_playlist_changed'])
         return HttpResponse(f"""
         <div hx-get="/playlist/{playlist_id}/update/checkforupdates" hx-trigger="load delay:4s" hx-target="#checkforupdates" class="sticky-top" style="top: 0.5rem;">
 Done! Playlist on UnTube will update in 3s...
@@ -524,10 +531,22 @@ def manage_create_playlist(request):
     return HttpResponse("")
 
 
+@login_required
+def load_more_videos(request, playlist_id, page):
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+    videos = playlist.videos.order_by("video_position")[50 * page:]
+
+    return HttpResponse(loader.get_template("intercooler/videos.html")
+        .render(
+        {
+            "playlist": playlist,
+            "videos": videos,
+            "page": page + 1}))
+
+
 @login_required
 @require_POST
 def update_playlist_settings(request, playlist_id):
-
     message_type = "success"
     message_content = "Saved!"
 
@@ -571,19 +590,19 @@ def update_playlist(request, playlist_id, type):
 
             print("CHANGES", deleted_videos, unavailable_videos, added_videos)
 
-            #playlist_changed_text = ["The following modifications happened to this playlist on YouTube:"]
+            # playlist_changed_text = ["The following modifications happened to this playlist on YouTube:"]
             if deleted_videos != 0 or unavailable_videos != 0 or added_videos != 0:
                 pass
-                #if added_videos > 0:
+                # if added_videos > 0:
                 #    playlist_changed_text.append(f"{added_videos} new video(s) were added")
-                #if deleted_videos > 0:
+                # if deleted_videos > 0:
                 #    playlist_changed_text.append(f"{deleted_videos} video(s) were deleted")
-                #if unavailable_videos > 0:
+                # if unavailable_videos > 0:
                 #    playlist_changed_text.append(f"{unavailable_videos} video(s) went private/unavailable")
 
-                #playlist.playlist_changed_text = "\n".join(playlist_changed_text)
-                #playlist.has_playlist_changed = True
-                #playlist.save()
+                # playlist.playlist_changed_text = "\n".join(playlist_changed_text)
+                # playlist.has_playlist_changed = True
+                # playlist.save()
             else:  # no updates found
                 return HttpResponse("""
                 <div id="checkforupdates" class="sticky-top" style="top: 0.5em;">
@@ -596,15 +615,15 @@ def update_playlist(request, playlist_id, type):
         elif result[0] == -1:  # playlist changed
             print("!!!Playlist changed")
 
-            #current_playlist_vid_count = playlist.video_count
-            #new_playlist_vid_count = result[1]
+            # current_playlist_vid_count = playlist.video_count
+            # new_playlist_vid_count = result[1]
 
-            #print(current_playlist_vid_count)
-            #print(new_playlist_vid_count)
+            # print(current_playlist_vid_count)
+            # print(new_playlist_vid_count)
 
             # playlist.has_playlist_changed = True
-            #playlist.save()
-            #print(playlist.playlist_changed_text)
+            # playlist.save()
+            # print(playlist.playlist_changed_text)
         else:  # no updates found
             return HttpResponse("""
             <div id="checkforupdates" class="sticky-top" style="top: 0.5em;">
@@ -691,8 +710,108 @@ def update_playlist(request, playlist_id, type):
         {"playlist_changed_text": "\n".join(playlist_changed_text),
          "playlist_id": playlist_id}))
 
-
+@login_required
 def view_playlist_settings(request, playlist_id):
     playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
 
     return render(request, 'view_playlist_settings.html', {"playlist": playlist})
+
+
+def get_playlist_tags(request, playlist_id):
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+    playlist_tags = playlist.tags.all()
+
+    return HttpResponse(loader.get_template("intercooler/playlist_tags.html")
+        .render(
+        {"playlist_id": playlist_id,
+            "playlist_tags": playlist_tags}))
+
+
+def get_unused_playlist_tags(request, playlist_id):
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+
+    user_created_tags = Tag.objects.filter(created_by=request.user)
+    playlist_tags = playlist.tags.all()
+
+    unused_tags = user_created_tags.difference(playlist_tags)
+
+    return HttpResponse(loader.get_template("intercooler/playlist_tags_unused.html")
+        .render(
+        {"unused_tags": unused_tags}))
+
+@login_required
+@require_POST
+def create_playlist_tag(request, playlist_id):
+    tag_name = request.POST["createTagField"]
+
+    if tag_name == 'Pick from existing unused tags':
+        return HttpResponse("Can't use that! Try again >_<")
+
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+
+    user_created_tags = Tag.objects.filter(created_by=request.user)
+    if user_created_tags.filter(name__iexact=tag_name).count() == 0:  # no tag found, so create it
+        tag = Tag(name=tag_name, created_by=request.user)
+        tag.save()
+
+        # add it to playlist
+        playlist.tags.add(tag)
+
+    else:
+        return HttpResponse("""
+                            Already created. Try Again >w<
+                    """)
+
+    #playlist_tags = playlist.tags.all()
+
+    #unused_tags = user_created_tags.difference(playlist_tags)
+
+    return HttpResponse(f"""
+            Created and Added!
+              <span class="visually-hidden" hx-get="/playlist/{playlist_id}/get-tags" hx-trigger="load" hx-target="#playlist-tags"></span>
+    """)
+
+
+@login_required
+@require_POST
+def add_playlist_tag(request, playlist_id):
+
+    tag_name = request.POST["playlistTag"]
+
+    if tag_name == 'Pick from existing unused tags':
+        return HttpResponse("Pick something! >w<")
+
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+
+    playlist_tags = playlist.tags.all()
+    if playlist_tags.filter(name__iexact=tag_name).count() == 0:  # tag not on this playlist, so add it
+        tag = Tag.objects.filter(Q(created_by=request.user) & Q(name__iexact=tag_name)).first()
+
+        # add it to playlist
+        playlist.tags.add(tag)
+    else:
+        return HttpResponse("Already Added >w<")
+
+    return HttpResponse(f"""
+                Added!
+                  <span class="visually-hidden" hx-get="/playlist/{playlist_id}/get-tags" hx-trigger="load" hx-target="#playlist-tags"></span>
+        """)
+
+
+@login_required
+@require_POST
+def remove_playlist_tag(request, playlist_id, tag_name):
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+
+    playlist_tags = playlist.tags.all()
+    if playlist_tags.filter(name__iexact=tag_name).count() != 0:  # tag on this playlist, remove it it
+        tag = Tag.objects.filter(Q(created_by=request.user) & Q(name__iexact=tag_name)).first()
+
+        print("Removed tag", tag_name)
+        # remove it from the playlist
+        playlist.tags.remove(tag)
+    else:
+        return HttpResponse("Whoops >w<")
+
+    return HttpResponse("")
+