Преглед на файлове

finished watched feature

sleepytaco преди 3 години
родител
ревизия
96c47d3b69

+ 11 - 2
apps/main/models.py

@@ -1072,6 +1072,7 @@ class PlaylistManager(models.Manager):
                 },
             )
 
+            print(details["description"])
             try:
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError as e:  # failed to update playlist details
@@ -1079,6 +1080,8 @@ class PlaylistManager(models.Manager):
                 # 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
 
@@ -1128,8 +1131,14 @@ class Playlist(models.Model):
     user_notes = models.CharField(max_length=420, default="")  # user can take notes on the playlist and save them
     user_label = models.CharField(max_length=100, default="")  # custom user given name for this playlist
 
+    # watch playlist details
+    num_videos_watched = models.IntegerField(default=0)
+    watch_time_left = models.CharField(max_length=150, default="")
+    started_on = models.DateTimeField(auto_now_add=True, null=True)
+    last_watched = models.DateTimeField(auto_now_add=True, null=True)
+
     # manage playlist
-    marked_as = models.CharField(default="",
+    marked_as = models.CharField(default="none",
                                  max_length=100)  # can be set to "none", "watching", "on-hold", "plan-to-watch"
     is_favorite = models.BooleanField(default=False, blank=True)  # to mark playlist as fav
     num_of_accesses = models.IntegerField(default="0")  # tracks num of times this playlist was opened by user
@@ -1201,7 +1210,7 @@ class Video(models.Model):
         default=False)  # True if the video was unavailable (private/deleted) when the API call was first made
     was_deleted_on_yt = models.BooleanField(default=False)  # True if video became unavailable on a subsequent API call
 
-    is_marked_as_watched = models.BooleanField(default=False, blank=True)  # mark video as watched
+    is_marked_as_watched = models.BooleanField(default=False)  # mark video as watched
     is_favorite = models.BooleanField(default=False, blank=True)  # mark video as favorite
     num_of_accesses = models.CharField(max_length=69,
                                        default="0")  # tracks num of times this video was clicked on by user

+ 12 - 7
apps/main/templates/home.html

@@ -80,16 +80,21 @@
                             {% if playlist.is_from_yt %}<small><span class="badge bg-danger text-dark">YT</span></small> {% endif %}
                         </h5>
                         <p class="card-text">
-                            <span class="badge bg-info text-white">15/23 viewed</span>
+                            <span class="badge bg-{% if playlist.watch_time_left == "0secs." %}success{% else %}info{% endif %} text-white">{{ playlist.num_videos_watched }}/{{ playlist.video_count }} viewed</span>
 
-                            <span class="badge bg-dark text-white">3hr. 4min. left</span>
+                            {% if playlist.watch_time_left != "0secs." %}<span class="badge bg-dark text-white">{{ playlist.watch_time_left }} left</span>{% endif %}
 
                         </p>
+                            {% if playlist.tags.all %}
                         <small>
-                            <span class="badge bg-primary rounded-pill">{{ playlist.video_count }} videos</span>
-                            <span class="badge bg-primary rounded-pill">{{ playlist.playlist_duration }} </span>
-                            <span class="badge bg-secondary rounded-pill">{{ playlist.num_of_accesses }} clicks </span>
+                        <i class="fas fa-tags fa-sm" style="color: yellow"></i>
+                            {% for tag in playlist.tags.all %}
+                                <span class="badge rounded-pill bg-primary mb-lg-1">
+                                    {{ tag.name }}
+                                </span>
+                            {% endfor %}
                         </small>
+                            {% endif %}
                         </div>
                         </a>
                     </div>
@@ -116,7 +121,7 @@
                 {% endif %}
 
             <br>
-            <h3><span style="border-bottom: 3px #ffffff dashed;">Most viewed playlists</span> {% if user_playlists.count > 3 %}<a href="{% url 'all_playlists' 'all' %}" class="btn btn-sm btn-info">View All</a>{% endif %}</h3>
+            <h3><span style="border-bottom: 3px #ffffff dashed;">Most viewed playlists</span> {% if user_playlists.count > 3 %}<a href="{% url 'all_playlists' 'all' %}" class="pt-1"><i class="fas fa-binoculars"></i> </a>{% endif %}</h3>
             {% if user_playlists %}
             <div class="row row-cols-1 row-cols-md-3 g-4 text-dark mt-0">
                 {% for playlist in user_playlists|slice:"0:3" %}
