Browse Source

move and copy implemented

sleepytaco 3 năm trước cách đây
mục cha
commit
e1d54bbc87

+ 26 - 0
apps/main/migrations/0038_auto_20210720_1114.py

@@ -0,0 +1,26 @@
+# Generated by Django 3.2.3 on 2021-07-20 16:14
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('main', '0037_alter_video_num_of_accesses'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='pin',
+            name='untube_user',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pins', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='playlistitem',
+            name='video',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.video'),
+        ),
+    ]

+ 36 - 0
apps/main/migrations/0039_auto_20210720_1116.py

@@ -0,0 +1,36 @@
+# Generated by Django 3.2.3 on 2021-07-20 16:16
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('main', '0038_auto_20210720_1114'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='pin',
+            name='playlist',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.playlist'),
+        ),
+        migrations.AlterField(
+            model_name='pin',
+            name='video',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.video'),
+        ),
+        migrations.AlterField(
+            model_name='playlistitem',
+            name='playlist',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='playlist_items', to='main.playlist'),
+        ),
+        migrations.AlterField(
+            model_name='tag',
+            name='created_by',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='playlist_tags', to=settings.AUTH_USER_MODEL),
+        ),
+    ]

+ 17 - 0
apps/main/migrations/0040_remove_playlist_has_duplicate_videos.py

@@ -0,0 +1,17 @@
+# Generated by Django 3.2.3 on 2021-07-20 20:25
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('main', '0039_auto_20210720_1116'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='playlist',
+            name='has_duplicate_videos',
+        ),
+    ]

+ 134 - 44
apps/main/models.py

@@ -254,7 +254,6 @@ class PlaylistManager(models.Manager):
                     # if video already in playlist.videos
                     is_duplicate = False
                     if playlist.videos.filter(video_id=video_id).exists():
-                        playlist.has_duplicate_videos = True
                         is_duplicate = True
                     else:
                         playlist.videos.add(video)
@@ -278,9 +277,10 @@ class PlaylistManager(models.Manager):
                     playlist_item.save()
 
                     # check if the video became unavailable on youtube
-                    if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" or
-                                                           item['snippet'][
-                                                               'description'] == "This video is unavailable.") or (
+                    if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (
+                            item['snippet']['title'] == "Deleted video" or
+                            item['snippet'][
+                                'description'] == "This video is unavailable.") or (
                             item['snippet']['title'] == "Private video" or item['snippet'][
                         'description'] == "This video is private."):
                         video.was_deleted_on_yt = True
@@ -348,7 +348,6 @@ class PlaylistManager(models.Manager):
                             # if video already in playlist.videos
                             is_duplicate = False
                             if playlist.videos.filter(video_id=video_id).exists():
-                                playlist.has_duplicate_videos = True
                                 is_duplicate = True
                             else:
                                 playlist.videos.add(video)
@@ -371,9 +370,10 @@ class PlaylistManager(models.Manager):
                             playlist_item.save()
 
                             # check if the video became unavailable on youtube
-                            if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" or
-                                                                   item['snippet'][
-                                                                       'description'] == "This video is unavailable.") or (
+                            if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (
+                                    item['snippet']['title'] == "Deleted video" or
+                                    item['snippet'][
+                                        'description'] == "This video is unavailable.") or (
                                     item['snippet']['title'] == "Private video" or item['snippet'][
                                 'description'] == "This video is private."):
                                 video.was_deleted_on_yt = True
@@ -451,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(minutes=2) < datetime.datetime.now(pytz.utc):
+        if playlist.last_full_scan_at + datetime.timedelta(seconds=2) < 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
@@ -488,9 +488,9 @@ class PlaylistManager(models.Manager):
                         # check if the video became unavailable on youtube
                         if not video.is_unavailable_on_yt and not video.was_deleted_on_yt:
                             if (item['snippet']['title'] == "Deleted video" or
-                                item['snippet']['description'] == "This video is unavailable." or
+                                    item['snippet']['description'] == "This video is unavailable." or
                                     item['snippet']['title'] == "Private video" or item['snippet'][
-                                'description'] == "This video is private."):
+                                        'description'] == "This video is private."):
                                 unavailable_videos += 1
 
                 while True:
@@ -515,9 +515,9 @@ class PlaylistManager(models.Manager):
                                 # check if the video became unavailable on youtube
                                 if not video.is_unavailable_on_yt and not video.was_deleted_on_yt:
                                     if (item['snippet']['title'] == "Deleted video" or
-                                        item['snippet']['description'] == "This video is unavailable." or
+                                            item['snippet']['description'] == "This video is unavailable." or
                                             item['snippet']['title'] == "Private video" or item['snippet'][
-                                        'description'] == "This video is private."):
+                                                'description'] == "This video is private."):
                                         unavailable_videos += 1
                     except AttributeError:
                         break
@@ -588,8 +588,6 @@ class PlaylistManager(models.Manager):
         credentials = self.getCredentials(user)
 
         playlist = user.playlists.get(playlist_id__exact=playlist_id)
-        playlist.has_duplicate_videos = False  # reset this to false for now
-        has_duplicate_videos = False
 
         current_video_ids = [playlist_item.video.video_id for playlist_item in playlist.playlist_items.all()]
         current_playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in playlist.playlist_items.all()]
@@ -627,7 +625,9 @@ class PlaylistManager(models.Manager):
                     if not user.videos.filter(video_id=video_id).exists():
                         if (item['snippet']['title'] == "Deleted video" and item['snippet'][
                             'description'] == "This video is unavailable.") or (item['snippet'][
-                            'title'] == "Private video" and item['snippet']['description'] == "This video is private."):
+                                                                                    'title'] == "Private video" and
+                                                                                item['snippet'][
+                                                                                    'description'] == "This video is private."):
                             video = Video(
                                 video_id=video_id,
                                 name=item['snippet']['title'],
@@ -654,9 +654,10 @@ class PlaylistManager(models.Manager):
                     video = user.videos.get(video_id=video_id)
 
                     # check if the video became unavailable on youtube
-                    if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" and
-                                                           item['snippet'][
-                                                               'description'] == "This video is unavailable.") or (
+                    if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (
+                            item['snippet']['title'] == "Deleted video" and
+                            item['snippet'][
+                                'description'] == "This video is unavailable.") or (
                             item['snippet']['title'] == "Private video" and item['snippet'][
                         'description'] == "This video is private."):
                         video.was_deleted_on_yt = True
@@ -667,7 +668,6 @@ class PlaylistManager(models.Manager):
                         playlist.videos.add(video)
                     else:
                         is_duplicate = True
-                        has_duplicate_videos = True
 
                     playlist_item = PlaylistItem(
                         playlist_item_id=playlist_item_id,
@@ -689,7 +689,8 @@ class PlaylistManager(models.Manager):
 
                     video.video_details_modified = True
                     video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
-                    video.save(update_fields=['video_details_modified', 'video_details_modified_at', 'was_deleted_on_yt'])
+                    video.save(
+                        update_fields=['video_details_modified', 'video_details_modified_at', 'was_deleted_on_yt'])
                     added_videos.append(video)
 
                 else:  # if playlist item already in playlist
@@ -709,7 +710,7 @@ class PlaylistManager(models.Manager):
                             playlist_item.video.video_details_modified = True
                             playlist_item.video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
                             playlist_item.video.save(update_fields=['was_deleted_on_yt', 'video_details_modified',
-                                                      'video_details_modified_at'])
+                                                                    'video_details_modified_at'])
 
                             unavailable_videos.append(playlist_item.video)
 
@@ -758,9 +759,10 @@ class PlaylistManager(models.Manager):
                             video = user.videos.get(video_id=video_id)
 
                             # check if the video became unavailable on youtube
