Explorar o código

implemented delete specific videos feature

sleepytaco %!s(int64=3) %!d(string=hai) anos
pai
achega
7fc0a78b9b

+ 22 - 0
apps/main/migrations/0042_auto_20210722_2040.py

@@ -0,0 +1,22 @@
+# Generated by Django 3.2.3 on 2021-07-23 01:40
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('main', '0041_video_liked'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='pin',
+            old_name='type',
+            new_name='kind',
+        ),
+        migrations.RemoveField(
+            model_name='playlist',
+            name='has_unavailable_videos',
+        ),
+    ]

+ 28 - 19
apps/main/models.py

@@ -286,7 +286,6 @@ class PlaylistManager(models.Manager):
                             item['snippet']['title'] == "Private video" or item['snippet'][
                         'description'] == "This video is private."):
                         video.was_deleted_on_yt = True
-                        playlist.has_unavailable_videos = True
                         video.save(update_fields=['was_deleted_on_yt'])
 
             while True:
@@ -379,7 +378,6 @@ class PlaylistManager(models.Manager):
                                     item['snippet']['title'] == "Private video" or item['snippet'][
                                 'description'] == "This video is private."):
                                 video.was_deleted_on_yt = True
-                                playlist.has_unavailable_videos = True
                                 video.save(update_fields=['was_deleted_on_yt'])
 
 
@@ -433,9 +431,6 @@ class PlaylistManager(models.Manager):
         playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
         playlist.playlist_duration = getHumanizedTimeString(playlist_duration_in_seconds)
 
-        if len(video_ids) != len(vid_durations):  # that means some videos in the playlist are deleted
-            playlist.has_unavailable_videos = True
-
         playlist.is_in_db = True
 
         playlist.save()
@@ -456,7 +451,7 @@ class PlaylistManager(models.Manager):
 
         # if its been a week since the last full scan, do a full playlist scan
         # basically checks all the playlist video for any updates
-        if playlist.last_full_scan_at + datetime.timedelta(seconds=2) < datetime.datetime.now(pytz.utc):
+        if playlist.last_full_scan_at + datetime.timedelta(minutes=1) < datetime.datetime.now(pytz.utc):
             print("DOING A FULL SCAN")
             current_video_ids = [playlist_item.video_id for playlist_item in playlist.playlist_items.all()]
             current_playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in
@@ -666,7 +661,6 @@ class PlaylistManager(models.Manager):
                             item['snippet']['title'] == "Private video" and item['snippet'][
                         'description'] == "This video is private."):
                         video.was_deleted_on_yt = True
-                        playlist.has_unavailable_videos = True
 
                     is_duplicate = False
                     if not playlist.videos.filter(video_id=video_id).exists():
@@ -771,7 +765,6 @@ class PlaylistManager(models.Manager):
                                     item['snippet']['title'] == "Private video" and item['snippet'][
                                 'description'] == "This video is private."):
                                 video.was_deleted_on_yt = True
-                                playlist.has_unavailable_videos = True
 
                             is_duplicate = False
                             if not playlist.videos.filter(video_id=video_id).exists():
@@ -852,7 +845,6 @@ class PlaylistManager(models.Manager):
                             'description'] == "This video is unavailable.") or (
                             item['snippet']['title'] == "Private video" or item['snippet'][
                         'description'] == "This video is private."):
-                        playlist.has_unavailable_videos = True
                         vid_durations.append(duration)
                         vid.video_details_modified = True
                         vid.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
@@ -885,10 +877,6 @@ class PlaylistManager(models.Manager):
         playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
         playlist.playlist_duration = getHumanizedTimeString(playlist_duration_in_seconds)
 
-        if len(video_ids) != len(vid_durations) or len(
-                unavailable_videos) != 0:  # that means some videos in the playlist became private/deleted
-            playlist.has_unavailable_videos = True
-
         playlist.has_playlist_changed = False
         playlist.video_count = updated_playlist_video_count
         playlist.has_new_updates = True
@@ -989,6 +977,20 @@ class PlaylistManager(models.Manager):
             playlist_item.save(update_fields=['video_position', 'is_duplicate'])
             counter += 1
 
