2
0

views.py 51 KB

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