@@ -157,7 +162,7 @@
 
 
         <br>
-            <h3><span style="border-bottom: 3px #ffffff dashed;">Recently Accessed</span></h3>
+            <h3><span style="border-bottom: 3px #ffffff dashed;">Recently Accessed</span> <i class="fas fa-redo fa-sm" style="color: #3c3fd2"></i></h3>
 
             {% if recently_accessed_playlists %}
             <div class="row row-cols-1 row-cols-md-3 g-4 text-dark mt-0">

+ 34 - 0
apps/main/templates/intercooler/playlist_watch_message.html

@@ -0,0 +1,34 @@
+<div class="py-2 bg-light rounded-3">
+  <div class="d-flex justify-content-center">
+      <span class="h3 {% if watching_message.percent_complete == 0 %}w-75{% endif %}" style="color: #70777E">
+          {% if watching_message.percent_complete == 0 %}
+              <i class="fas fa-sun fa-spin" style="color: #d0bc0b"></i> Yay, you marked a playlist as watching! This message will keep updating as you mark videos as watched. Good luck! <i class="fas fa-sun fa-spin" style="color: #d0bc0b"></i>
+          {% else %}
+            {% if watching_message.percent_complete == 100 %}
+                <i class="fas fa-flag-checkered" style="color: #2c9c6b"></i>
+            {% endif %}
+          <span style="color: #2c9c2c">{{ watching_message.watched_videos }}</span>
+              /
+              <span {% if watching_message.percent_complete == 100 %}style="color: #2c9c2c"{% endif %}>{{ watching_message.total_videos }}</span> <span {% if watching_message.percent_complete == 100 %}style="color: #2c9c2c"{% endif %}> watched!</span>
+          {% if watching_message.percent_complete != 100 %}<span class="text-dark" style="border-bottom: 3px #e760f1 dashed; {% if watching_message.percent_complete >= 70 and watching_message.percent_complete != 100 %}background: linear-gradient(-45deg, #AE876B, #ABA27B, #A7BC8A, #A3D69A);
+                        background-size: 400% 400%; animation: gradient 7s ease infinite;{% endif %}">{{ watching_message.watch_time_left }}</span> left to go!{% endif %}
+
+            {% if watching_message.percent_complete == 100 %}
+                <i class="fas fa-flag-checkered" style="color: #2c9c6b"></i>
+            {% elif watching_message.percent_complete >= 90  %}
+              <i class="fas fa-glass-cheers" style="color: #9C2C42FF"></i>
+            {% elif watching_message.percent_complete >= 75  %}
+              <i class="fas fa-cocktail" style="color: #9C2C42FF"></i>
+            {% elif watching_message.percent_complete >= 60 %}
+              <i class="fas fa-glass-martini" style="color: #9C2C42FF"></i>
+            {% elif watching_message.percent_complete >= 40 %}
+                <i class="fas fa-glass-martini-alt" style="color: #9C2C42FF"></i>
+            {% elif watching_message.percent_complete >= 20 %}
+              <i class="fas fa-mug-hot" style="color: #9C2C42FF"></i>
+            {% elif watching_message.percent_complete >= 0 %}
+              <i class="fas fa-coffee" style="color: #9C2C42FF"></i>
+            {% endif %}
+          {% endif %}
+      </span>
+  </div>
+</div>

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

@@ -22,11 +22,9 @@
 
         </div>
         {% if playlist.marked_as == "watching" %}
-        <div class="py-2 bg-light">
-          <div class="d-flex justify-content-center">
-              <span class="h3 text-muted">2/23 watched! <span style="border-bottom: 3px #e760f1 dashed;">2hr. 23min.</span> left to go! <i class="fas fa-glass-cheers" style="color: lightcoral"></i></span>
-          </div>
-        </div>
+            <div id="playlist-watch-message">
+                {% include 'intercooler/playlist_watch_message.html' %}
+            </div>
         {% endif %}
     </div>
     <div class="list-group-item list-group-item-action active">