+    def deleteSpecificPlaylistItems(self, user, playlist_id, command):
+        playlist = user.playlists.get(playlist_id=playlist_id)
+        playlist_items = []
+        if command == "duplicate":
+            playlist_items = playlist.playlist_items.filter(is_duplicate=True)
+        elif command == "unavailable":
+            playlist_items = playlist.playlist_items.filter(Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=False))
+
+        playlist_item_ids = []
+        for playlist_item in playlist_items:
+            playlist_item_ids.append(playlist_item.playlist_item_id)
+
+        self.deletePlaylistItems(user, playlist_id, playlist_item_ids)
+
     def updatePlaylistDetails(self, user, playlist_id, details):
         """
         Takes in playlist itemids for the videos in a particular playlist
@@ -1062,19 +1064,19 @@ class PlaylistManager(models.Manager):
                     except googleapiclient.errors.HttpError as e:  # failed to update playlist details
                         # possible causes:
                         # playlistItemsNotAccessible (403)
-                        # playlistItemNotFound (404)
+                        # playlistItemNotFound (404) - I ran into 404 while trying to copy an unavailable video into another playlist
                         # playlistOperationUnsupported (400)
                         # errors i ran into:
                         # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
-                        print("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
-                        return -1
+                        print("ERROR UPDATING PLAYLIST DETAILS", e.status_code, e.error_details)
+                        return [-1, e.status_code]
 
                     print(pl_response)
 
         if action == "move":  # delete from the current playlist
             self.deletePlaylistItems(user, from_playlist_id, playlist_item_ids)
 
-        return 0
+        return [0]
 
 
 
@@ -1192,7 +1194,6 @@ class Playlist(models.Model):
 
     playlist_duration = models.CharField(max_length=69, blank=True)  # string version of playlist dureation
     playlist_duration_in_seconds = models.IntegerField(default=0)
-    has_unavailable_videos = models.BooleanField(default=False)  # if videos in playlist are private/deleted
 
     # watch playlist details
     # watch_time_left = models.CharField(max_length=150, default="")
@@ -1230,6 +1231,11 @@ class Playlist(models.Model):
     def __str__(self):
         return str(self.playlist_id)
 
+    def has_unavailable_videos(self):
+        if self.playlist_items.filter(Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=False)).exists():
+            return True
+        return False
+
     def has_duplicate_videos(self):
         if self.playlist_items.filter(is_duplicate=True).exists():
             return True
@@ -1259,6 +1265,9 @@ class Playlist(models.Model):
     def get_unavailable_videos_count(self):
         return self.video_count - self.get_watchable_videos_count()
 
+    def get_duplicate_videos_count(self):
+        return self.playlist_items.filter(is_duplicate=True).count()
+
     # return count of watchable videos, i.e # videos that are not private or deleted in the playlist
     def get_watchable_videos_count(self):
         return self.playlist_items.filter(
@@ -1336,6 +1345,6 @@ class PlaylistItem(models.Model):
 class Pin(models.Model):
     untube_user = models.ForeignKey(User, related_name="pins",
                                     on_delete=models.CASCADE, null=True)  # untube user this pin is linked to
-    type = models.CharField(max_length=100)  # "playlist", "video"
+    kind = models.CharField(max_length=100)  # "playlist", "video"
     playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE, null=True)
     video = models.ForeignKey(Video, on_delete=models.CASCADE, null=True)

+ 28 - 16
apps/main/templates/view_playlist.html

@@ -5,6 +5,8 @@
 
 {% block content %}
 
+    <script src="{% static 'htmx/extensions/class-tools.js' %}" type="application/javascript"></script>
+
     <div id="view_playlist">
         {% if not_imported_LL %}
             <div hx-get="/import/liked-videos-playlist" hx-trigger="load" hx-swap="outerHTML">
@@ -27,7 +29,9 @@
             <div class="sticky-top mb-3" style="top: 0.5rem;">
                 {% if not playlist.is_yt_mix %}
                     <div hx-get="{% url 'update_playlist' playlist.playlist_id 'checkforupdates' %}" hx-trigger="load" hx-swap="outerHTML" id="checkforupdates">
-
+                        <div class="alert alert-info alert-dismissible fade show" role="alert">
+                            Checking playlist for updates...
+                        </div>
                     </div>
                 {% endif %}
                 {% if playlist.marked_as == "watching" %}
@@ -101,7 +105,7 @@
                                     </span>
                                 {% endif %}
                                 {% if playlist.has_duplicate_videos %}
-                                    <span class="badge bg-light text-black-50">DUPLICATE VIDEOS</span>
+                                    <span class="badge bg-light text-black-50">{{ playlist.get_duplicate_videos_count }} DUPLICATE VIDEOS</span>
                                 {% endif %}
                               <a data-bs-toggle="collapse" href="#channelVidsChartCollapse" role="button" aria-expanded="false" aria-controls="channelVidsChartCollapse">
                                         <span class="badge bg-light text-black-50">{{ playlist.get_channels_list.0 }} channels</span>
@@ -338,12 +342,12 @@
                 <div class="collapse border-danger" id="moveItemsToCollapse">
                     <div class="card card-body bg-dark text-white">
 
-                        <h5>{% if playlist.is_user_owned %}Move or {% endif %}Copy videos to another playlist!</h5>
+                        <h5>{% if playlist.is_user_owned %}Move or {% endif %}Copy videos to other playlist(s)!</h5>
 
                         <div class="row d-flex justify-content-start">
 
 
-                            <div class="col-md-8 text-dark">
+                            <div class="col-md-6 text-dark">
                                 <select class="visually-hidden"
                                     id="playlists-to-move-to" name="playlist-ids" placeholder="Select Playlists" multiple>
                                     {% for pl in user_owned_playlists %}
@@ -356,24 +360,27 @@
 
                             <div class="col-md d-flex justify-content-start">
 
+                            <div class="htmx-indicator d-flex align-items-center" id="move-copy-loader">
+                                <img src="{% static 'svg-loaders/grid.svg' %}" width="25" height="25" class="ms-2 mt-2">
+                                <span class="mt-2 ms-2">Loading, please wait... </span>
+                            </div>
+                            </div>
+
+                        </div>
+
+                        <div class="d-flex justify-content-start mt-2">
                             {% if playlist.is_user_owned %}
                                 <div class="btn-group mt-1">
-                                    <button hx-post="{% url 'playlist_move_copy_videos' playlist.playlist_id 'move' %}" hx-include="#playlists-to-move-to, #video-checkboxes" hx-target="#move-copy-videos-box" type="button" class="btn btn-primary">
+                                    <button hx-indicator="#move-copy-loader" hx-post="{% url 'playlist_move_copy_videos' playlist.playlist_id 'move' %}" hx-include="#playlists-to-move-to, #video-checkboxes" hx-target="#move-copy-videos-box" type="button" class="btn btn-primary">
                                         Move!
                                     </button>
                                 </div>
                             {% endif %}
                             <div class="btn-group ms-1 mt-1">
-                                <button 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-info">
+                                <button hx-indicator="#move-copy-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-info">
                                     Copy!
                                 </button>
                             </div>
-
-                            </div>
-
-                        </div>
-
-                        <div class="d-flex justify-content-start mt-2">
               <div id="move-copy-videos-box" class="d-flex align-items-end ms-2">
                                 <span class="text-warning">Note: Move will delete the selected videos from this playlist. Copy won't.</span>
                             </div>
@@ -400,23 +407,28 @@
 
                             {% if playlist.has_unavailable_videos %}
                                 <div class="btn-group me-2">
-                                    <button hx-post="#" hx-include="[id='video-checkboxes']" type="button" class="btn btn-info">
+                                    <button hx-post="{% url 'delete_specific_videos' playlist.playlist_id 'unavailable' %}" hx-target="#delete-videos-confirm-box" type="button" class="btn btn-info" hx-indicator="#delete-loader">
                                         Delete {{ playlist.get_unavailable_videos_count }} Unavailable Videos
                                     </button>
                                 </div>
                             {% endif %}
                             {% if playlist.has_duplicate_videos %}
                                 <div class="btn-group me-2">
-                                    <button hx-post="#" hx-include="[id='video-checkboxes']" type="button" class="btn btn-warning">
-                                        Delete Duplicate Videos
+                                    <button hx-post="{% url 'delete_specific_videos' playlist.playlist_id 'duplicate' %}" hx-target="#delete-videos-confirm-box" type="button" class="btn btn-warning" hx-indicator="#delete-loader">
+                                        Delete {{ playlist.get_duplicate_videos_count }} Duplicate Videos
                                     </button>
                                 </div>
                             {% endif %}
                             <div class="btn-group me-2">
-                                <button hx-post="{% url 'delete_videos' playlist.playlist_id 'confirm' %}" hx-include="[id='video-checkboxes']" hx-target="#delete-videos-confirm-box" type="button" class="btn btn-danger">
+                                <button hx-post="{% url 'delete_videos' playlist.playlist_id 'confirm' %}" hx-vals='{"all": "yes", "confirm before deleting": "{{ playlist.confirm_before_deleting }}"}' hx-target="#delete-videos-confirm-box" type="button" class="btn btn-danger">
                                     Empty this Playlist
                                 </button>
                             </div>
+
+                            <div class="htmx-indicator d-flex align-items-center" id="delete-loader">
+                                <img src="{% static 'svg-loaders/grid.svg' %}" width="25" height="25" class="ms-2 mt-2">
+                                <span class="mt-2 ms-2">Deleting, please wait... </span>
+                            </div>
                         </div>
 
                     </div>

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

@@ -63,7 +63,7 @@
                                 <span class="badge bg-warning text-black-50 mb-1"><i class="fas fa-thumbs-down"></i> {% if video.dislike_count == -1 %}HIDDEN{% else %}{{ video.dislike_count|intcomma }}{% endif %}</span>
                                 <span class="badge bg-info text-black-50 mb-1"><i class="fas fa-comments"></i> {% if video.comment_count == -1 %}HIDDEN{% else %}{{ video.comment_count|intcomma }}{% endif %} </span>
                                 {% if video.is_unavailable_on_yt or video.was_deleted_on_yt %}<span class="badge bg-light text-black-50 mb-1">UNAVAILABLE</span>{% endif %}
-                                <span class="badge bg-light text-black-50 mb-1"><a href="#found-in" style="text-decoration: none; color: grey"> Found in {{ video.playlistitem_set.count }} playlist{% if video.playlistitem_set.count > 1 %}s{% endif %}</a></span>
+                                <span class="badge bg-light text-black-50 mb-1"><a href="#found-in" style="text-decoration: none; color: grey"> Found in {{ video.playlists.all.count }} playlist{% if video.playlists.all.count > 1 %}s{% endif %}</a></span>
 
 
                             </h6>

+ 2 - 1
apps/main/urls.py

@@ -28,7 +28,7 @@ urlpatterns = [
          name='order_playlist_by'),
     path("playlist/<slug:playlist_id>/mark-as/<slug:mark_as>", views.mark_playlist_as,
          name='mark_playlist_as'),
-    path("playlist/<slug:playlist_id>/update/<slug:type>", views.update_playlist, name="update_playlist"),
+    path("playlist/<slug:playlist_id>/update/<slug:command>", 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>/<slug:order_by>/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"),
@@ -38,6 +38,7 @@ urlpatterns = [
     path("playlist/<slug:playlist_id>/get-unused-tags", views.get_unused_playlist_tags, name="get_unused_playlist_tags"),
     path("playlist/<slug:playlist_id>/get-watch-message", views.get_watch_message, name="get_watch_message"),
     path("playlist/<slug:playlist_id>/delete-videos/<slug:command>", views.delete_videos, name='delete_videos'),
+    path("playlist/<slug:playlist_id>/delete-specific-videos/<slug:command>", views.delete_specific_videos, name='delete_specific_videos'),
     path("playlist/<slug:playlist_id>/delete-playlist", views.delete_playlist, name="delete_playlist"),
     path("playlist/<slug:playlist_id>/reset-watched", views.reset_watched, name="reset_watched"),
     path("playlist/<slug:playlist_id>/move-copy-videos/<str:action>", views.playlist_move_copy_videos, name="playlist_move_copy_videos"),

+ 132 - 56
apps/main/views.py

@@ -77,11 +77,16 @@ def home(request):
 
     if total_num_playlists != 0:
         # x means  percentage
-        statistics["public_x"] = round(user_playlists.filter(is_private_on_yt=False).count() / total_num_playlists, 1) * 100
-        statistics["private_x"] = round(user_playlists.filter(is_private_on_yt=True).count() / total_num_playlists, 1) * 100
-        statistics["favorites_x"] = round(user_playlists.filter(is_favorite=True).count() / total_num_playlists, 1) * 100
-        statistics["watching_x"] = round(user_playlists.filter(marked_as="watching").count() / total_num_playlists, 1) * 100
-        statistics["imported_x"] = round(user_playlists.filter(is_user_owned=False).count() / total_num_playlists, 1) * 100
+        statistics["public_x"] = round(user_playlists.filter(is_private_on_yt=False).count() / total_num_playlists,
+                                       1) * 100
+        statistics["private_x"] = round(user_playlists.filter(is_private_on_yt=True).count() / total_num_playlists,
+                                        1) * 100
+        statistics["favorites_x"] = round(user_playlists.filter(is_favorite=True).count() / total_num_playlists,
+                                          1) * 100
+        statistics["watching_x"] = round(user_playlists.filter(marked_as="watching").count() / total_num_playlists,
+                                         1) * 100
+        statistics["imported_x"] = round(user_playlists.filter(is_user_owned=False).count() / total_num_playlists,
+                                         1) * 100
 
     return render(request, 'home.html', {"channel_found": channel_found,
                                          "user_playlists": user_playlists,
@@ -109,7 +114,6 @@ def view_video(request, video_id):
         return redirect('home')
 
 
-
 @login_required
 @require_POST
 def video_notes(request, video_id):
@@ -298,7 +302,8 @@ def order_playlist_by(request, playlist_id, order_by):
     if order_by == "all":
         playlist_items = playlist.playlist_items.select_related('video').order_by("video_position")
     elif order_by == "favorites":
-        playlist_items = playlist.playlist_items.select_related('video').filter(video__is_favorite=True).order_by("video_position")
+        playlist_items = playlist.playlist_items.select_related('video').filter(video__is_favorite=True).order_by(
+            "video_position")
         videos_details = "Sorted by Favorites"
         display_text = "No favorites yet!"
     elif order_by == "popularity":
@@ -312,7 +317,8 @@ def order_playlist_by(request, playlist_id, order_by):
         playlist_items = playlist.playlist_items.select_related('video').order_by("-video__view_count")
     elif order_by == "has-cc":
         videos_details = "Filtered by Has CC"
-        playlist_items = playlist.playlist_items.select_related('video').filter(video__has_cc=True).order_by("video_position")
+        playlist_items = playlist.playlist_items.select_related('video').filter(video__has_cc=True).order_by(
+            "video_position")
         display_text = "No videos in this playlist have CC :("
     elif order_by == "duration":
         videos_details = "Sorted by Video Duration"
@@ -322,7 +328,8 @@ def order_playlist_by(request, playlist_id, order_by):
         videos_details = "Sorted by New Updates"
         display_text = "No new updates! Note that deleted videos will not show up here."
         if playlist.has_new_updates:
-            recently_updated_videos = playlist.playlist_items.select_related('video').filter(video__video_details_modified=True)
+            recently_updated_videos = playlist.playlist_items.select_related('video').filter(
+                video__video_details_modified=True)
 
             for playlist_item in recently_updated_videos:
                 if playlist_item.video.video_details_modified_at + datetime.timedelta(hours=12) < datetime.datetime.now(
@@ -336,12 +343,14 @@ def order_playlist_by(request, playlist_id, order_by):
             else:
                 playlist_items = recently_updated_videos.order_by("video_position")
     elif order_by == 'unavailable-videos':
-        playlist_items = playlist.playlist_items.select_related('video').filter(Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=True))
+        playlist_items = playlist.playlist_items.select_related('video').filter(
+            Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=True))
         videos_details = "Sorted by Unavailable Videos"
         display_text = "None of the videos in this playlist have gone unavailable... yet."
     elif order_by == 'channel':
         channel_name = request.GET["channel-name"]
-        playlist_items = playlist.playlist_items.select_related('video').filter(video__channel_name=channel_name).order_by("video_position")
+        playlist_items = playlist.playlist_items.select_related('video').filter(
+            video__channel_name=channel_name).order_by("video_position")
         videos_details = f"Sorted by Channel '{channel_name}'"
     else:
         return HttpResponse("Something went wrong :(")
@@ -432,53 +441,99 @@ def playlists_home(request):
 @login_required
 @require_POST
 def delete_videos(request, playlist_id, command):
-    playlist_item_ids = request.POST.getlist("video-id", default=[])
-
+    all = False
+    num_vids = 0
+    playlist_item_ids = []
     print(request.POST)
-    num_vids = len(playlist_item_ids)
+    if "all" in request.POST:
+        if request.POST["all"] == "yes":
+            all = True
+            num_vids = request.user.playlists.get(playlist_id=playlist_id).playlist_items.all().count()
+            if command == "start":
+                playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in request.user.playlists.get(playlist_id=playlist_id).playlist_items.all()]
+    else:
+        playlist_item_ids = request.POST.getlist("video-id", default=[])
+        num_vids = len(playlist_item_ids)
+
     extra_text = " "
     if num_vids == 0:
-        return HttpResponse("<h5>Select some videos first!</h5><hr>")
+        return HttpResponse("""
+        <div hx-ext="class-tools">
+            <div classes="add visually-hidden:3s">
+                <h5>Select some videos first!</h5><hr>
+            </div>
+        </div>
+        """)
 
     if 'confirm before deleting' in request.POST:
         if request.POST['confirm before deleting'] == 'False':
             command = "confirmed"
 
     if command == "confirm":
-        print(playlist_item_ids)
-
-        if num_vids == request.user.playlists.get(playlist_id=playlist_id).playlist_items.all().count():
+        if all or num_vids == request.user.playlists.get(playlist_id=playlist_id).playlist_items.all().count():
+            hx_vals = """hx-vals='{"all": "yes"}'"""
             delete_text = "ALL VIDEOS"
             extra_text = " This will not delete the playlist itself, will only make the playlist empty. "
         else:
+            hx_vals = ""
             delete_text = f"{num_vids} videos"
+
+        url = f"/playlist/{playlist_id}/delete-videos/confirmed"
         return HttpResponse(
-            f"""<h5>
-                Are you sure you want to delete {delete_text} from your YouTube playlist?{extra_text}This cannot be undone.</h5>
-                <button hx-post="/playlist/{playlist_id}/delete-videos/confirmed" hx-include="[id='video-checkboxes']" hx-target="#delete-videos-confirm-box" type="button" class="btn btn-outline-danger btn-sm">Confirm</button>
-                <hr>
+            f"""
+                <div hx-ext="class-tools">
+                <div classes="add visually-hidden:4s">
+                    <h5>
+                    Are you sure you want to delete {delete_text} from your YouTube playlist?{extra_text}This cannot be undone.</h5>
+                    <button hx-post="{url}" hx-include="[id='video-checkboxes']" {hx_vals} hx-target="#delete-videos-confirm-box" type="button" class="btn btn-outline-danger btn-sm">Confirm</button>
+                    <hr>
+                </div>
+                </div>
             """)
     elif command == "confirmed":
