views.py 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288
  1. import datetime
  2. import random
  3. import bleach
  4. import pytz
  5. from django.db.models import Q, Count
  6. from django.http import HttpResponse
  7. from django.shortcuts import render, redirect, get_object_or_404
  8. from django.utils.html import strip_tags
  9. import apps
  10. from apps.main.models import Playlist, Tag, Video
  11. from django.contrib.auth.decorators import login_required # redirects user to settings.LOGIN_URL
  12. from allauth.socialaccount.models import SocialToken
  13. from django.views.decorators.http import require_POST
  14. from django.contrib import messages
  15. from django.template import loader
  16. from .util import *
  17. # Create your views here.
  18. @login_required
  19. def home(request):
  20. user_profile = request.user
  21. watching = user_profile.playlists.filter(Q(marked_as="watching") & Q(is_in_db=True)).order_by("-num_of_accesses")
  22. recently_accessed_playlists = user_profile.playlists.filter(is_in_db=True).filter(
  23. updated_at__gt=user_profile.profile.updated_at).order_by("-updated_at")[:6]
  24. recently_added_playlists = user_profile.playlists.filter(is_in_db=True).order_by("-created_at")[:6]
  25. #### FOR NEWLY JOINED USERS ######
  26. channel_found = True
  27. if user_profile.profile.show_import_page:
  28. """
  29. Logic:
  30. show_import_page is True by default. When a user logs in for the first time (infact anytime), google
  31. redirects them to 'home' url. Since, show_import_page is True by default, the user is then redirected
  32. from 'home' to 'import_in_progress' url
  33. show_import_page is only set false in the import_in_progress.html page, i.e when user cancels YT import
  34. """
  35. # user_profile.show_import_page = False
  36. if user_profile.profile.access_token.strip() == "" or user_profile.profile.refresh_token.strip() == "":
  37. user_social_token = SocialToken.objects.get(account__user=request.user)
  38. user_profile.profile.access_token = user_social_token.token
  39. user_profile.profile.refresh_token = user_social_token.token_secret
  40. user_profile.profile.expires_at = user_social_token.expires_at
  41. user_profile.save()
  42. Playlist.objects.getUserYTChannelID(user_profile)
  43. if user_profile.profile.imported_yt_playlists:
  44. user_profile.profile.show_import_page = False # after user imports all their YT playlists no need to show_import_page again
  45. user_profile.profile.save(update_fields=['show_import_page'])
  46. return render(request, "home.html", {"import_successful": True})
  47. return render(request, "import_in_progress.html")
  48. # if Playlist.objects.getUserYTChannelID(request.user) == -1: # user channel not found
  49. # channel_found = False
  50. # else:
  51. # Playlist.objects.initPlaylist(request.user, None) # get all playlists from user's YT channel
  52. # return render(request, "home.html", {"import_successful": True})
  53. ##################################
  54. user_playlists = request.user.playlists.filter(is_in_db=True)
  55. total_num_playlists = user_playlists.count()
  56. user_playlists = user_playlists.filter(num_of_accesses__gt=0).order_by(
  57. "-num_of_accesses")
  58. statistics = {
  59. "public_x": 0,
  60. "private_x": 0,
  61. "favorites_x": 0,
  62. "watching_x": 0,
  63. "imported_x": 0
  64. }
  65. if total_num_playlists != 0:
  66. # x means percentage
  67. statistics["public_x"] = round(user_playlists.filter(is_private_on_yt=False).count() / total_num_playlists,
  68. 1) * 100
  69. statistics["private_x"] = round(user_playlists.filter(is_private_on_yt=True).count() / total_num_playlists,
  70. 1) * 100
  71. statistics["favorites_x"] = round(user_playlists.filter(is_favorite=True).count() / total_num_playlists,
  72. 1) * 100
  73. statistics["watching_x"] = round(user_playlists.filter(marked_as="watching").count() / total_num_playlists,
  74. 1) * 100
  75. statistics["imported_x"] = round(user_playlists.filter(is_user_owned=False).count() / total_num_playlists,
  76. 1) * 100
  77. videos = request.user.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=False))
  78. channels = videos.values(
  79. 'channel_name').annotate(channel_videos_count=Count('video_id'))
  80. return render(request, 'home.html', {"channel_found": channel_found,
  81. "user_playlists": user_playlists,
  82. "watching": watching,
  83. "recently_accessed_playlists": recently_accessed_playlists,
  84. "recently_added_playlists": recently_added_playlists,
  85. "statistics": statistics,
  86. "videos": videos,
  87. "channels": channels})
  88. @login_required
  89. def favorites(request):
  90. favorite_playlists = request.user.playlists.filter(Q(is_favorite=True) & Q(is_in_db=True)).order_by('-last_accessed_on')
  91. favorite_videos = request.user.videos.filter(is_favorite=True).order_by('updated_at')
  92. return render(request, 'favorites.html', {"playlists": favorite_playlists,
  93. "videos": favorite_videos})
  94. @login_required
  95. def view_video(request, video_id):
  96. if request.user.videos.filter(video_id=video_id).exists():
  97. video = request.user.videos.get(video_id=video_id)
  98. if video.is_unavailable_on_yt or video.was_deleted_on_yt:
  99. messages.error(request, "Video went private/deleted on YouTube!")
  100. return redirect('home')
  101. video.num_of_accesses += 1
  102. video.save(update_fields=['num_of_accesses'])
  103. return render(request, 'view_video.html', {"video": video})
  104. else:
  105. messages.error(request, "No such video in your UnTube collection!")
  106. return redirect('home')
  107. @login_required
  108. @require_POST
  109. def video_notes(request, video_id):
  110. print(request.POST)
  111. if request.user.videos.filter(video_id=video_id).exists():
  112. video = request.user.videos.get(video_id=video_id)
  113. if 'video-notes-text-area' in request.POST:
  114. video.user_notes = bleach.clean(request.POST['video-notes-text-area'], tags=['br'])
  115. video.save(update_fields=['user_notes', 'user_label'])
  116. # messages.success(request, 'Saved!')
  117. return HttpResponse("""
  118. <div hx-ext="class-tools">
  119. <div classes="add visually-hidden:2s">Saved!</div>
  120. </div>
  121. """)
  122. else:
  123. return HttpResponse('No such video in your UnTube collection!')
  124. @login_required
  125. def view_playlist(request, playlist_id):
  126. user_profile = request.user
  127. user_owned_playlists = user_profile.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True))
  128. # specific playlist requested
  129. if user_profile.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=True)).exists():
  130. playlist = user_profile.playlists.get(playlist_id__exact=playlist_id)
  131. # if its been 30 days since the last playlist visit, force refresh the playlist
  132. if datetime.datetime.now(pytz.utc) - playlist.last_accessed_on > datetime.timedelta(days=15):
  133. playlist.has_playlist_changed = True
  134. # only note down that the playlist as been viewed when 5mins has passed since the last access
  135. if playlist.last_accessed_on + datetime.timedelta(minutes=1) < datetime.datetime.now(pytz.utc):
  136. playlist.num_of_accesses += 1
  137. playlist.last_accessed_on = datetime.datetime.now(pytz.utc)
  138. playlist.save(update_fields=['num_of_accesses', 'last_accessed_on', 'has_playlist_changed'])
  139. else:
  140. if playlist_id == "LL": # liked videos playlist hasnt been imported yet
  141. return render(request, 'view_playlist.html', {"not_imported_LL": True})
  142. messages.error(request, "No such playlist found!")
  143. return redirect('home')
  144. if playlist.has_new_updates:
  145. recently_updated_videos = playlist.videos.filter(video_details_modified=True)
  146. for video in recently_updated_videos:
  147. if video.video_details_modified_at + datetime.timedelta(hours=12) < datetime.datetime.now(
  148. pytz.utc): # expired
  149. video.video_details_modified = False
  150. video.save()
  151. if not recently_updated_videos.exists():
  152. playlist.has_new_updates = False
  153. playlist.save()
  154. playlist_items = playlist.playlist_items.select_related('video').order_by("video_position")
  155. user_created_tags = Tag.objects.filter(created_by=request.user)
  156. playlist_tags = playlist.tags.all()
  157. for tag in playlist_tags:
  158. tag.times_viewed += 1
  159. tag.save(update_fields=['times_viewed'])
  160. unused_tags = user_created_tags.difference(playlist_tags)
  161. return render(request, 'view_playlist.html', {"playlist": playlist,
  162. "playlist_tags": playlist_tags,
  163. "unused_tags": unused_tags,
  164. "playlist_items": playlist_items,
  165. "user_owned_playlists": user_owned_playlists,
  166. "watching_message": generateWatchingMessage(playlist),
  167. })
  168. @login_required
  169. def tagged_playlists(request, tag):
  170. tag = get_object_or_404(Tag, created_by=request.user, name=tag)
  171. playlists = tag.playlists.all()
  172. return render(request, 'all_playlists_with_tag.html', {"playlists": playlists, "tag": tag})
  173. @login_required
  174. def all_playlists(request, playlist_type):
  175. """
  176. Possible playlist types for marked_as attribute: (saved in database like this)
  177. "none", "watching", "plan-to-watch"
  178. """
  179. playlist_type = playlist_type.lower()
  180. watching = False
  181. if playlist_type == "" or playlist_type == "all":
  182. playlists = request.user.playlists.all().filter(is_in_db=True)
  183. playlist_type_display = "All Playlists"
  184. elif playlist_type == "user-owned": # YT playlists owned by user
  185. playlists = request.user.playlists.all().filter(Q(is_user_owned=True) & Q(is_in_db=True))
  186. playlist_type_display = "Your YouTube Playlists"
  187. elif playlist_type == "imported": # YT playlists (public) owned by others
  188. playlists = request.user.playlists.all().filter(Q(is_user_owned=False) & Q(is_in_db=True))
  189. playlist_type_display = "Imported playlists"
  190. elif playlist_type == "favorites": # YT playlists (public) owned by others
  191. playlists = request.user.playlists.all().filter(Q(is_favorite=True) & Q(is_in_db=True))
  192. playlist_type_display = "Favorites"
  193. elif playlist_type.lower() in ["watching", "plan-to-watch"]:
  194. playlists = request.user.playlists.filter(Q(marked_as=playlist_type.lower()) & Q(is_in_db=True))
  195. playlist_type_display = playlist_type.lower().replace("-", " ")
  196. if playlist_type.lower() == "watching":
  197. watching = True
  198. elif playlist_type.lower() == "home": # displays cards of all playlist types
  199. return render(request, 'playlists_home.html')
  200. elif playlist_type.lower() == "random": # randomize playlist
  201. if request.method == "POST":
  202. playlists_type = request.POST["playlistsType"]
  203. if playlists_type == "All":
  204. playlists = request.user.playlists.all().filter(is_in_db=True)
  205. elif playlists_type == "Favorites":
  206. playlists = request.user.playlists.all().filter(Q(is_favorite=True) & Q(is_in_db=True))
  207. elif playlists_type == "Watching":
  208. playlists = request.user.playlists.filter(Q(marked_as="watching") & Q(is_in_db=True))
  209. elif playlists_type == "Plan to Watch":
  210. playlists = request.user.playlists.filter(Q(marked_as="plan-to-watch") & Q(is_in_db=True))
  211. else:
  212. return redirect('/playlists/home')
  213. if not playlists.exists():
  214. messages.info(request, f"No playlists in {playlists_type}")
  215. return redirect('/playlists/home')
  216. random_playlist = random.choice(playlists)
  217. return redirect(f'/playlist/{random_playlist.playlist_id}')
  218. return render(request, 'playlists_home.html')
  219. else:
  220. return redirect('home')
  221. return render(request, 'all_playlists.html', {"playlists": playlists,
  222. "playlist_type": playlist_type,
  223. "playlist_type_display": playlist_type_display,
  224. "watching": watching})
  225. @login_required
  226. def all_videos(request, videos_type):
  227. """
  228. To implement this need to redesign the database
  229. Currently videos -> playlist -> user.profile
  230. Need to do
  231. user.profile <- videos <- playlistItem -> playlist
  232. many ways actually
  233. """
  234. videos_type = videos_type.lower()
  235. if videos_type == "" or videos_type == "all":
  236. playlists = request.user.playlists.all().filter(is_in_db=True)
  237. videos_type_display = "All Videos"
  238. elif videos_type == "user-owned": # YT playlists owned by user
  239. playlists = request.user.playlists.all().filter(Q(is_user_owned=True) & Q(is_in_db=True))
  240. videos_type_display = "All Videos in your YouTube Playlists"
  241. elif videos_type == "imported": # YT playlists (public) owned by others
  242. playlists = request.user.playlists.all().filter(Q(is_user_owned=False) & Q(is_in_db=True))
  243. videos_type_display = "Imported YouTube Playlists Videos"
  244. elif videos_type == "favorites": # YT playlists (public) owned by others
  245. playlists = request.user.playlists.all().filter(Q(is_favorite=True) & Q(is_in_db=True))
  246. videos_type_display = "Favorite Videos"
  247. elif videos_type == "watched": # YT playlists (public) owned by others
  248. playlists = request.user.playlists.all().filter(Q(is_favorite=True) & Q(is_in_db=True))
  249. videos_type_display = "Watched Videos"
  250. elif videos_type == 'hidden-videos': # YT playlists (public) owned by others
  251. playlists = request.user.playlists.all().filter(Q(is_favorite=True) & Q(is_in_db=True))
  252. videos_type_display = "Hidden Videos"
  253. elif videos_type.lower() == "home": # displays cards of all playlist types
  254. return render(request, 'videos_home.html')
  255. else:
  256. return redirect('home')
  257. return render(request, 'all_playlists.html', {"playlists": playlists,
  258. "videos_type": videos_type,
  259. "videos_type_display": videos_type_display})
  260. @login_required
  261. def order_playlist_by(request, playlist_id, order_by):
  262. playlist = request.user.playlists.get(Q(playlist_id=playlist_id) & Q(is_in_db=True))
  263. display_text = "Nothing in this playlist! Add something!" # what to display when requested order/filter has no videws
  264. videos_details = ""
  265. if order_by == "all":
  266. playlist_items = playlist.playlist_items.select_related('video').order_by("video_position")
  267. elif order_by == "favorites":
  268. playlist_items = playlist.playlist_items.select_related('video').filter(video__is_favorite=True).order_by(
  269. "video_position")
  270. videos_details = "Sorted by Favorites"
  271. display_text = "No favorites yet!"
  272. elif order_by == "popularity":
  273. videos_details = "Sorted by Popularity"
  274. playlist_items = playlist.playlist_items.select_related('video').order_by("-video__like_count")
  275. elif order_by == "date-published":
  276. videos_details = "Sorted by Date Published"
  277. playlist_items = playlist.playlist_items.select_related('video').order_by("-published_at")
  278. elif order_by == "views":
  279. videos_details = "Sorted by View Count"
  280. playlist_items = playlist.playlist_items.select_related('video').order_by("-video__view_count")
  281. elif order_by == "has-cc":
  282. videos_details = "Filtered by Has CC"
  283. playlist_items = playlist.playlist_items.select_related('video').filter(video__has_cc=True).order_by(
  284. "video_position")
  285. display_text = "No videos in this playlist have CC :("
  286. elif order_by == "duration":
  287. videos_details = "Sorted by Video Duration"
  288. playlist_items = playlist.playlist_items.select_related('video').order_by("-video__duration_in_seconds")
  289. elif order_by == 'new-updates':
  290. playlist_items = []
  291. videos_details = "Sorted by New Updates"
  292. display_text = "No new updates! Note that deleted videos will not show up here."
  293. if playlist.has_new_updates:
  294. recently_updated_videos = playlist.playlist_items.select_related('video').filter(
  295. video__video_details_modified=True)
  296. for playlist_item in recently_updated_videos:
  297. if playlist_item.video.video_details_modified_at + datetime.timedelta(hours=12) < datetime.datetime.now(
  298. pytz.utc): # expired
  299. playlist_item.video.video_details_modified = False
  300. playlist_item.video.save(update_fields=['video_details_modified'])
  301. if not recently_updated_videos.exists():
  302. playlist.has_new_updates = False
  303. playlist.save(update_fields=['has_new_updates'])
  304. else:
  305. playlist_items = recently_updated_videos.order_by("video_position")
  306. elif order_by == 'unavailable-videos':
  307. playlist_items = playlist.playlist_items.select_related('video').filter(
  308. Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=True))
  309. videos_details = "Sorted by Unavailable Videos"
  310. display_text = "None of the videos in this playlist have gone unavailable... yet."
  311. elif order_by == 'channel':
  312. channel_name = request.GET["channel-name"]
  313. playlist_items = playlist.playlist_items.select_related('video').filter(
  314. video__channel_name=channel_name).order_by("video_position")
  315. videos_details = f"Sorted by Channel '{channel_name}'"
  316. else:
  317. return HttpResponse("Something went wrong :(")
  318. return HttpResponse(loader.get_template("intercooler/videos.html").render({"playlist": playlist,
  319. "playlist_items": playlist_items,
  320. "videos_details": videos_details,
  321. "display_text": display_text,
  322. "order_by": order_by}))
  323. @login_required
  324. def order_playlists_by(request, playlist_type, order_by):
  325. print("GET", request.GET)
  326. print("POST", request.POST)
  327. print("CONTENT PARAMS", request.content_params)
  328. print("HEAD", request.headers)
  329. print("BODY", request.body)
  330. watching = False
  331. if playlist_type == "" or playlist_type.lower() == "all":
  332. playlists = request.user.playlists.all()
  333. elif playlist_type.lower() == "favorites":
  334. playlists = request.user.playlists.filter(Q(is_favorite=True) & Q(is_in_db=True))
  335. elif playlist_type.lower() in ["watching", "plan-to-watch"]:
  336. playlists = request.user.playlists.filter(Q(marked_as=playlist_type.lower()) & Q(is_in_db=True))
  337. if playlist_type.lower() == "watching":
  338. watching = True
  339. elif playlist_type.lower() == "imported":
  340. playlists = request.user.playlists.filter(Q(is_user_owned=False) & Q(is_in_db=True))
  341. elif playlist_type.lower() == "user-owned":
  342. playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True))
  343. else:
  344. return HttpResponse("Not found.")
  345. if order_by == 'recently-accessed':
  346. playlists = playlists.order_by("-updated_at")
  347. elif order_by == 'playlist-duration-in-seconds':
  348. playlists = playlists.order_by("-playlist_duration_in_seconds")
  349. elif order_by == 'video-count':
  350. playlists = playlists.order_by("-video_count")
  351. return HttpResponse(loader.get_template("intercooler/playlists.html")
  352. .render({"playlists": playlists, "watching": watching}))
  353. @login_required
  354. def mark_playlist_as(request, playlist_id, mark_as):
  355. playlist = request.user.playlists.get(playlist_id=playlist_id)
  356. marked_as_response = '<span></span><meta http-equiv="refresh" content="0" />'
  357. if mark_as in ["watching", "on-hold", "plan-to-watch"]:
  358. playlist.marked_as = mark_as
  359. playlist.save()
  360. icon = ""
  361. if mark_as == "watching":
  362. playlist.last_watched = datetime.datetime.now(pytz.utc)
  363. playlist.save(update_fields=['last_watched'])
  364. icon = '<i class="fas fa-fire-alt me-2"></i>'
  365. elif mark_as == "plan-to-watch":
  366. icon = '<i class="fas fa-flag me-2"></i>'
  367. marked_as_response = f'<span class="badge bg-success text-white" >{icon}{mark_as}</span> <meta http-equiv="refresh" content="0" />'
  368. elif mark_as == "none":
  369. playlist.marked_as = mark_as
  370. playlist.save()
  371. elif mark_as == "favorite":
  372. if playlist.is_favorite:
  373. playlist.is_favorite = False
  374. playlist.save()
  375. return HttpResponse('<i class="far fa-star"></i>')
  376. else:
  377. playlist.is_favorite = True
  378. playlist.save()
  379. return HttpResponse('<i class="fas fa-star" style="color: #fafa06"></i>')
  380. else:
  381. return redirect('home')
  382. return HttpResponse(marked_as_response)
  383. @login_required
  384. def playlists_home(request):
  385. return render(request, 'playlists_home.html')
  386. @login_required
  387. @require_POST
  388. def delete_videos(request, playlist_id, command):
  389. all = False
  390. num_vids = 0
  391. playlist_item_ids = []
  392. print(request.POST)
  393. if "all" in request.POST:
  394. if request.POST["all"] == "yes":
  395. all = True
  396. num_vids = request.user.playlists.get(playlist_id=playlist_id).playlist_items.all().count()
  397. if command == "start":
  398. playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in
  399. request.user.playlists.get(playlist_id=playlist_id).playlist_items.all()]
  400. else:
  401. playlist_item_ids = request.POST.getlist("video-id", default=[])
  402. num_vids = len(playlist_item_ids)
  403. extra_text = " "
  404. if num_vids == 0:
  405. return HttpResponse("""
  406. <div hx-ext="class-tools">
  407. <div classes="add visually-hidden:3s">
  408. <h5>Select some videos first!</h5><hr>
  409. </div>
  410. </div>
  411. """)
  412. if 'confirm before deleting' in request.POST:
  413. if request.POST['confirm before deleting'] == 'False':
  414. command = "confirmed"
  415. if command == "confirm":
  416. if all or num_vids == request.user.playlists.get(playlist_id=playlist_id).playlist_items.all().count():
  417. hx_vals = """hx-vals='{"all": "yes"}'"""
  418. delete_text = "ALL VIDEOS"
  419. extra_text = " This will not delete the playlist itself, will only make the playlist empty. "
  420. else:
  421. hx_vals = ""
  422. delete_text = f"{num_vids} videos"
  423. if playlist_id == "LL":
  424. extra_text += "Since you're deleting from your Liked Videos playlist, the selected videos will also be unliked from YouTube. "
  425. url = f"/playlist/{playlist_id}/delete-videos/confirmed"
  426. return HttpResponse(
  427. f"""
  428. <div hx-ext="class-tools">
  429. <div classes="add visually-hidden:30s">
  430. <h5>
  431. Are you sure you want to delete {delete_text} from your YouTube playlist?{extra_text}This cannot be undone.</h5>
  432. <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>
  433. <hr>
  434. </div>
  435. </div>
  436. """)
  437. elif command == "confirmed":
  438. if all:
  439. hx_vals = """hx-vals='{"all": "yes"}'"""
  440. else:
  441. hx_vals = ""
  442. url = f"/playlist/{playlist_id}/delete-videos/start"
  443. return HttpResponse(
  444. f"""
  445. <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>
  446. """)
  447. elif command == "start":
  448. print("Deleting", len(playlist_item_ids), "videos")
  449. Playlist.objects.deletePlaylistItems(request.user, playlist_id, playlist_item_ids)
  450. if all:
  451. help_text = "Finished emptying this playlist."
  452. else:
  453. help_text = "Done deleting selected videos from your playlist on YouTube."
  454. return HttpResponse(f"""
  455. <h5 hx-get="/playlist/{playlist_id}/update/checkforupdates" hx-trigger="load delay:2s" hx-target="#checkforupdates">
  456. {help_text} Refresh page!
  457. </h5>
  458. <hr>
  459. """)
  460. @login_required
  461. @require_POST
  462. def delete_specific_videos(request, playlist_id, command):
  463. Playlist.objects.deleteSpecificPlaylistItems(request.user, playlist_id, command)
  464. help_text = "Error."
  465. if command == "unavailable":
  466. help_text = "Deleted all unavailable videos."
  467. elif command == "duplicate":
  468. help_text = "Deleted all duplicate videos."
  469. return HttpResponse(f"""
  470. <h5>
  471. {help_text} Refresh page!
  472. </h5>
  473. <hr>
  474. """)
  475. @login_required
  476. @require_POST
  477. def search_tagged_playlists(request, tag):
  478. tag = get_object_or_404(Tag, created_by=request.user, name=tag)
  479. playlists = tag.playlists.all()
  480. return HttpResponse("yay")
  481. @login_required
  482. @require_POST
  483. def search_playlists(request, playlist_type):
  484. # print(request.POST) # prints <QueryDict: {'search': ['aa']}>
  485. search_query = request.POST["search"]
  486. watching = False
  487. playlists = None
  488. if playlist_type == "all":
  489. try:
  490. playlists = request.user.playlists.all().filter(Q(name__startswith=search_query) & Q(is_in_db=True))
  491. except:
  492. playlists = request.user.playlists.all()
  493. elif playlist_type == "user-owned": # YT playlists owned by user
  494. try:
  495. playlists = request.user.playlists.filter(
  496. Q(name__startswith=search_query) & Q(is_user_owned=True) & Q(is_in_db=True))
  497. except:
  498. playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True))
  499. elif playlist_type == "imported": # YT playlists (public) owned by others
  500. try:
  501. playlists = request.user.playlists.filter(
  502. Q(name__startswith=search_query) & Q(is_user_owned=False) & Q(is_in_db=True))
  503. except:
  504. playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True))
  505. elif playlist_type == "favorites": # YT playlists (public) owned by others
  506. try:
  507. playlists = request.user.playlists.filter(
  508. Q(name__startswith=search_query) & Q(is_favorite=True) & Q(is_in_db=True))
  509. except:
  510. playlists = request.user.playlists.filter(Q(is_favorite=True) & Q(is_in_db=True))
  511. elif playlist_type in ["watching", "plan-to-watch"]:
  512. try:
  513. playlists = request.user.playlists.filter(
  514. Q(name__startswith=search_query) & Q(marked_as=playlist_type) & Q(is_in_db=True))
  515. except:
  516. playlists = request.user.playlists.all().filter(Q(marked_as=playlist_type) & Q(is_in_db=True))
  517. if playlist_type == "watching":
  518. watching = True
  519. return HttpResponse(loader.get_template("intercooler/playlists.html")
  520. .render({"playlists": playlists,
  521. "watching": watching}))
  522. #### MANAGE VIDEOS #####
  523. @login_required
  524. def mark_video_favortie(request, video_id):
  525. video = request.user.videos.get(video_id=video_id)
  526. if video.is_favorite:
  527. video.is_favorite = False
  528. video.save(update_fields=['is_favorite'])
  529. return HttpResponse('<i class="far fa-heart"></i>')
  530. else:
  531. video.is_favorite = True
  532. video.save(update_fields=['is_favorite'])
  533. return HttpResponse('<i class="fas fa-heart" style="color: #fafa06"></i>')
  534. @login_required
  535. def mark_video_watched(request, playlist_id, video_id):
  536. playlist = request.user.playlists.get(playlist_id=playlist_id)
  537. video = playlist.videos.get(video_id=video_id)
  538. if video.is_marked_as_watched:
  539. video.is_marked_as_watched = False
  540. video.save(update_fields=['is_marked_as_watched'])
  541. return HttpResponse(
  542. f'<i class="far fa-check-circle" hx-get="/playlist/{playlist_id}/get-watch-message" hx-trigger="load" hx-target="#playlist-watch-message"></i>')
  543. else:
  544. video.is_marked_as_watched = True
  545. video.save(update_fields=['is_marked_as_watched'])
  546. playlist.last_watched = datetime.datetime.now(pytz.utc)
  547. playlist.save(update_fields=['last_watched'])
  548. return HttpResponse(
  549. f'<i class="fas fa-check-circle" hx-get="/playlist/{playlist_id}/get-watch-message" hx-trigger="load" hx-target="#playlist-watch-message"></i>')
  550. ###########
  551. @login_required
  552. def search(request):
  553. if request.method == "GET":
  554. return render(request, 'search_untube_page.html', {"playlists": request.user.playlists.all()})
  555. else:
  556. return redirect('home')
  557. @login_required
  558. @require_POST
  559. def search_UnTube(request):
  560. print(request.POST)
  561. search_query = request.POST["search"]
  562. all_playlists = request.user.playlists.filter(is_in_db=True)
  563. if 'playlist-tags' in request.POST:
  564. tags = request.POST.getlist('playlist-tags')
  565. for tag in tags:
  566. all_playlists = all_playlists.filter(tags__name=tag)
  567. # all_playlists = all_playlists.filter(tags__name__in=tags)
  568. playlist_items = []
  569. if request.POST['search-settings'] == 'starts-with':
  570. playlists = all_playlists.filter(Q(name__istartswith=search_query) | Q(
  571. user_label__istartswith=search_query)) if search_query != "" else all_playlists.none()
  572. if search_query != "":
  573. for playlist in all_playlists:
  574. pl_items = playlist.playlist_items.select_related('video').filter(
  575. Q(video__name__istartswith=search_query) | Q(video__user_label__istartswith=search_query) & Q(
  576. is_duplicate=False))
  577. if pl_items.exists():
  578. for v in pl_items.all():
  579. playlist_items.append(v)
  580. else:
  581. playlists = all_playlists.filter(Q(name__icontains=search_query) | Q(
  582. user_label__istartswith=search_query)) if search_query != "" else all_playlists.none()
  583. if search_query != "":
  584. for playlist in all_playlists:
  585. pl_items = playlist.playlist_items.select_related('video').filter(
  586. Q(video__name__icontains=search_query) | Q(video__user_label__istartswith=search_query) & Q(
  587. is_duplicate=False))
  588. if pl_items.exists():
  589. for v in pl_items.all():
  590. playlist_items.append(v)
  591. return HttpResponse(loader.get_template("intercooler/search_untube_results.html")
  592. .render({"playlists": playlists,
  593. "playlist_items": playlist_items,
  594. "videos_count": len(playlist_items),
  595. "search_query": True if search_query != "" else False,
  596. "all_playlists": all_playlists}))
  597. @login_required
  598. def manage_playlists(request):
  599. return render(request, "manage_playlists.html")
  600. @login_required
  601. def manage_view_page(request, page):
  602. if page == "import":
  603. return render(request, "manage_playlists_import.html",
  604. {"manage_playlists_import_textarea": request.user.profile.manage_playlists_import_textarea})
  605. elif page == "create":
  606. return render(request, "manage_playlists_create.html")
  607. else:
  608. return HttpResponse('Working on this!')
  609. @login_required
  610. @require_POST
  611. def manage_save(request, what):
  612. if what == "manage_playlists_import_textarea":
  613. request.user.profile.manage_playlists_import_textarea = request.POST["import-playlist-textarea"]
  614. request.user.save()
  615. return HttpResponse("")
  616. @login_required
  617. @require_POST
  618. def manage_import_playlists(request):
  619. playlist_links = request.POST["import-playlist-textarea"].replace(",", "").split("\n")
  620. num_playlists_already_in_db = 0
  621. num_playlists_initialized_in_db = 0
  622. num_playlists_not_found = 0
  623. new_playlists = []
  624. old_playlists = []
  625. not_found_playlists = []
  626. done = []
  627. for playlist_link in playlist_links:
  628. if playlist_link.strip() != "" and playlist_link.strip() not in done:
  629. pl_id = Playlist.objects.getPlaylistId(playlist_link.strip())
  630. if pl_id is None:
  631. num_playlists_not_found += 1
  632. continue
  633. status = Playlist.objects.initializePlaylist(request.user, pl_id)["status"]
  634. if status == -1 or status == -2:
  635. print("\nNo such playlist found:", pl_id)
  636. num_playlists_not_found += 1
  637. not_found_playlists.append(playlist_link)
  638. elif status == -3: # playlist already in db
  639. num_playlists_already_in_db += 1
  640. playlist = request.user.playlists.get(playlist_id__exact=pl_id)
  641. old_playlists.append(playlist)
  642. else: # only if playlist exists on YT, so import its videos
  643. print(status)
  644. Playlist.objects.getAllVideosForPlaylist(request.user, pl_id)
  645. playlist = request.user.playlists.get(playlist_id__exact=pl_id)
  646. new_playlists.append(playlist)
  647. num_playlists_initialized_in_db += 1
  648. done.append(playlist_link.strip())
  649. request.user.profile.manage_playlists_import_textarea = ""
  650. request.user.save()
  651. return HttpResponse(loader.get_template("intercooler/manage_playlists_import_results.html")
  652. .render(
  653. {"new_playlists": new_playlists,
  654. "old_playlists": old_playlists,
  655. "not_found_playlists": not_found_playlists,
  656. "num_playlists_already_in_db": num_playlists_already_in_db,
  657. "num_playlists_initialized_in_db": num_playlists_initialized_in_db,
  658. "num_playlists_not_found": num_playlists_not_found
  659. }))
  660. @login_required
  661. @require_POST
  662. def manage_create_playlist(request):
  663. print(request.POST)
  664. return HttpResponse("")
  665. @login_required
  666. def load_more_videos(request, playlist_id, order_by, page):
  667. playlist = request.user.playlists.get(playlist_id=playlist_id)
  668. playlist_items = None
  669. if order_by == "all":
  670. playlist_items = playlist.playlist_items.select_related('video').order_by("video_position")
  671. print(f"loading page 1: {playlist_items.count()} videos")
  672. elif order_by == "favorites":
  673. playlist_items = playlist.playlist_items.select_related('video').filter(video__is_favorite=True).order_by(
  674. "video_position")
  675. elif order_by == "popularity":
  676. playlist_items = playlist.playlist_items.select_related('video').order_by("-video__like_count")
  677. elif order_by == "date-published":
  678. playlist_items = playlist.playlist_items.select_related('video').order_by("-published_at")
  679. elif order_by == "views":
  680. playlist_items = playlist.playlist_items.select_related('video').order_by("-video__view_count")
  681. elif order_by == "has-cc":
  682. playlist_items = playlist.playlist_items.select_related('video').filter(video__has_cc=True).order_by(
  683. "video_position")
  684. elif order_by == "duration":
  685. playlist_items = playlist.playlist_items.select_related('video').order_by("-video__duration_in_seconds")
  686. elif order_by == 'new-updates':
  687. playlist_items = []
  688. if playlist.has_new_updates:
  689. recently_updated_videos = playlist.playlist_items.select_related('video').filter(
  690. video__video_details_modified=True)
  691. for playlist_item in recently_updated_videos:
  692. if playlist_item.video.video_details_modified_at + datetime.timedelta(hours=12) < datetime.datetime.now(
  693. pytz.utc): # expired
  694. playlist_item.video.video_details_modified = False
  695. playlist_item.video.save()
  696. if not recently_updated_videos.exists():
  697. playlist.has_new_updates = False
  698. playlist.save()
  699. else:
  700. playlist_items = recently_updated_videos.order_by("video_position")
  701. elif order_by == 'unavailable-videos':
  702. playlist_items = playlist.playlist_items.select_related('video').filter(
  703. Q(video__is_unavailable_on_yt=True) & Q(video__was_deleted_on_yt=True))
  704. elif order_by == 'channel':
  705. channel_name = request.GET["channel-name"]
  706. playlist_items = playlist.playlist_items.select_related('video').filter(
  707. video__channel_name=channel_name).order_by("video_position")
  708. return HttpResponse(loader.get_template("intercooler/videos.html")
  709. .render(
  710. {
  711. "playlist": playlist,
  712. "playlist_items": playlist_items[50 * page:], # only send 50 results per page
  713. "page": page + 1,
  714. "order_by": order_by}))
  715. @login_required
  716. @require_POST
  717. def update_playlist_settings(request, playlist_id):
  718. message_type = "success"
  719. message_content = "Saved!"
  720. print(request.POST)
  721. playlist = request.user.playlists.get(playlist_id=playlist_id)
  722. if "user_label" in request.POST:
  723. playlist.user_label = request.POST["user_label"]
  724. playlist.save(update_fields=['user_label'])
  725. return HttpResponse(loader.get_template("intercooler/messages.html")
  726. .render(
  727. {"message_type": message_type,
  728. "message_content": message_content}))
  729. if 'confirm before deleting' in request.POST:
  730. playlist.confirm_before_deleting = True
  731. else:
  732. playlist.confirm_before_deleting = False
  733. if 'hide videos' in request.POST:
  734. playlist.hide_unavailable_videos = True
  735. else:
  736. playlist.hide_unavailable_videos = False
  737. playlist.save(update_fields=['hide_unavailable_videos', 'confirm_before_deleting'])
  738. valid_title = request.POST['playlistTitle'].replace(">", "greater than").replace("<", "less than")
  739. valid_description = request.POST['playlistDesc'].replace(">", "greater than").replace("<", "less than")
  740. details = {
  741. "title": valid_title,
  742. "description": valid_description,
  743. "privacyStatus": True if request.POST['playlistPrivacy'] == "Private" else False
  744. }
  745. status = Playlist.objects.updatePlaylistDetails(request.user, playlist_id, details)
  746. if status == -1:
  747. message_type = "danger"
  748. message_content = "Could not save :("
  749. return HttpResponse(loader.get_template("intercooler/messages.html")
  750. .render(
  751. {"message_type": message_type,
  752. "message_content": message_content}))
  753. @login_required
  754. def update_playlist(request, playlist_id, command):
  755. playlist = request.user.playlists.get(playlist_id=playlist_id)
  756. if command == "checkforupdates":
  757. print("Checking if playlist changed...")
  758. result = Playlist.objects.checkIfPlaylistChangedOnYT(request.user, playlist_id)
  759. if result[0] == 1: # full scan was done (full scan is done for a playlist if a week has passed)
  760. deleted_videos, unavailable_videos, added_videos = result[1:]
  761. print("CHANGES", deleted_videos, unavailable_videos, added_videos)
  762. # playlist_changed_text = ["The following modifications happened to this playlist on YouTube:"]
  763. if deleted_videos != 0 or unavailable_videos != 0 or added_videos != 0:
  764. pass
  765. # if added_videos > 0:
  766. # playlist_changed_text.append(f"{added_videos} new video(s) were added")
  767. # if deleted_videos > 0:
  768. # playlist_changed_text.append(f"{deleted_videos} video(s) were deleted")
  769. # if unavailable_videos > 0:
  770. # playlist_changed_text.append(f"{unavailable_videos} video(s) went private/unavailable")
  771. # playlist.playlist_changed_text = "\n".join(playlist_changed_text)
  772. # playlist.has_playlist_changed = True
  773. # playlist.save()
  774. else: # no updates found
  775. return HttpResponse("""
  776. <div hx-ext="class-tools">
  777. <div id="checkforupdates" class="sticky-top" style="top: 0.5em;">
  778. <div class="alert alert-success alert-dismissible fade show" classes="add visually-hidden:1s" role="alert">
  779. Playlist upto date!
  780. </div>
  781. </div>
  782. </div>
  783. """)
  784. elif result[0] == -1: # playlist changed
  785. print("!!!Playlist changed")
  786. # current_playlist_vid_count = playlist.video_count
  787. # new_playlist_vid_count = result[1]
  788. # print(current_playlist_vid_count)
  789. # print(new_playlist_vid_count)
  790. # playlist.has_playlist_changed = True
  791. # playlist.save()
  792. # print(playlist.playlist_changed_text)
  793. else: # no updates found
  794. return HttpResponse("""
  795. <div id="checkforupdates" class="sticky-top" style="top: 0.5em;">
  796. <div hx-ext="class-tools">
  797. <div classes="add visually-hidden:2s" class="alert alert-success alert-dismissible fade show sticky-top visually-hidden" role="alert" style="top: 0.5em;">
  798. No new updates!
  799. </div>
  800. </div>
  801. </div>
  802. """)
  803. return HttpResponse(f"""
  804. <div hx-get="/playlist/{playlist_id}/update/auto" hx-trigger="load" hx-target="this" class="sticky-top" style="top: 0.5em;">
  805. <div class="alert alert-success alert-dismissible fade show" role="alert">
  806. <div class="d-flex justify-content-center" id="loading-sign">
  807. <img src="/static/svg-loaders/circles.svg" width="40" height="40">
  808. <h5 class="mt-2 ms-2">Changes detected on YouTube, updating playlist '{playlist.name}'...</h5>
  809. </div>
  810. </div>
  811. </div>
  812. """)
  813. if command == "manual":
  814. print("MANUAL")
  815. return HttpResponse(
  816. f"""<div hx-get="/playlist/{playlist_id}/update/auto" hx-trigger="load" hx-swap="outerHTML">
  817. <div class="d-flex justify-content-center mt-4 mb-3" id="loading-sign">
  818. <img src="/static/svg-loaders/circles.svg" width="40" height="40" style="filter: invert(0%) sepia(18%) saturate(7468%) hue-rotate(241deg) brightness(84%) contrast(101%);">
  819. <h5 class="mt-2 ms-2">Refreshing playlist '{playlist.name}', please wait!</h5>
  820. </div>
  821. </div>""")
  822. print("Attempting to update playlist")
  823. status, deleted_playlist_item_ids, unavailable_videos, added_videos = Playlist.objects.updatePlaylist(request.user,
  824. playlist_id)
  825. playlist = request.user.playlists.get(playlist_id=playlist_id)
  826. if status == -1:
  827. playlist_name = playlist.name
  828. playlist.delete()
  829. return HttpResponse(
  830. f"""
  831. <div class="d-flex justify-content-center mt-4 mb-3" id="loading-sign">
  832. <h5 class="mt-2 ms-2">Looks like the playlist '{playlist_name}' was deleted on YouTube. It has been removed from UnTube as well.</h5>
  833. </div>
  834. """)
  835. print("Updated playlist")
  836. playlist_changed_text = []
  837. if len(added_videos) != 0:
  838. playlist_changed_text.append(f"{len(added_videos)} added")
  839. for video in added_videos:
  840. playlist_changed_text.append(f"--> {video.name}")
  841. # if len(added_videos) > 3:
  842. # playlist_changed_text.append(f"+ {len(added_videos) - 3} more")
  843. if len(unavailable_videos) != 0:
  844. if len(playlist_changed_text) == 0:
  845. playlist_changed_text.append(f"{len(unavailable_videos)} went unavailable")
  846. else:
  847. playlist_changed_text.append(f"\n{len(unavailable_videos)} went unavailable")
  848. for video in unavailable_videos:
  849. playlist_changed_text.append(f"--> {video.name}")
  850. if len(deleted_playlist_item_ids) != 0:
  851. if len(playlist_changed_text) == 0:
  852. playlist_changed_text.append(f"{len(deleted_playlist_item_ids)} deleted")
  853. else:
  854. playlist_changed_text.append(f"\n{len(deleted_playlist_item_ids)} deleted")
  855. for playlist_item_id in deleted_playlist_item_ids:
  856. playlist_item = playlist.playlist_items.select_related('video').get(playlist_item_id=playlist_item_id)
  857. video = playlist_item.video
  858. playlist_changed_text.append(f"--> {playlist_item.video.name}")
  859. playlist_item.delete()
  860. if playlist_id == "LL":
  861. video.liked = False
  862. video.save(update_fields=['liked'])
  863. if not playlist.playlist_items.filter(video__video_id=video.video_id).exists():
  864. playlist.videos.remove(video)
  865. if len(playlist_changed_text) == 0:
  866. playlist_changed_text = [
  867. "Updated playlist and video details to their latest. No new changes found in terms of modifications made to this playlist!"]
  868. # return HttpResponse
  869. return HttpResponse(loader.get_template("intercooler/playlist_updates.html")
  870. .render(
  871. {"playlist_changed_text": "\n".join(playlist_changed_text),
  872. "playlist_id": playlist_id}))
  873. @login_required
  874. def view_playlist_settings(request, playlist_id):
  875. try:
  876. playlist = request.user.playlists.get(playlist_id=playlist_id)
  877. except apps.main.models.Playlist.DoesNotExist:
  878. messages.error(request, "No such playlist found!")
  879. return redirect('home')
  880. return render(request, 'view_playlist_settings.html', {"playlist": playlist})
  881. @login_required
  882. def get_playlist_tags(request, playlist_id):
  883. playlist = request.user.playlists.get(playlist_id=playlist_id)
  884. playlist_tags = playlist.tags.all()
  885. return HttpResponse(loader.get_template("intercooler/playlist_tags.html")
  886. .render(
  887. {"playlist_id": playlist_id,
  888. "playlist_tags": playlist_tags}))
  889. @login_required
  890. def get_unused_playlist_tags(request, playlist_id):
  891. playlist = request.user.playlists.get(playlist_id=playlist_id)
  892. user_created_tags = Tag.objects.filter(created_by=request.user)
  893. playlist_tags = playlist.tags.all()
  894. unused_tags = user_created_tags.difference(playlist_tags)
  895. return HttpResponse(loader.get_template("intercooler/playlist_tags_unused.html")
  896. .render(
  897. {"unused_tags": unused_tags}))
  898. @login_required
  899. def get_watch_message(request, playlist_id):
  900. playlist = request.user.playlists.get(playlist_id=playlist_id)
  901. return HttpResponse(loader.get_template("intercooler/playlist_watch_message.html")
  902. .render(
  903. {"playlist": playlist}))
  904. @login_required
  905. @require_POST
  906. def create_playlist_tag(request, playlist_id):
  907. tag_name = request.POST["createTagField"]
  908. if tag_name.lower() == 'Pick from existing unused tags'.lower():
  909. return HttpResponse("Can't use that! Try again >_<")
  910. playlist = request.user.playlists.get(playlist_id=playlist_id)
  911. user_created_tags = Tag.objects.filter(created_by=request.user)
  912. if not user_created_tags.filter(name__iexact=tag_name).exists(): # no tag found, so create it
  913. tag = Tag(name=tag_name, created_by=request.user)
  914. tag.save()
  915. # add it to playlist
  916. playlist.tags.add(tag)
  917. else:
  918. return HttpResponse("""
  919. Already created. Try Again >w<
  920. """)
  921. # playlist_tags = playlist.tags.all()
  922. # unused_tags = user_created_tags.difference(playlist_tags)
  923. return HttpResponse(f"""
  924. Created and Added!
  925. <span class="visually-hidden" hx-get="/playlist/{playlist_id}/get-tags" hx-trigger="load" hx-target="#playlist-tags"></span>
  926. """)
  927. @login_required
  928. @require_POST
  929. def add_playlist_tag(request, playlist_id):
  930. tag_name = request.POST["playlistTag"]
  931. if tag_name == 'Pick from existing unused tags':
  932. return HttpResponse("Pick something! >w<")
  933. playlist = request.user.playlists.get(playlist_id=playlist_id)
  934. playlist_tags = playlist.tags.all()
  935. if not playlist_tags.filter(name__iexact=tag_name).exists(): # tag not on this playlist, so add it
  936. tag = Tag.objects.filter(Q(created_by=request.user) & Q(name__iexact=tag_name)).first()
  937. # add it to playlist
  938. playlist.tags.add(tag)
  939. else:
  940. return HttpResponse("Already Added >w<")
  941. return HttpResponse(f"""
  942. Added!
  943. <span class="visually-hidden" hx-get="/playlist/{playlist_id}/get-tags" hx-trigger="load" hx-target="#playlist-tags"></span>
  944. """)
  945. @login_required
  946. @require_POST
  947. def remove_playlist_tag(request, playlist_id, tag_name):
  948. playlist = request.user.playlists.get(playlist_id=playlist_id)
  949. playlist_tags = playlist.tags.all()
  950. if playlist_tags.filter(name__iexact=tag_name).exists(): # tag on this playlist, remove it it
  951. tag = Tag.objects.filter(Q(created_by=request.user) & Q(name__iexact=tag_name)).first()
  952. print("Removed tag", tag_name)
  953. # remove it from the playlist
  954. playlist.tags.remove(tag)
  955. else:
  956. return HttpResponse("Whoops >w<")
  957. return HttpResponse("")
  958. @login_required
  959. def delete_playlist(request, playlist_id):
  960. playlist = request.user.playlists.get(playlist_id=playlist_id)
  961. if request.GET["confirmed"] == "no":
  962. return HttpResponse(f"""
  963. <a href="/playlist/{playlist_id}/delete-playlist?confirmed=yes" class="btn btn-danger">Confirm Delete</a>
  964. <a href="/playlist/{playlist_id}" class="btn btn-secondary ms-1">Cancel</a>
  965. """)
  966. if not playlist.is_user_owned: # if playlist trying to delete isn't user owned
  967. playlist.delete() # just delete it from untrue
  968. messages.success(request, "Successfully deleted playlist from UnTube.")
  969. else:
  970. # deletes it from YouTube first then from UnTube
  971. status = Playlist.objects.deletePlaylistFromYouTube(request.user, playlist_id)
  972. if status == -1: # failed to delete playlist from youtube
  973. messages.error(request, "Failed to delete playlist from YouTube :(")
  974. return redirect('view_playlist_settings', playlist_id=playlist_id)
  975. messages.success(request, "Successfully deleted playlist from YouTube and removed it from UnTube as well.")
  976. return redirect('home')
  977. @login_required
  978. def reset_watched(request, playlist_id):
  979. playlist = request.user.playlists.get(playlist_id=playlist_id)
  980. for video in playlist.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=False)):
  981. video.is_marked_as_watched = False
  982. video.save(update_fields=['is_marked_as_watched'])
  983. # messages.success(request, "Successfully marked all videos unwatched.")
  984. return redirect(f'/playlist/{playlist.playlist_id}')
  985. @login_required
  986. @require_POST
  987. def playlist_move_copy_videos(request, playlist_id, action):
  988. playlist_ids = request.POST.getlist("playlist-ids", default=[])
  989. playlist_item_ids = request.POST.getlist("video-id", default=[])
  990. # basic processing
  991. if not playlist_ids and not playlist_item_ids:
  992. return HttpResponse(f"""
  993. <span class="text-warning">Mistakes happen. Try again >w<</span>""")
  994. elif not playlist_ids:
  995. return HttpResponse(f"""
  996. <span class="text-danger">First select some playlists to {action} to!</span>""")
  997. elif not playlist_item_ids:
  998. return HttpResponse(f"""
  999. <span class="text-danger">First select some videos to {action}!</span>""")
  1000. success_message = f"""
  1001. <div hx-ext="class-tools">
  1002. <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)!
  1003. Go visit those playlist(s)!</span>
  1004. </div>
  1005. """
  1006. if action == "move":
  1007. status = Playlist.objects.moveCopyVideosFromPlaylist(request.user,
  1008. from_playlist_id=playlist_id,
  1009. to_playlist_ids=playlist_ids,
  1010. playlist_item_ids=playlist_item_ids,
  1011. action="move")
  1012. if status[0] == -1:
  1013. if status[1] == 404:
  1014. return HttpResponse(
  1015. "<span class='text-danger'>You cannot copy/move unavailable videos! De-select them and try again.</span>")
  1016. return HttpResponse("Error moving!")
  1017. else: # copy
  1018. status = Playlist.objects.moveCopyVideosFromPlaylist(request.user,
  1019. from_playlist_id=playlist_id,
  1020. to_playlist_ids=playlist_ids,
  1021. playlist_item_ids=playlist_item_ids)
  1022. if status[0] == -1:
  1023. if status[1] == 404:
  1024. return HttpResponse(
  1025. "<span class='text-danger'>You cannot copy/move unavailable videos! De-select them and try again.</span>")
  1026. return HttpResponse("Error copying!")
  1027. return HttpResponse(success_message)
  1028. @login_required
  1029. def playlist_open_random_video(request, playlist_id):
  1030. playlist = request.user.playlists.get(playlist_id=playlist_id)
  1031. videos = playlist.videos.all()
  1032. random_video = random.choice(videos)
  1033. return redirect(f'/video/{random_video.video_id}')
  1034. @login_required
  1035. def playlist_completion_times(request, playlist_id):
  1036. playlist_duration = request.user.playlists.get(playlist_id=playlist_id).playlist_duration_in_seconds
  1037. return HttpResponse(f"""
  1038. <h5 class="text-warning">Playlist completion times:</h5>
  1039. <h6>At 1.25x speed: {getHumanizedTimeString(playlist_duration / 1.25)}</h6>
  1040. <h6>At 1.5x speed: {getHumanizedTimeString(playlist_duration / 1.5)}</h6>
  1041. <h6>At 1.75x speed: {getHumanizedTimeString(playlist_duration / 1.75)}</h6>
  1042. <h6>At 2x speed: {getHumanizedTimeString(playlist_duration / 2)}</h6>
  1043. """)
  1044. @login_required
  1045. def video_completion_times(request, video_id):
  1046. video_duration = request.user.videos.get(video_id=video_id).duration_in_seconds
  1047. return HttpResponse(f"""
  1048. <h5 class="text-warning">Video completion times:</h5>
  1049. <h6>At 1.25x speed: {getHumanizedTimeString(video_duration / 1.25)}</h6>
  1050. <h6>At 1.5x speed: {getHumanizedTimeString(video_duration / 1.5)}</h6>
  1051. <h6>At 1.75x speed: {getHumanizedTimeString(video_duration / 1.75)}</h6>
  1052. <h6>At 2x speed: {getHumanizedTimeString(video_duration / 2)}</h6>
  1053. """)