@@ -47,9 +45,9 @@
                     {% if playlist.marked_as != "none" %}
                     <span class="badge bg-success text-white" >
                         {% if playlist.marked_as == "watching" %}
-                            <i class="fas fa-fire-alt me-2"></i>
+                            <i class="fas fa-fire-alt me-1"></i>
                         {% elif playlist.marked_as == "plan-to-watch"%}
-                            <i class="fas fa-flag me-2"></i>
+                            <i class="fas fa-flag me-1"></i>
                         {% endif %}
                         {{ playlist.marked_as }}
                     </span>
@@ -59,7 +57,7 @@
         </div>
         <p class="mb-1">
             {% if playlist.description %}
-            <h5>{{ playlist.description|truncatewords:"50" }}</h5>
+            <h5>{{ playlist.description|truncatewords:"50"|linebreaksbr }}</h5>
             {% else %}
             <h5>No description</h5>
             {% endif %}
@@ -431,19 +429,23 @@
                 <button class="btn btn-sm btn-dark mb-1" type="button" hx-get="{% url 'mark_video_favorite' playlist.playlist_id video.video_id %}" hx-target="#video-{{ forloop.counter }}-fav">
                     <div id="video-{{ forloop.counter }}-fav">
                         {% if video.is_favorite %}
-                            <i class="fas fa-heart"></i>
+                            <i class="fas fa-heart" style="color: #fafa06"></i>
                         {% else %}
                             <i class="far fa-heart"></i>
                         {% endif %}
                     </div>
                 </button>
-                    {% if playlist.marked_as == "watching" %}
-                                        <button class="btn btn-sm btn-light mb-1" type="button">
-
-                        <i class="far fa-check-circle"></i>
-                                        </button>
-
-                    {% endif %}
+                {% if playlist.marked_as == "watching" %}
+                <button class="btn btn-sm btn-light mb-1" type="button" hx-get="{% url 'mark_video_watched' playlist.playlist_id video.video_id %}" hx-target="#video-{{ forloop.counter }}-watched">
+                    <div id="video-{{ forloop.counter }}-watched">
+                        {% if video.is_marked_as_watched %}
+                            <i class="fas fa-check-circle"></i>
+                        {% else %}
+                            <i class="far fa-check-circle"></i>
+                        {% endif %}
+                    </div>
+                </button>
+                {% endif %}
                 {% endif %}
             </div>
           {% endif %}

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

@@ -25,7 +25,8 @@
                       <h6 class="mb-0">Playlist Name</h6>
                     </div>
                     <div class="col-sm-9 text-white-50">
-                        <input type="text" class="form-control" name="playlistTitle" value="{{ playlist.name }}">
+                        <input type="text" class="form-control" name="playlistTitle" value="{{ playlist.name }}" aria-describedby="plTitleHelp">
+                        <div id="plTitleHelp" class="form-text">Make sure the title does not contain '>' or '<'. They will be replaced by 'greater than' or 'less than' if found.</div>
                     </div>
                   </div>
                     <hr>
@@ -41,11 +42,13 @@
                       <hr>
                       <div class="row">
                           <div class="col-sm-3">
-                          <h6 class="mb-0">Playlist Description</h6>
-                        </div>
-                        <div class="col-sm-9 text-white-50">
-                            <textarea class="form-control form-text" name="playlistDesc" rows="6" placeholder="Enter a playlist description here!">{{ playlist.description }}</textarea>
-                        </div>
+                            <h6 class="mb-0">Playlist Description</h6>
+                          </div>
+
+                            <div class="col-sm-9 text-white-50">
+                                <textarea class="form-control form-text" name="playlistDesc" rows="6" placeholder="Enter a playlist description here!" aria-describedby="plDescHelp">{{ playlist.description }}</textarea>
+                                <div id="plDescHelp" class="form-text">Make sure the description does not contain '>' or '<'. They will be replaced by 'greater than' or 'less than' if found.</div>
+                            </div>
                         </div>
                       <hr>
                     <div class="row">

+ 2 - 0
apps/main/urls.py