-        print(playlist_item_ids)
+        if all:
+            hx_vals = """hx-vals='{"all": "yes"}'"""
+        else:
+            hx_vals = ""
         url = f"/playlist/{playlist_id}/delete-videos/start"
         return HttpResponse(
             f"""
-            <div class="spinner-border text-light" role="status" hx-post="{url}" hx-trigger="load" hx-include="[id='video-checkboxes']" hx-target="#delete-videos-confirm-box"></div><hr>
+            <div class="spinner-border text-light" role="status" hx-post="{url}" {hx_vals} hx-trigger="load" hx-include="[id='video-checkboxes']" hx-target="#delete-videos-confirm-box"></div><hr>
             """)
     elif command == "start":
         print("Deleting", len(playlist_item_ids), "videos")
         Playlist.objects.deletePlaylistItems(request.user, playlist_id, playlist_item_ids)
-        # playlist = request.user.playlists.get(playlist_id=playlist_id)
-        # playlist.has_playlist_changed = True
-        # playlist.save(update_fields=['has_playlist_changed'])
+        if all:
+            help_text = "Finished emptying this playlist."
+        else:
+            help_text = "Done deleting selected videos from your playlist on YouTube."
+
         return HttpResponse(f"""
         <h5 hx-get="/playlist/{playlist_id}/update/checkforupdates" hx-trigger="load delay:2s" hx-target="#checkforupdates">
-            Done deleting selected videos from your playlist on YouTube. Refresh page!
+            {help_text} Refresh page!
         </h5>
         <hr>
         """)
 
 
