Przeglądaj źródła

added create new playlist with selected videos feature and created a manage_playlists app

sleepytaco 3 lat temu
rodzic
commit
23666a8600

+ 1 - 0
UnTube/settings.py

@@ -51,6 +51,7 @@ INSTALLED_APPS = [
     'crispy_forms',
     'apps.users',  # has stuff related to user management in it (login, signup, show homepage, import)
     'apps.main',  # main app, shows user their homepage
+    'apps.manage_playlists',
     'apps.charts',
     'apps.search',
 ]

+ 2 - 2
UnTube/urls.py

@@ -20,7 +20,7 @@ urlpatterns = [
     path('admin/', admin.site.urls),
     path('', include("apps.users.urls")),
     path('', include("apps.main.urls")),
+    path('manage/', include("apps.manage_playlists.urls")),
+    path('search/', include("apps.search.urls")),
     path('charts/', include("apps.charts.urls")),
-    path('search/', include("apps.search.urls"))
-
 ]

+ 46 - 8
apps/main/models.py

@@ -552,7 +552,8 @@ class PlaylistManager(models.Manager):
 
             return [1, deleted_videos, unavailable_videos, added_videos]
         else:
-            print("YOU CAN DO A FULL SCAN AGAIN IN", str(datetime.datetime.now(pytz.utc) - (playlist.last_full_scan_at + datetime.timedelta(minutes=1))))
+            print("YOU CAN DO A FULL SCAN AGAIN IN",
+                  str(datetime.datetime.now(pytz.utc) - (playlist.last_full_scan_at + datetime.timedelta(minutes=1))))
         """
         print("DOING A SMOL SCAN")
 
@@ -1028,6 +1029,40 @@ class PlaylistManager(models.Manager):
 
         self.deletePlaylistItems(user, playlist_id, playlist_item_ids)
 
+    def createNewPlaylist(self, user, playlist_name, playlist_description):
+        """
+        Takes in playlist details and creates a new private playlist in the user's account
+        """
+        credentials = self.getCredentials(user)
+        result = {
+            "status": 0,
+            "playlist_id": None
+        }
+        with build('youtube', 'v3', credentials=credentials) as youtube:
+            pl_request = youtube.playlists().insert(
+                part='snippet,status',
+                body={
+                    "snippet": {
+                        "title": playlist_name,
+                        "description": playlist_description,
+                        "defaultLanguage": "en"
+                    },
+                    "status": {
+                        "privacyStatus": "private"
+                    }
+                }
+            )
+            try:
+                pl_response = pl_request.execute()
+            except googleapiclient.errors.HttpError as e:  # failed to create playlist
+                print(e.status_code, e.error_details)
+                if e.status_code == 400:  # maxPlaylistExceeded
+                    result["status"] = 400
+                result["status"] = -1
+            result["playlist_id"] = pl_response["id"]
+
+        return result
+
     def updatePlaylistDetails(self, user, playlist_id, details):
         """
         Takes in playlist itemids for the videos in a particular playlist