@@ -15,6 +15,7 @@ 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/notes", views.video_notes, name='video_notes'),
     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'),
 
     ### STUFF RELATED TO ONE PLAYLIST
@@ -32,6 +33,7 @@ urlpatterns = [
     path("playlist/<slug:playlist_id>/remove-tag/<str:tag_name>", views.remove_playlist_tag, name="remove_playlist_tag"),
     path("playlist/<slug:playlist_id>/get-tags", views.get_playlist_tags, name="get_playlist_tags"),
     path("playlist/<slug:playlist_id>/get-unused-tags", views.get_unused_playlist_tags, name="get_unused_playlist_tags"),
+    path("playlist/<slug:playlist_id>/get-watch-message", views.get_watch_message, name="get_watch_message"),
 
     ### STUFF RELATED TO PLAYLISTS IN BULK
     path("search/playlists/<slug:playlist_type>", views.search_playlists, name="search_playlists"),

+ 78 - 9
apps/main/views.py

@@ -1,5 +1,6 @@
 import datetime
 
+import humanize
 import pytz
 from django.db.models import Q
 from django.http import HttpResponse, HttpResponseRedirect
@@ -9,7 +10,44 @@ from django.contrib.auth.decorators import login_required  # redirects user to s
 from allauth.socialaccount.models import SocialToken
 from django.views.decorators.http import require_POST
 from django.contrib import messages
-from django.template import Context, loader
+from django.template import loader
+
+
+def generateWatchingMessage(playlist):
+    """
+    This is the message that will be seen when a playlist is set to watching.
+    """
+    total_playlist_video_count = playlist.video_count
+    num_videos_watched = playlist.videos.filter(is_marked_as_watched=True).count()
+    percent_complete = round((num_videos_watched / total_playlist_video_count) * 100, 1) if total_playlist_video_count != 0 else 100
+
+    print(total_playlist_video_count, num_videos_watched)
+    if num_videos_watched == 0:  # hasnt started watching any videos yet
+        watch_time_left = playlist.playlist_duration.replace(" month,".upper(), "m.").replace(" days,".upper(),
+                                                                                              "d.").replace(
+            " hours,".upper(), "hr.").replace(" minutes".upper(), "mins.").replace(
+            "and".upper(), "").replace(" seconds".upper(), "sec.")
+    elif total_playlist_video_count == num_videos_watched:  # finished watching all videos in the playlist
+        watch_time_left = "0secs."
+    else:
+        watch_time_left = playlist.watch_time_left
+        watched_seconds = 0
+        for video in playlist.videos.filter(is_marked_as_watched=True):
+            watched_seconds += video.duration_in_seconds
+
+        watch_time_left = humanize.precisedelta(datetime.timedelta(seconds=playlist.playlist_duration_in_seconds - watched_seconds)).upper().\
+            replace(" month,".upper(), "m.").replace(" months,".upper(), "m.").replace(" days,".upper(), "d.").replace(" day,".upper(), "d.").replace(" hours,".upper(), "hrs.").replace(" hour,".upper(), "hr.").replace(
+            " minutes".upper(), "mins.").replace(
+            "and".upper(), "").replace(" seconds".upper(), "secs.").replace(" second".upper(), "sec.")
+
+    playlist.watch_time_left = watch_time_left
+    playlist.num_videos_watched = num_videos_watched
+    playlist.save(update_fields=['watch_time_left', 'num_videos_watched'])
+
+    return {"total_videos": total_playlist_video_count,
+            "watched_videos": num_videos_watched,
+            "percent_complete": percent_complete,
+            "watch_time_left": watch_time_left}
 
 
 # Create your views here.
@@ -142,7 +180,8 @@ def view_playlist(request, playlist_id):
                                                   "playlist_tags": playlist_tags,
                                                   "unused_tags": unused_tags,
                                                   "videos": videos,
-                                                  "user_owned_playlists": user_owned_playlists})
+                                                  "user_owned_playlists": user_owned_playlists,
+                                                  "watching_message": generateWatchingMessage(playlist)})
 
 
 @login_required
@@ -277,7 +316,7 @@ def order_playlists_by(request, playlist_type, order_by):
 def mark_playlist_as(request, playlist_id, mark_as):
     playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
 