+@login_required
+@require_POST
+def delete_specific_videos(request, playlist_id, command):
+    Playlist.objects.deleteSpecificPlaylistItems(request.user, playlist_id, command)
+
+    help_text = "Error."
+    if command == "unavailable":
+        help_text = "Deleted all unavailable videos."
+    elif command == "duplicate":
+        help_text = "Deleted all duplicate videos."
+
+    return HttpResponse(f"""
+        <h5>
+            {help_text} Refresh page!
+        </h5>
+        <hr>
+        """)
+
 @login_required
 @require_POST
 def search_tagged_playlists(request, tag):
@@ -591,27 +646,33 @@ def search_UnTube(request):
         tags = request.POST.getlist('playlist-tags')
         for tag in tags:
             all_playlists = all_playlists.filter(tags__name=tag)
-        #all_playlists = all_playlists.filter(tags__name__in=tags)
+        # all_playlists = all_playlists.filter(tags__name__in=tags)
 
     playlist_items = []
 
     if request.POST['search-settings'] == 'starts-with':
-        playlists = all_playlists.filter(Q(name__istartswith=search_query) | Q(user_label__istartswith=search_query)) if search_query != "" else all_playlists.none()
+        playlists = all_playlists.filter(Q(name__istartswith=search_query) | Q(
+            user_label__istartswith=search_query)) if search_query != "" else all_playlists.none()
 
         if search_query != "":
             for playlist in all_playlists:
-                pl_items = playlist.playlist_items.select_related('video').filter(Q(video__name__istartswith=search_query) | Q(video__user_label__istartswith=search_query) & Q(is_duplicate=False))
+                pl_items = playlist.playlist_items.select_related('video').filter(
+                    Q(video__name__istartswith=search_query) | Q(video__user_label__istartswith=search_query) & Q(
+                        is_duplicate=False))
 
                 if pl_items.exists():
                     for v in pl_items.all():
                         playlist_items.append(v)
 
     else:
-        playlists = all_playlists.filter(Q(name__icontains=search_query) | Q(user_label__istartswith=search_query)) if search_query != "" else all_playlists.none()
+        playlists = all_playlists.filter(Q(name__icontains=search_query) | Q(
+            user_label__istartswith=search_query)) if search_query != "" else all_playlists.none()
 
         if search_query != "":
             for playlist in all_playlists:
-                pl_items = playlist.playlist_items.select_related('video').filter(Q(video__name__icontains=search_query) | Q(video__user_label__istartswith=search_query) & Q(is_duplicate=False))
+                pl_items = playlist.playlist_items.select_related('video').filter(
+                    Q(video__name__icontains=search_query) | Q(video__user_label__istartswith=search_query) & Q(
+                        is_duplicate=False))
 
                 if pl_items.exists():
                     for v in pl_items.all():