-                            if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" and
-                                                                   item['snippet'][
-                                                                       'description'] == "This video is unavailable.") or (
+                            if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (
+                                    item['snippet']['title'] == "Deleted video" and
+                                    item['snippet'][
+                                        'description'] == "This video is unavailable.") or (
                                     item['snippet']['title'] == "Private video" and item['snippet'][
                                 'description'] == "This video is private."):
                                 video.was_deleted_on_yt = True
@@ -771,7 +773,6 @@ class PlaylistManager(models.Manager):
                                 playlist.videos.add(video)
                             else:
                                 is_duplicate = True
-                                has_duplicate_videos = True
 
                             playlist_item = PlaylistItem(
                                 playlist_item_id=playlist_item_id,
@@ -846,7 +847,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
@@ -889,25 +889,54 @@ class PlaylistManager(models.Manager):
         playlist.has_new_updates = True
         playlist.save()
 
-        playlist.has_duplicate_videos = has_duplicate_videos
-
         deleted_playlist_item_ids = current_playlist_item_ids  # left out playlist_item_ids
 
         return [0, deleted_playlist_item_ids, unavailable_videos, added_videos]
 
+    def deletePlaylistFromYouTube(self, user, playlist_id):
+        """
+        Takes in playlist itemids for the videos in a particular playlist
+        """
+        credentials = self.getCredentials(user)
+        playlist = user.playlists.get(playlist_id=playlist_id)
+
+        # new_playlist_duration_in_seconds = playlist.playlist_duration_in_seconds
+        # new_playlist_video_count = playlist.video_count
+        with build('youtube', 'v3', credentials=credentials) as youtube:
+            pl_request = youtube.playlists().delete(
+                id=playlist_id
+            )
+            try:
+                pl_response = pl_request.execute()
+                print(pl_response)
+            except googleapiclient.errors.HttpError as e:  # failed to delete playlist
+                # possible causes:
+                # playlistForbidden (403)
+                # playlistNotFound  (404)
+                # playlistOperationUnsupported (400)
+                print(e.error_details, e.status_code)
+                return -1
+
+            # playlistItem was successfully deleted if no HttpError, so delete it from db
+            playlist.delete()
+
+        return 0
+
     def deletePlaylistItems(self, user, playlist_id, playlist_item_ids):
         """
         Takes in playlist itemids for the videos in a particular playlist
         """
         credentials = self.getCredentials(user)
         playlist = user.playlists.get(playlist_id=playlist_id)
+        playlist_items = user.playlists.get(playlist_id=playlist_id).playlist_items.select_related('video').filter(
+            playlist_item_id__in=playlist_item_ids)
 
         # new_playlist_duration_in_seconds = playlist.playlist_duration_in_seconds
         # new_playlist_video_count = playlist.video_count
         with build('youtube', 'v3', credentials=credentials) as youtube:
-            for playlist_item_id in playlist_item_ids:
+            for playlist_item in playlist_items:
                 pl_request = youtube.playlistItems().delete(
-                    id=playlist_item_id
+                    id=playlist_item.playlist_item_id
                 )
                 print(pl_request)
                 try:
@@ -922,6 +951,12 @@ class PlaylistManager(models.Manager):
                     continue
 
                 # playlistItem was successfully deleted if no HttpError, so delete it from db
+                video = playlist_item.video
+                playlist_item.delete()
+
+                if not playlist.playlist_items.filter(video__video_id=video.video_id).exists():
+                    playlist.videos.remove(video)
+
                 # video = playlist.videos.get(playlist_item_id=playlist_item_id)
                 # new_playlist_video_count -= 1
                 # new_playlist_duration_in_seconds -= video.duration_in_seconds
@@ -976,10 +1011,54 @@ class PlaylistManager(models.Manager):
 
             return 0
 
+    def moveCopyVideosFromPlaylist(self, user, from_playlist_id, to_playlist_ids, playlist_item_ids, action="copy"):
+        """
+        Takes in playlist itemids for the videos in a particular playlist
+        """
+        credentials = self.getCredentials(user)
+        playlist_items = user.playlists.get(playlist_id=from_playlist_id).playlist_items.select_related('video').filter(
+            playlist_item_id__in=playlist_item_ids)
+
+        with build('youtube', 'v3', credentials=credentials) as youtube:
+            for playlist_id in to_playlist_ids:
+                for playlist_item in playlist_items:
+                    pl_request = youtube.playlistItems().insert(
+                        part="snippet",
+                        body={
+                            "snippet": {
+                                "playlistId": playlist_id,
+                                "position": 0,
+                                "resourceId": {
+                                    "kind": "youtube#video",
+                                    "videoId": playlist_item.video.video_id,
+                                }
+                            },
+                        }
+                    )
+
+                    try:
+                        pl_response = pl_request.execute()
+                    except googleapiclient.errors.HttpError as e:  # failed to update playlist details
+                        # possible causes:
+                        # playlistItemsNotAccessible (403)
+                        # playlistItemNotFound (404)
+                        # 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(pl_response)
+
+        if action == "move":  # delete from the current playlist
+            self.deletePlaylistItems(user, from_playlist_id, playlist_item_ids)
+
+        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)
+    created_by = models.ForeignKey(User, related_name="playlist_tags", on_delete=models.CASCADE, null=True)
 
     times_viewed = models.IntegerField(default=0)
     # type = models.CharField(max_length=10)  # either 'playlist' or 'video'
@@ -1091,7 +1170,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
-    has_duplicate_videos = models.BooleanField(default=False)  # duplicate videos will not be shown on site
 
     # watch playlist details
     # watch_time_left = models.CharField(max_length=150, default="")
@@ -1129,6 +1207,11 @@ class Playlist(models.Model):
     def __str__(self):
         return str(self.playlist_id)
 
+    def has_duplicate_videos(self):
+        if self.playlist_items.filter(is_duplicate=True).exists():
+            return True
+        return False
+
     def get_channels_list(self):
         channels_list = []
         num_channels = 0
@@ -1155,11 +1238,13 @@ class Playlist(models.Model):
 
     # 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(Q(is_duplicate=False) & Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False)).count()
+        return self.playlist_items.filter(
+            Q(is_duplicate=False) & Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False)).count()
 
     def get_watched_videos_count(self):
         return self.playlist_items.filter(Q(is_duplicate=False) &
-            Q(video__is_marked_as_watched=True) & Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False)).count()
+                                          Q(video__is_marked_as_watched=True) & Q(
+            video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False)).count()
 
     # diff of time from when playlist was first marked as watched and playlist reached 100% completion
     def get_finish_time(self):