@@ -1147,8 +1182,6 @@ class PlaylistManager(models.Manager):
         """
         credentials = self.getCredentials(user)
 
-        playlist = user.playlists.get(playlist_id=playlist_id)
-
         result = {
             "num_added": 0,
             "playlistContainsMaximumNumberOfVideos": False,
@@ -1198,9 +1231,14 @@ class PlaylistManager(models.Manager):
                     continue
                 added += 1
         result["num_added"] = added
-        if added > 0:
-            playlist.has_playlist_changed = True
-            playlist.save(update_fields=['has_playlist_changed'])
+
+        try:
+            playlist = user.playlists.get(playlist_id=playlist_id)
+            if added > 0:
+                playlist.has_playlist_changed = True
+                playlist.save(update_fields=['has_playlist_changed'])
+        except:
+            pass
         return result
 
 
@@ -1340,8 +1378,8 @@ class Playlist(models.Model):
     objects = PlaylistManager()
 
     # playlist settings (moved to global preferences)
-    #hide_unavailable_videos = models.BooleanField(default=False)
-    #confirm_before_deleting = models.BooleanField(default=True)
+    # hide_unavailable_videos = models.BooleanField(default=False)
+    # confirm_before_deleting = models.BooleanField(default=True)
     auto_check_for_updates = models.BooleanField(default=False)
 
     # for import

+ 1 - 1
apps/main/templates/home.html

@@ -289,7 +289,7 @@
 
         <br>
 
-        <div class="row text-dark mt-0 d-flex justify-content-evenly">
+        <div class="row text-dark mt-0 d-flex justify-content-evenly" id="recent-playlists">
             <div class="col">
 
                 <h3><span style="border-bottom: 3px #e24949 dashed;">Recently Added</span> <i class="fas fa-plus-square" style="color:#972727;"></i></h3>

+ 2 - 2
apps/main/templates/intercooler/playlists.html

@@ -4,7 +4,7 @@
         <div class="col">
 
             <div class="card overflow-auto" style="background-color: {{ bg_color|default:"#357779" }};">
-                <img  class="bd-placeholder-img card-img-top" src="{{ playlist.thumbnail_url }}" style="max-width:100%; height: 200px;   object-fit: cover;" alt="{{ playlist.name }} thumbnail">
+                <img  class="bd-placeholder-img card-img-top" src="{% if playlist.playlist_items.all.count > 0 %}{{ playlist.playlist_items.all.0.video.thumbnail_url }}{% else %}https://i.ytimg.com/vi/9219YrnwDXE/maxresdefault.jpg{% endif %}" style="max-width:100%; height: 200px;   object-fit: cover;" alt="{{ playlist.name }} thumbnail">
 
                 <div class="card-body">
                     <h5 class="card-title"><a href="{% url 'playlist' playlist.playlist_id %}" class="stretched-link" style="text-decoration: none; color: white">{{ playlist.name }}</a></h5>
@@ -23,7 +23,7 @@
         <div class="col">
             <div class="card overflow-auto" style="background-color: {{ bg_color|default:"#357779" }};">
                 <a href="{% url 'playlist' playlist.playlist_id %}" style="text-decoration: none; color: black">
-                    <img  class="bd-placeholder-img card-img-top" src="{% if playlist.thumbnail_url %}{{ playlist.thumbnail_url }}{% else %}https://i.ytimg.com/vi/9219YrnwDXE/maxresdefault.jpg{% endif %}" style="max-width:100%; height: 200px;   object-fit: cover;" alt="{{ playlist.name }} thumbnail">
+                    <img  class="bd-placeholder-img card-img-top" src="{% if playlist.playlist_items.all.count > 0 %}{{ playlist.playlist_items.all.0.video.thumbnail_url }}{% else %}https://i.ytimg.com/vi/9219YrnwDXE/maxresdefault.jpg{% endif %}" style="max-width:100%; height: 200px;   object-fit: cover;" alt="{{ playlist.name }} thumbnail">
                 </a>
                 <div class="card-body">
                     <h5 class="card-title">

+ 19 - 12
apps/main/templates/view_playlist.html

@@ -447,14 +447,19 @@
                             <div id="delete-videos-confirm-box">
                                 <h5>You can select videos by clicking any where on the video box. Selected videos will be highlighted red.</h5>
                                 {% if not user.profile.confirm_before_deleting %}
-                                    <h5>Note: You have set confirm before deleting to False. Buttons below will take effect immediately when clicked.</h5>
+
+                                    <h5>
+                                    <i class="fas fa-exclamation-triangle" style="color: yellow"></i>
+                                        You have set confirm before deleting to False. Buttons below will take effect immediately when clicked.
+                                    <i class="fas fa-exclamation-triangle" style="color: yellow"></i>
+                                    </h5>
                                     <hr>
                                 {% else %}
                                     {% if playlist.has_unavailable_videos %}
-                                        <h5>Note: Clicking on delete unavailable videos button will take immediate effect. All videos labelled unavailable in this playlist will be deleted from your YouTube playlist.</h5>
+                                        <h5><i class="fas fa-exclamation-triangle" style="color: yellow"></i> Clicking on delete unavailable videos button will take immediate effect. All videos labelled unavailable in this playlist will be deleted from your YouTube playlist.</h5>
                                     {% endif %}
                                     {% if playlist.has_duplicate_videos %}
-                                        <h5>Note: Clicking on delete duplicate videos button will take immediate effect. All videos labelled duplicate in this playlist will be deleted from your YouTube playlist.</h5>
+                                        <h5><i class="fas fa-exclamation-triangle" style="color: yellow"></i> Clicking on delete duplicate videos button will take immediate effect. All videos labelled duplicate in this playlist will be deleted from your YouTube playlist.</h5>
                                     {% endif %}
                                     <hr>
                                 {% endif %}
@@ -550,10 +555,8 @@
                                 <div class="col-md-8 text-dark">
                                     <div id="create-playlist-from">
                                         <input type="text" name="playlist-name" class="form-control" placeholder="Enter new playlist name">
-                                        <textarea name="create-playlist-textarea" class="form-control mt-2" id="create-playlist-text-area" placeholder="Enter playlist description here" rows="5"
-                                            hx-post="{% url 'manage_save' 'manage_playlists_import_textarea' %}"
-                                            hx-trigger="keyup changed delay:500ms"
-                                            hx-indicator="#spinner">{{ manage_playlists_import_textarea }}</textarea>
+                                        <textarea name="playlist-description" class="form-control mt-2" placeholder="Enter playlist description here"
+                                                  rows="5">{{ manage_playlists_import_textarea }}</textarea>
                                     </div>
 
                                 </div>
@@ -563,11 +566,11 @@
                             <div class="d-flex justify-content-start mt-2">
 
                                 <div class="btn-group ms-1 mt-1">
-                                    <button hx-indicator="#create-playlist-loader" hx-post="{% url 'playlist_move_copy_videos' playlist.playlist_id 'copy' %}" hx-include="#playlists-to-move-to, #video-checkboxes" hx-target="#move-copy-videos-box" type="button" class="btn btn-primary">
+                                    <button hx-indicator="#create-playlist-loader" hx-post="{% url 'playlist_create_new_playlist' playlist.playlist_id %}" hx-include="#create-playlist-from, #video-checkboxes" hx-target="#create-playlist-box" type="button" class="btn btn-primary">
                                         Create
                                     </button>
                                 </div>
-                                <div id="add-videos-box" class="d-flex align-items-end ms-2">
+                                <div id="create-playlist-box" class="d-flex align-items-end ms-2 text-warning">
                                 </div>
                                 <div class="htmx-indicator d-flex align-items-center" id="create-playlist-loader">
                                     <img src="{% static 'svg-loaders/grid.svg' %}" width="25" height="25" class="ms-2 mt-2">
@@ -719,7 +722,7 @@
                                     Playlist is empty ;-;
                                 </div>
                                 <div class="d-flex justify-content-center align-content-center">
-                                    Consider moving/copying videos from other playlists into this playlist by copying and using this playlist's ID.
+                                    Consider moving/copying videos from other playlists into this playlist.
                                 </div>
                                 <div class="d-flex justify-content-center align-content-center">
                                     - OR -
@@ -901,11 +904,15 @@
             document.getElementById("delete-videos-confirm-box").innerHTML = `
                 <h5>You can select videos by clicking any where on the video box. Selected videos will be highlighted red.</h5>
                 {% if not user.profile.confirm_before_deleting %}
-                    <h5>Note: You have set confirm before deleting to False. Buttons below will take effect immediately when clicked.</h5>
+                    <h5>
+                    <i class="fas fa-exclamation-triangle" style="color: yellow"></i>
+                    Note: You have set confirm before deleting to False. Buttons below will take effect immediately when clicked.
+                    <i class="fas fa-exclamation-triangle" style="color: yellow"></i>
+                    </h5>
                     <hr>
                 {% else %}
                     {% if playlist.has_unavailable_videos %}
-                        <h5>Note: Clicking on delete unavailable videos button will take immediate effect. All videos labelled unavailable in this playlist will be deleted from your YouTube playlist.</h5>
+                        <h5><i class="fas fa-exclamation-triangle" style="color: yellow"></i> Clicking on delete unavailable videos button will take immediate effect. All videos labelled unavailable in this playlist will be deleted from your YouTube playlist.</h5>
                     {% endif %}
                     {% if playlist.has_duplicate_videos %}
                         <h5>Note: Clicking on delete duplicate videos button will take immediate effect. All videos labelled duplicate in this playlist will be deleted from your YouTube playlist.</h5>

+ 2 - 7
apps/main/urls.py

@@ -48,15 +48,10 @@ urlpatterns = [
          name="playlist_completion_times"),
     path("playlist/<slug:playlist_id>/add-new-videos", views.playlist_add_new_videos,
          name="playlist_add_new_videos"),
+    path("playlist/<slug:playlist_id>/create-new-playlist", views.playlist_create_new_playlist,
+             name="playlist_create_new_playlist"),
 
     ### STUFF RELATED TO PLAYLISTS IN BULK
     path("playlists/<slug:playlist_type>/order-by/<slug:order_by>", views.order_playlists_by, name='order_playlists_by'),
     path("playlists/tag/<str:tag>", views.tagged_playlists, name='tagged_playlists'),
-
-    ### STUFF RELATED TO MANAGING A PLAYLIST
-    path("manage", views.manage_playlists, name='manage_playlists'),
-    path("manage/save/<slug:what>", views.manage_save, name='manage_save'),  # to help auto save the input texts found in the below pages
-    path("manage/view/<slug:page>", views.manage_view_page, name='manage_view_page'),  # views the import pl, create pl, create untube pl pages
-    path("manage/import", views.manage_import_playlists, name="manage_import_playlists"),
-    path("manage/create", views.manage_create_playlist, name="manage_create_playlist")
 ]

+ 39 - 84
apps/main/views.py

@@ -539,89 +539,6 @@ def mark_video_watched(request, playlist_id, video_id):
 ###########
 
 
-@login_required
-def manage_playlists(request):
-    return render(request, "manage_playlists.html")
-
-
-@login_required
-def manage_view_page(request, page):
-    if page == "import":
-        return render(request, "manage_playlists_import.html",
-                      {"manage_playlists_import_textarea": request.user.profile.manage_playlists_import_textarea})
-    elif page == "create":
-        return render(request, "manage_playlists_create.html")
-    else:
-        return HttpResponse('Working on this!')
-
-
-@login_required
-@require_POST
-def manage_save(request, what):
-    if what == "manage_playlists_import_textarea":
-        request.user.profile.manage_playlists_import_textarea = request.POST["import-playlist-textarea"]
-        request.user.save()
-
-    return HttpResponse("")
-
-
-@login_required
-@require_POST
-def manage_import_playlists(request):
-    playlist_links = request.POST["import-playlist-textarea"].replace(",", "").split("\n")
-
-    num_playlists_already_in_db = 0
-    num_playlists_initialized_in_db = 0
-    num_playlists_not_found = 0
-    new_playlists = []
-    old_playlists = []
-    not_found_playlists = []
-
-    done = []
-    for playlist_link in playlist_links:
-        if playlist_link.strip() != "" and playlist_link.strip() not in done:
-            pl_id = Playlist.objects.getPlaylistId(playlist_link.strip())
-            if pl_id is None:
-                num_playlists_not_found += 1
-                continue
-
-            status = Playlist.objects.initializePlaylist(request.user, pl_id)["status"]
-            if status == -1 or status == -2:
-                print("\nNo such playlist found:", pl_id)
-                num_playlists_not_found += 1
-                not_found_playlists.append(playlist_link)
-            elif status == -3:  # playlist already in db
-                num_playlists_already_in_db += 1
-                playlist = request.user.playlists.get(playlist_id__exact=pl_id)
-                old_playlists.append(playlist)
-            else:  # only if playlist exists on YT, so import its videos
-                print(status)
-                Playlist.objects.getAllVideosForPlaylist(request.user, pl_id)
-                playlist = request.user.playlists.get(playlist_id__exact=pl_id)
-                new_playlists.append(playlist)
-                num_playlists_initialized_in_db += 1
-            done.append(playlist_link.strip())
-
-    request.user.profile.manage_playlists_import_textarea = ""
-    request.user.save()
-
-    return HttpResponse(loader.get_template("intercooler/manage_playlists_import_results.html")
-        .render(
-        {"new_playlists": new_playlists,
-         "old_playlists": old_playlists,
-         "not_found_playlists": not_found_playlists,
-         "num_playlists_already_in_db": num_playlists_already_in_db,
-         "num_playlists_initialized_in_db": num_playlists_initialized_in_db,
-         "num_playlists_not_found": num_playlists_not_found
-         }))
-
-
-@login_required
-@require_POST
-def manage_create_playlist(request):
-    print(request.POST)
-    return HttpResponse("")
-
 
 @login_required
 def load_more_videos(request, playlist_id, order_by, page):
@@ -1173,4 +1090,42 @@ def playlist_add_new_videos(request, playlist_id):
             <script>
             window.location.reload();
             </script>
-    """)
+    """)
+
+@login_required
+@require_POST
+def playlist_create_new_playlist(request, playlist_id):
+    playlist_name = bleach.clean(request.POST["playlist-name"].strip())
+    playlist_description = bleach.clean(request.POST["playlist-description"])
+    if playlist_name == "":
+        return HttpResponse("Enter a playlist name first!")
+
+    unclean_playlist_item_ids = request.POST.getlist("video-id", default=[])
+    clean_playlist_item_ids = [bleach.clean(playlist_item_id) for playlist_item_id in unclean_playlist_item_ids]
+    playlist_items = request.user.playlists.get(playlist_id=playlist_id).playlist_items.filter(playlist_item_id__in=clean_playlist_item_ids)
+
+    if not playlist_items.exists():
+        return HttpResponse("Select some videos first!")
+    else:
+        result = Playlist.objects.createNewPlaylist(request.user, playlist_name, playlist_description)
+        if result["status"] == 0:  # playlist created on youtube
+            new_playlist_id = result["playlist_id"]
+        elif result["status"] == -1:
+            return HttpResponse("Error creating playlist!")
+        elif result["status"] == 400:
+            return HttpResponse("Max playlists limit reached!")
+
+    video_ids = []
+    for playlist_item in playlist_items:
+        video_ids.append(playlist_item.video.video_id)
+
+    result = Playlist.objects.addVideosToPlaylist(request.user, new_playlist_id, video_ids)
+
+    added = result["num_added"]
+    max_limit_reached = result["playlistContainsMaximumNumberOfVideos"]
+    if max_limit_reached:
+        message = f"Only added the first {added} video link(s) to the new playlist as the max playlist limit has been reached :("
+    else:
+        message = f"""Successfully created '{playlist_name}' and added {added} videos to it. Visit the <a href="/home/" target="_blank" style="text-decoration: none; color: white" class="ms-1 me-1">dashboard</a> to import it into UnTube."""
+
+    return HttpResponse(message)

+ 0 - 0
apps/manage_playlists/__init__.py


+ 3 - 0
apps/manage_playlists/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
apps/manage_playlists/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ManagePlaylistsConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.manage_playlists'

+ 0 - 0
apps/manage_playlists/migrations/__init__.py


+ 3 - 0
apps/manage_playlists/models.py

@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.

+ 3 - 0
apps/manage_playlists/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 13 - 0
apps/manage_playlists/urls.py

@@ -0,0 +1,13 @@
+from django.conf.urls import url
+from django.urls import path
+from . import views
+
+urlpatterns = [
+    ### STUFF RELATED TO MANAGING A PLAYLIST
+    path("", views.manage_playlists, name='manage_playlists'),
+    path("save/<slug:what>", views.manage_save, name='manage_save'),  # to help auto save the input texts found in the below pages
+    path("view/<slug:page>", views.manage_view_page, name='manage_view_page'),  # views the import pl, create pl, create untube pl pages
+    path("import", views.manage_import_playlists, name="manage_import_playlists"),
+    path("create", views.manage_create_playlist, name="manage_create_playlist"),
+    path("nuke", views.manage_nuke_playlists, name="manage_nuke_playlists"),
+]

+ 97 - 0
apps/manage_playlists/views.py

@@ -0,0 +1,97 @@
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse
+from django.shortcuts import render
+from django.template import loader
+from django.views.decorators.http import require_POST
+from apps.main.models import Playlist
+
+
+@login_required
+def manage_playlists(request):
+    return render(request, "manage_playlists.html")
+
+
+@login_required
+def manage_view_page(request, page):
+    if page == "import":
+        return render(request, "manage_playlists_import.html",
+                      {"manage_playlists_import_textarea": request.user.profile.manage_playlists_import_textarea})
+    elif page == "create":
+        return render(request, "manage_playlists_create.html")
+    else:
+        return HttpResponse('Working on this!')
+
+
+@login_required
+@require_POST
+def manage_save(request, what):
+    if what == "manage_playlists_import_textarea":
+        request.user.profile.manage_playlists_import_textarea = request.POST["import-playlist-textarea"]
+        request.user.save()
+
+    return HttpResponse("")
+
+
+@login_required
+@require_POST
+def manage_import_playlists(request):
+    playlist_links = request.POST["import-playlist-textarea"].replace(",", "").split("\n")
+
+    num_playlists_already_in_db = 0
+    num_playlists_initialized_in_db = 0
+    num_playlists_not_found = 0
+    new_playlists = []
+    old_playlists = []
+    not_found_playlists = []
+
+    done = []
+    for playlist_link in playlist_links:
+        if playlist_link.strip() != "" and playlist_link.strip() not in done:
+            pl_id = Playlist.objects.getPlaylistId(playlist_link.strip())
+            if pl_id is None:
+                num_playlists_not_found += 1
+                continue
+
+            status = Playlist.objects.initializePlaylist(request.user, pl_id)["status"]
+            if status == -1 or status == -2:
+                print("\nNo such playlist found:", pl_id)
+                num_playlists_not_found += 1
+                not_found_playlists.append(playlist_link)
+            elif status == -3:  # playlist already in db
+                num_playlists_already_in_db += 1
+                playlist = request.user.playlists.get(playlist_id__exact=pl_id)
+                old_playlists.append(playlist)
+            else:  # only if playlist exists on YT, so import its videos
+                print(status)
+                Playlist.objects.getAllVideosForPlaylist(request.user, pl_id)
+                playlist = request.user.playlists.get(playlist_id__exact=pl_id)
+                new_playlists.append(playlist)
+                num_playlists_initialized_in_db += 1
+            done.append(playlist_link.strip())
+
+    request.user.profile.manage_playlists_import_textarea = ""
+    request.user.save()
+
+    return HttpResponse(loader.get_template("intercooler/manage_playlists_import_results.html")
+        .render(
+        {"new_playlists": new_playlists,
+         "old_playlists": old_playlists,
+         "not_found_playlists": not_found_playlists,
+         "num_playlists_already_in_db": num_playlists_already_in_db,
+         "num_playlists_initialized_in_db": num_playlists_initialized_in_db,
+         "num_playlists_not_found": num_playlists_not_found
+         }))
+
+
+@login_required
+@require_POST
+def manage_create_playlist(request):
+    print(request.POST)
+    return HttpResponse("")
+
+
+@login_required
+@require_POST
+def manage_nuke_playlists(request):
+    print(request.POST)
+    return HttpResponse("")

+ 3 - 1
apps/users/views.py

@@ -348,7 +348,9 @@ def user_playlists_updates(request, action):
 
         return HttpResponse("""
         <div class="alert alert-success alert-dismissible fade show d-flex justify-content-center" role="alert">
-          <h4 class="">Successfully imported new playlists into UnTube! Refresh :)</h4>
+          <h4 class="">Successfully imported new playlists into UnTube!</h4>
+            <meta http-equiv="refresh" content="0;url=/home/#recent-playlists" />
+            <meta http-equiv="refresh" content="2;url=/home/" />
 
             <button type="button" class="btn-close" data-bs-dismiss="alert" aria-la bel="Close"></button>
         </div>