@@ -670,7 +731,7 @@ def manage_import_playlists(request):
             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)
@@ -718,7 +779,8 @@ def load_more_videos(request, playlist_id, order_by, page):
         playlist_items = playlist.playlist_items.select_related('video').order_by("video_position")
         print(f"loading page 1: {playlist_items.count()} videos")
     elif order_by == "favorites":
-        playlist_items = playlist.playlist_items.select_related('video').filter(video__is_favorite=True).order_by("video_position")
+        playlist_items = playlist.playlist_items.select_related('video').filter(video__is_favorite=True).order_by(
+            "video_position")
     elif order_by == "popularity":
         playlist_items = playlist.playlist_items.select_related('video').order_by("-video__like_count")
     elif order_by == "date-published":
@@ -726,13 +788,15 @@ def load_more_videos(request, playlist_id, order_by, page):
     elif order_by == "views":
         playlist_items = playlist.playlist_items.select_related('video').order_by("-video__view_count")
     elif order_by == "has-cc":
-        playlist_items = playlist.playlist_items.select_related('video').filter(video__has_cc=True).order_by("video_position")
+        playlist_items = playlist.playlist_items.select_related('video').filter(video__has_cc=True).order_by(
+            "video_position")
     elif order_by == "duration":
         playlist_items = playlist.playlist_items.select_related('video').order_by("-video__duration_in_seconds")
     elif order_by == 'new-updates':
         playlist_items = []
         if playlist.has_new_updates:
-            recently_updated_videos = playlist.playlist_items.select_related('video').filter(video__video_details_modified=True)
+            recently_updated_videos = playlist.playlist_items.select_related('video').filter(
+                video__video_details_modified=True)
 
             for playlist_item in recently_updated_videos:
                 if playlist_item.video.video_details_modified_at + datetime.timedelta(hours=12) < datetime.datetime.now(
@@ -746,7 +810,8 @@ def load_more_videos(request, playlist_id, order_by, page):
             else:
                 playlist_items = recently_updated_videos.order_by("video_position")
     elif order_by == 'unavailable-videos':
-        playlist_items = playlist.playlist_items.select_related('video').filter(Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=True))
+        playlist_items = playlist.playlist_items.select_related('video').filter(
+            Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=True))
     elif order_by == 'channel':
         channel_name = request.GET["channel-name"]
         playlist_items = playlist.playlist_items.select_related('video').filter(
@@ -810,10 +875,10 @@ def update_playlist_settings(request, playlist_id):
 
 
 @login_required
-def update_playlist(request, playlist_id, type):
+def update_playlist(request, playlist_id, command):
     playlist = request.user.playlists.get(playlist_id=playlist_id)
 
-    if type == "checkforupdates":
+    if command == "checkforupdates":
         print("Checking if playlist changed...")
         result = Playlist.objects.checkIfPlaylistChangedOnYT(request.user, playlist_id)
 
@@ -837,10 +902,14 @@ def update_playlist(request, playlist_id, type):
                 # playlist.save()
             else:  # no updates found
                 return HttpResponse("""
-                <div id="checkforupdates" class="sticky-top" style="top: 0.5em;">
-                <div class="alert alert-success alert-dismissible fade show visually-hidden" role="alert">
-                    No new updates!
-                </div>
+                <div hx-ext="class-tools">
+
+                    <div id="checkforupdates" class="sticky-top" style="top: 0.5em;">
+                    
+                        <div class="alert alert-success alert-dismissible fade show" classes="add visually-hidden:1s" role="alert">
+                            Playlist upto date!
+                        </div>
+                    </div>
                 </div>
                 """)
         elif result[0] == -1:  # playlist changed
@@ -858,9 +927,11 @@ def update_playlist(request, playlist_id, type):
         else:  # no updates found
             return HttpResponse("""
             <div id="checkforupdates" class="sticky-top" style="top: 0.5em;">
-            <div class="alert alert-success alert-dismissible fade show visually-hidden sticky-top" role="alert" style="top: 0.5em;">
-                No new updates!
-            </div>
+                <div hx-ext="class-tools">
+                <div classes="add visually-hidden:2s" class="alert alert-success alert-dismissible fade show sticky-top visually-hidden" role="alert" style="top: 0.5em;">
+                    No new updates!
+                </div>
+                </div>
             </div>
             """)
 
@@ -876,7 +947,7 @@ def update_playlist(request, playlist_id, type):
         </div>
         """)
 
-    if type == "manual":
+    if command == "manual":
         print("MANUAL")
         return HttpResponse(
             f"""<div hx-get="/playlist/{playlist_id}/update/auto" hx-trigger="load" hx-swap="outerHTML">
@@ -888,7 +959,7 @@ def update_playlist(request, playlist_id, type):
 
     print("Attempting to update playlist")
     status, deleted_playlist_item_ids, unavailable_videos, added_videos = Playlist.objects.updatePlaylist(request.user,
-                                                                                                  playlist_id)
+                                                                                                          playlist_id)
 
     playlist = request.user.playlists.get(playlist_id=playlist_id)
 