@@ -1167,11 +1252,13 @@ class Playlist(models.Model):
 
     def get_watch_time_left(self):
         unwatched_playlist_items_secs = self.playlist_items.filter(Q(is_duplicate=False) &
-            Q(video__is_marked_as_watched=False) &
-          Q(video__is_unavailable_on_yt=False) &
-          Q(video__was_deleted_on_yt=False)).aggregate(Sum('video__duration_in_seconds'))['video__duration_in_seconds__sum']
+                                                                   Q(video__is_marked_as_watched=False) &
+                                                                   Q(video__is_unavailable_on_yt=False) &
+                                                                   Q(video__was_deleted_on_yt=False)).aggregate(
+            Sum('video__duration_in_seconds'))['video__duration_in_seconds__sum']
 
-        watch_time_left = getHumanizedTimeString(unwatched_playlist_items_secs) if unwatched_playlist_items_secs is not None else getHumanizedTimeString(0)
+        watch_time_left = getHumanizedTimeString(
+            unwatched_playlist_items_secs) if unwatched_playlist_items_secs is not None else getHumanizedTimeString(0)
 
         return watch_time_left
 
@@ -1179,7 +1266,8 @@ class Playlist(models.Model):
     def get_percent_complete(self):
         total_playlist_video_count = self.get_watchable_videos_count()
         watched_videos = self.playlist_items.filter(Q(is_duplicate=False) &
-            Q(video__is_marked_as_watched=True) & Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False))
+                                                    Q(video__is_marked_as_watched=True) & Q(
+            video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False))
         num_videos_watched = watched_videos.count()
         percent_complete = round((num_videos_watched / total_playlist_video_count) * 100,
                                  1) if total_playlist_video_count != 0 else 0