-    marked_as_response = ""
+    marked_as_response = '<span></span><meta http-equiv="refresh" content="0" />'
 
     if mark_as in ["watching", "on-hold", "plan-to-watch"]:
         playlist.marked_as = mark_as
@@ -409,14 +448,31 @@ def mark_video_favortie(request, playlist_id, video_id):
 
     if video.is_favorite:
         video.is_favorite = False
-        video.save()
+        video.save(update_fields=['is_favorite'])
         return HttpResponse('<i class="far fa-heart"></i>')
     else:
         video.is_favorite = True
-        video.save()
-        return HttpResponse('<i class="fas fa-heart"></i>')
+        video.save(update_fields=['is_favorite'])
+        return HttpResponse('<i class="fas fa-heart" style="color: #fafa06"></i>')
+
 
+def mark_video_watched(request, playlist_id, video_id):
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+    video = playlist.videos.get(video_id=video_id)
+
+    if video.is_marked_as_watched:
+        video.is_marked_as_watched = False
+        video.save(update_fields=['is_marked_as_watched'])
 
+        return HttpResponse(
+            f'<i class="far fa-check-circle" hx-get="/playlist/{playlist_id}/get-watch-message" hx-trigger="load" hx-target="#playlist-watch-message"></i>')
+    else:
+        video.is_marked_as_watched = True
+        video.save(update_fields=['is_marked_as_watched'])
+        return HttpResponse(
+            f'<i class="fas fa-check-circle" hx-get="/playlist/{playlist_id}/get-watch-message" hx-trigger="load" hx-target="#playlist-watch-message"></i>')
+
+    generateWatchingMessage(playlist)
 ###########
 @login_required
 def search(request):
@@ -581,15 +637,17 @@ def update_playlist_settings(request, playlist_id):
             {"message_type": message_type,
              "message_content": message_content}))
 
+    valid_title = request.POST['playlistTitle'].replace(">", "greater than").replace("<", "less than")
+    valid_description = request.POST['playlistDesc'].replace(">", "greater than").replace("<", "less than")
     details = {
-        "title": request.POST['playlistTitle'],
-        "description": request.POST['playlistDesc'],
+        "title": valid_title,
+        "description": valid_description,
         "privacyStatus": True if request.POST['playlistPrivacy'] == "Private" else False
     }
 
     status = Playlist.objects.updatePlaylistDetails(request.user, playlist_id, details)
     if status == -1:
-        message_type = "error"
+        message_type = "danger"
         message_content = "Could not save :("
 
     return HttpResponse(loader.get_template("intercooler/messages.html")
@@ -737,6 +795,7 @@ def view_playlist_settings(request, playlist_id):
     return render(request, 'view_playlist_settings.html', {"playlist": playlist})
 
 
+@login_required
 def get_playlist_tags(request, playlist_id):
     playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
     playlist_tags = playlist.tags.all()
@@ -747,6 +806,7 @@ def get_playlist_tags(request, playlist_id):
          "playlist_tags": playlist_tags}))
 
 
+@login_required
 def get_unused_playlist_tags(request, playlist_id):
     playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
 
@@ -760,6 +820,15 @@ def get_unused_playlist_tags(request, playlist_id):
         {"unused_tags": unused_tags}))
 
 
+@login_required
+def get_watch_message(request, playlist_id):
+    playlist = request.user.profile.playlists.get(playlist_id=playlist_id)
+
+    return HttpResponse(loader.get_template("intercooler/playlist_watch_message.html")
+        .render(
+        {"watching_message": generateWatchingMessage(playlist)}))
+
+
 @login_required
 @require_POST
 def create_playlist_tag(request, playlist_id):

+ 3 - 2
apps/users/templates/settings.html

@@ -100,8 +100,9 @@
                 </div>
                 <div class="col-sm-9 text-white-50">
                     <input type="email" class="form-control" id="email" aria-describedby="emailHelp" value="{{ user.email }}" disabled>
-                    <div id="emailHelp" class="form-text">This is the google account you logged in with.</div>                </div>
-                  </div>
+                    <div id="emailHelp" class="form-text">This is the google account you logged in with.</div>
+                </div>
+              </div>
               <hr>
 
                 <div class="row">