@@ -1109,12 +1180,9 @@ def reset_watched(request, playlist_id):
 @login_required
 @require_POST
 def playlist_move_copy_videos(request, playlist_id, action):
-    playlist = request.user.playlists.get(playlist_id=playlist_id)
-
     playlist_ids = request.POST.getlist("playlist-ids", default=[])
     playlist_item_ids = request.POST.getlist("video-id", default=[])
 
-
     # basic processing
     if not playlist_ids and not playlist_item_ids:
         return HttpResponse(f"""
@@ -1127,21 +1195,29 @@ def playlist_move_copy_videos(request, playlist_id, action):
                 <span class="text-danger">First select some videos to {action}!</span>""")
 
     success_message = f"""
-                <span class="text-success">Successfully {'moved' if action == 'move' else 'copied'} {len(playlist_item_ids)} videos to {len(playlist_ids)} other playlist! Refresh depage!</span>"""
+                <div hx-ext="class-tools">
+                <span classes="add visually-hidden:5s" class="text-success">Successfully {'moved' if action == 'move' else 'copied'} {len(playlist_item_ids)} video(s) to {len(playlist_ids)} other playlist(s)! 
+                Go visit those playlist(s)!</span>
+                </div>
+                """
     if action == "move":
         status = Playlist.objects.moveCopyVideosFromPlaylist(request.user,
                                                              from_playlist_id=playlist_id,
                                                              to_playlist_ids=playlist_ids,
                                                              playlist_item_ids=playlist_item_ids,
                                                              action="move")
-        if status == -1:
+        if status[0] == -1:
+            if status[1] == 404:
+                return HttpResponse("<span class='text-danger'>You cannot copy/move unavailable videos! De-select them and try again.</span>")
             return HttpResponse("Error moving!")
     else:  # copy
         status = Playlist.objects.moveCopyVideosFromPlaylist(request.user,
                                                              from_playlist_id=playlist_id,
                                                              to_playlist_ids=playlist_ids,
                                                              playlist_item_ids=playlist_item_ids)
-        if status == -1:
+        if status[0] == -1:
+            if status[1] == 404:
+                return HttpResponse("<span class='text-danger'>You cannot copy/move unavailable videos! De-select them and try again.</span>")
             return HttpResponse("Error copying!")
 
-    return HttpResponse(success_message)
+    return HttpResponse(success_message)