@@ -1195,8 +1283,8 @@ class Playlist(models.Model):
 
 class PlaylistItem(models.Model):
     playlist = models.ForeignKey(Playlist, related_name="playlist_items",
-                                 on_delete=models.CASCADE)  # playlist this pl item belongs to
-    video = models.ForeignKey(Video, on_delete=models.CASCADE)
+                                 on_delete=models.CASCADE, null=True)  # playlist this pl item belongs to
+    video = models.ForeignKey(Video, on_delete=models.CASCADE, null=True)
 
     # details
     playlist_item_id = models.CharField(max_length=100)  # the item id of the playlist this video beo
@@ -1223,6 +1311,8 @@ 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"
-    playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
-    video = models.ForeignKey(Video, on_delete=models.CASCADE)
+    playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE, null=True)
+    video = models.ForeignKey(Video, on_delete=models.CASCADE, null=True)

+ 8 - 0
apps/main/templates/search_untube_page.html

@@ -87,6 +87,14 @@
 
     <script type="application/javascript">
 
+    $(document).ready(function(){
+        // multiple choices select search box
+        var multipleCancelButton = new Choices('#choices-multiple-remove-button', {
+                    removeItemButton: true,
+                });
+
+            });
+
         function triggerSubmit() {
             var startsWithCB = document.getElementById("starts-with-cb");
             var containsCB = document.getElementById("contains-cb");

+ 27 - 11
apps/main/templates/view_playlist.html

@@ -334,8 +334,8 @@
 
 
                             <div class="col-md-8 text-dark">
-                                <select class="visually-hidden" onchange="triggerSubmit()"
-                                    id="choices-multiple-remove-button" name="playlist-tags" placeholder="Select Playlists" multiple>
+                                <select class="visually-hidden"
+                                    id="playlists-to-move-to" name="playlist-ids" placeholder="Select Playlists" multiple>
                                     {% for pl in user_owned_playlists %}
                                         {% if pl.playlist_id != playlist.playlist_id %}
                                             <option value="{{ pl.playlist_id }}" class="text-dark">{{ pl.name }}</option>
@@ -347,18 +347,21 @@
                         </div>
 
                         <div class="d-flex justify-content-start mt-2">
-                            <!--
-          <div class="btn-group">
-              <a href="{% url 'all_playlists' 'all' %}" target="_blank" class="btn btn-sm btn-success" id="select-all-btn">Search for Playlists <i class="fas fa-external-link-alt" aria-hidden="true"></i></a>
-          </div>
-          -->
+
                             {% if playlist.is_user_owned %}
                                 <div class="btn-group me-2">
-                                    <button type="button" class="btn btn-info" id="select-all-btn">Move!</button>
+                                    <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">
+                                        Move!
+                                    </button>
                                 </div>
                             {% endif %}
                             <div class="btn-group">
-                                <button type="button" class="btn btn-info" id="select-all-btn">Copy!</button>
+                                <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">
+                                    Copy!
+                                </button>
+                            </div>
+                            <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>
                         </div>
 
@@ -470,7 +473,7 @@
 
                                                 {% if playlist_item.is_duplicate %}<span class="badge bg-primary">duplicate</span>{% endif %}
                                                 {% if playlist_item.video.playlists.count|add:"-1" != 0 %}<span class="badge bg-dark"><a href="{% url 'video' playlist_item.video.video_id %}#found-in" style="text-decoration: none; color: white">found in {{ playlist_item.video.playlists.count|add:"-1" }} other playlist{% if playlist_item.video.playlists.count|add:"-1" > 1 %}s{% endif %}</a></span>{% endif %}
-                                                {% if playlist_item.video.video_details_modified %}<span class="badge bg-danger">{% if playlist_item.video.was_deleted_on_yt %}WENT PRIVATE/DELETED{% else %}ADDED{% endif %} {{ playlist_item.video.created_at|naturaltime|upper }}</span>{% endif %}<br>
+                                                {% if playlist_item.video.video_details_modified %}<span class="badge bg-danger">{% if playlist_item.video.was_deleted_on_yt %}went private/deleted{% else %}added{% endif %} {{ playlist_item.created_at|naturaltime }}</span>{% endif %}<br>
                                                 <br>
                                             {% endif %}
 
@@ -558,6 +561,20 @@
 
     <script type="application/javascript">
 
+
+        $(document).ready(function(){
+
+                // multiple choices select search box
+                var multipleCancelButton = new Choices('#playlists-to-move-to', {
+                    removeItemButton: true,
+                });
+
+                BackgroundCheck.init({
+                  targets: '.ui',
+                    images: '.ui-img'
+                });
+            });
+
             $(function () {
 
                   var $populationChart = $("#channel-videos-chart");
@@ -611,7 +628,6 @@
 
                 });
 
-
         // when a video list item is clicked on, it gets checked and its bg turns red
         function selectVideo(video) {
             if ($('#row2').is(':visible')) {

+ 6 - 3
apps/main/templates/view_playlist_settings.html

@@ -7,7 +7,6 @@
 
 
     <div id="view_playlist">
-        <br>
     {% if playlist.is_user_owned %}
 
         <div class="table-responsive" id="videos-div">
@@ -87,7 +86,9 @@
                           <h6 class="mb-0">Danger Zone</h6>
                         </div>
                         <div class="col-sm-9 text-white-50">
-                            <button type="button" class="btn btn-outline-danger">Delete Playlist From YouTube</button>
+                            <div id="delete-box">
+                                <a hx-get="{% url 'delete_playlist' playlist.playlist_id %}" hx-target="#delete-box" hx-vals='{"confirmed": "no"}' class="btn btn-outline-danger">Delete Playlist From YouTube</a>
+                            </div>
                         </div>
                       </div>
 
@@ -169,7 +170,9 @@
                           <h6 class="mb-0">Danger Zone</h6>
                         </div>
                         <div class="col-sm-9 text-white-50">
-                            <a href="{% url 'delete_playlist' playlist.playlist_id %}" class="btn btn-outline-danger">Remove Playlist From UnTube</a>
+                            <div id="delete-box">
+                                <a hx-get="{% url 'delete_playlist' playlist.playlist_id %}" hx-target="#delete-box" hx-vals='{"confirmed": "no"}' class="btn btn-outline-danger">Remove Playlist From UnTube</a>
+                            </div>
                         </div>
                       </div>
 

+ 3 - 2
apps/main/urls.py

@@ -19,7 +19,6 @@ urlpatterns = [
     path("<slug:playlist_id>/<slug:video_id>/video-details", views.view_video, name='video_details'),
     path("<slug:playlist_id>/<slug:video_id>/video-details/favorite", views.mark_video_favortie, name='mark_video_favorite'),
     path('<slug:playlist_id>/<slug:video_id>/video-details/watched', views.mark_video_watched, name='mark_video_watched'),
-    path("from/<slug:playlist_id>/delete-videos/<slug:command>", views.delete_videos, name='delete_videos'),
     path("videos/<slug:videos_type>", views.all_videos, name='all_videos'),
 
     ### STUFF RELATED TO ONE PLAYLIST
@@ -38,8 +37,10 @@ urlpatterns = [
     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"),
     path("playlist/<slug:playlist_id>/get-watch-message", views.get_watch_message, name="get_watch_message"),
-    path("playlist/<slug:playlist_id>/delete", views.delete_playlist, name="delete_playlist"),
+    path("playlist/<slug:playlist_id>/delete-videos/<slug:command>", views.delete_videos, name='delete_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"),
 
     ### STUFF RELATED TO PLAYLISTS IN BULK
     path("search/playlists/<slug:playlist_type>", views.search_playlists, name="search_playlists"),

+ 63 - 9
apps/main/views.py

@@ -430,10 +430,10 @@ def playlists_home(request):
 @login_required
 @require_POST
 def delete_videos(request, playlist_id, command):
-    video_ids = request.POST.getlist("video-id", default=[])
+    playlist_item_ids = request.POST.getlist("video-id", default=[])
 
     print(request.POST)
-    num_vids = len(video_ids)
+    num_vids = len(playlist_item_ids)
     extra_text = " "
     if num_vids == 0:
         return HttpResponse("<h5>Select some videos first!</h5><hr>")
@@ -443,7 +443,7 @@ def delete_videos(request, playlist_id, command):
             command = "confirmed"
 
     if command == "confirm":
-        print(video_ids)
+        print(playlist_item_ids)
 
         if num_vids == request.user.playlists.get(playlist_id=playlist_id).videos.all().count():
             delete_text = "ALL VIDEOS"
@@ -457,15 +457,15 @@ def delete_videos(request, playlist_id, command):
                 <hr>
             """)
     elif command == "confirmed":
-        print(video_ids)
+        print(playlist_item_ids)
         url = f"/from/{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>
             """)
     elif command == "start":
-        print("Deleting", len(video_ids), "videos")
-        Playlist.objects.deletePlaylistItems(request.user, playlist_id, video_ids)
+        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'])
@@ -926,8 +926,11 @@ def update_playlist(request, playlist_id, type):
 
         for playlist_item_id in deleted_playlist_item_ids:
             playlist_item = playlist.playlist_items.select_related('video').get(playlist_item_id=playlist_item_id)
+            video = playlist_item.video
             playlist_changed_text.append(f"--> {playlist_item.video.name}")
             playlist_item.delete()
+            if not playlist.playlist_items.filter(video__video_id=video.video_id).exists():
+                playlist.videos.remove(video)
 
     if len(playlist_changed_text) == 0:
         playlist_changed_text = ["Successfully refreshed playlist! No new changes found!"]
@@ -1064,13 +1067,23 @@ def remove_playlist_tag(request, playlist_id, tag_name):
 def delete_playlist(request, playlist_id):
     playlist = request.user.playlists.get(playlist_id=playlist_id)
 
+    if request.GET["confirmed"] == "no":
+        return HttpResponse(f"""
+            <a href="/playlist/{playlist_id}/delete-playlist?confirmed=yes" class="btn btn-danger">Confirm Delete</a>
+            <a href="/playlist/{playlist_id}" class="btn btn-secondary ms-1">Cancel</a>
+        """)
+
     if not playlist.is_user_owned:  # if playlist trying to delete isn't user owned
         playlist.delete()  # just delete it from untrue
+        messages.success(request, "Successfully deleted playlist from UnTube.")
     else:
-        # delete it from YouTube first then from UnTube
-        pass
+        # deletes it from YouTube first then from UnTube
+        status = Playlist.objects.deletePlaylistFromYouTube(request.user, playlist_id)
+        if status == -1:  # failed to delete playlist from youtube
+            messages.error(request, "Failed to delete playlist from YouTube :(")
+            return redirect('view_playlist_settings', playlist_id=playlist_id)
 
-    messages.success(request, "Successfully deleted playlist from UnTube.")
+        messages.success(request, "Successfully deleted playlist from YouTube and removed it from UnTube as well.")
 
     return redirect('home')
 
@@ -1086,3 +1099,44 @@ def reset_watched(request, playlist_id):
     # messages.success(request, "Successfully marked all videos unwatched.")
 
     return redirect(f'/playlist/{playlist.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"""
+                <span class="text-warning">Mistakes happen. Try again >w<</span>""")
+    elif not playlist_ids:
+        return HttpResponse(f"""
+        <span class="text-danger">First select some playlists to {action} to!</span>""")
+    elif not playlist_item_ids:
+        return HttpResponse(f"""
+                <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!</span>"""
+    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:
+            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:
+            return HttpResponse("Error copying!")
+
+    return HttpResponse(success_message)

+ 0 - 14
templates/base.html

@@ -236,20 +236,6 @@
 
         <script type="application/javascript">
 
-            $(document).ready(function(){
-
-                // multiple choices select search box
-                var multipleCancelButton = new Choices('#choices-multiple-remove-button', {
-                    removeItemButton: true,
-                });
-
-                BackgroundCheck.init({
-                  targets: '.ui',
-                    images: '.ui-img'
-                });
-            });
-
-
             // copy functionality
             var clipboard = new ClipboardJS('.copy-btn');