models.py 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499
  1. import requests
  2. from django.contrib.auth.models import User
  3. from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
  4. from apps.users.models import Profile
  5. from .util import *
  6. import pytz
  7. from UnTube.secrets import SECRETS
  8. from django.db import models
  9. from google.oauth2.credentials import Credentials
  10. from google.auth.transport.requests import Request
  11. from datetime import timedelta
  12. from googleapiclient.discovery import build
  13. import googleapiclient.errors
  14. from django.db.models import Q, Sum
  15. class PlaylistManager(models.Manager):
  16. def getCredentials(self, user):
  17. credentials = Credentials(
  18. user.profile.access_token,
  19. refresh_token=user.profile.refresh_token,
  20. # id_token=session.token.get("id_token"),
  21. token_uri="https://oauth2.googleapis.com/token",
  22. client_id=SECRETS["GOOGLE_OAUTH_CLIENT_ID"],
  23. client_secret=SECRETS["GOOGLE_OAUTH_CLIENT_SECRET"],
  24. scopes=SECRETS["GOOGLE_OAUTH_SCOPES"]
  25. )
  26. credentials.expiry = user.profile.expires_at.replace(tzinfo=None)
  27. if not credentials.valid:
  28. # if credentials and credentials.expired and credentials.refresh_token:
  29. credentials.refresh(Request())
  30. user.profile.expires_at = credentials.expiry
  31. user.profile.access_token = credentials.token
  32. user.profile.refresh_token = credentials.refresh_token
  33. user.save()
  34. return credentials
  35. def getPlaylistId(self, video_link):
  36. temp = video_link.split("?")[-1].split("&")
  37. for el in temp:
  38. if "list=" in el:
  39. return el.split("list=")[-1]
  40. # Used to check if the user has a vaild YouTube channel
  41. # Will return -1 if user does not have a YouTube channel
  42. def getUserYTChannelID(self, user):
  43. credentials = self.getCredentials(user)
  44. with build('youtube', 'v3', credentials=credentials) as youtube:
  45. pl_request = youtube.channels().list(
  46. part='id,topicDetails,status,statistics,snippet,localizations,contentOwnerDetails,contentDetails,brandingSettings',
  47. mine=True # get playlist details for this user's playlists
  48. )
  49. pl_response = pl_request.execute()
  50. print(pl_response)
  51. if pl_response['pageInfo']['totalResults'] == 0:
  52. print("Looks like do not have a channel on youtube. Create one to import all of your playlists. Retry?")
  53. return -1
  54. else:
  55. user.profile.yt_channel_id = pl_response['items'][0]['id']
  56. user.save()
  57. return 0
  58. def initPlaylist(self, user, pl_id): # takes in playlist id and saves all of the vids in user's db
  59. credentials = self.getCredentials(user)
  60. with build('youtube', 'v3', credentials=credentials) as youtube:
  61. pl_request = youtube.playlists().list(
  62. part='contentDetails, snippet, id, player, status',
  63. id=pl_id, # get playlist details for this playlist id
  64. maxResults=50
  65. )
  66. # execute the above request, and store the response
  67. try:
  68. pl_response = pl_request.execute()
  69. except googleapiclient.errors.HttpError:
  70. print("YouTube channel not found if mine=True")
  71. print("YouTube playlist not found if id=playlist_id")
  72. return -1
  73. print("Playlist", pl_response)
  74. if pl_response["pageInfo"]["totalResults"] == 0:
  75. print("No playlists created yet on youtube.")
  76. return -2
  77. playlist_items = []
  78. for item in pl_response["items"]:
  79. playlist_items.append(item)
  80. while True:
  81. try:
  82. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  83. pl_response = pl_request.execute()
  84. for item in pl_response["items"]:
  85. playlist_items.append(item)
  86. except AttributeError:
  87. break
  88. for item in playlist_items:
  89. playlist_id = item["id"]
  90. # check if this playlist already exists in user's untube database
  91. if user.playlists.filter(playlist_id=playlist_id).exists():
  92. playlist = user.playlists.get(playlist_id__exact=playlist_id)
  93. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  94. # POSSIBLE CASES:
  95. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  96. # check if playlist count changed on youtube
  97. if playlist.video_count != item['contentDetails']['itemCount']:
  98. playlist.has_playlist_changed = True
  99. playlist.save()
  100. return -3
  101. else: # no such playlist in database
  102. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  103. playlist = Playlist( # create the playlist and link it to current user
  104. playlist_id=playlist_id,
  105. name=item['snippet']['title'],
  106. description=item['snippet']['description'],
  107. published_at=item['snippet']['publishedAt'],
  108. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  109. video_count=item['contentDetails']['itemCount'],
  110. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  111. playlist_yt_player_HTML=item['player']['embedHtml'],
  112. untube_user=user
  113. )
  114. playlist.save()
  115. playlist = user.playlists.get(playlist_id=playlist_id)
  116. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  117. video_ids = [] # stores list of all video ids for a given playlist
  118. with build('youtube', 'v3', credentials=credentials) as youtube:
  119. pl_request = youtube.playlistItems().list(
  120. part='contentDetails, snippet, status',
  121. playlistId=playlist_id, # get all playlist videos details for this playlist id
  122. maxResults=50
  123. )
  124. # execute the above request, and store the response
  125. pl_response = pl_request.execute()
  126. print("Playlist Items", pl_response)
  127. if playlist.channel_id == "":
  128. playlist.channel_id = item['snippet']['channelId']
  129. playlist.channel_name = item['snippet']['channelTitle']
  130. if user.profile.yt_channel_id.strip() != item['snippet']['channelId']:
  131. playlist.is_user_owned = False
  132. playlist.save()
  133. for item in pl_response['items']:
  134. video_id = item['contentDetails']['videoId']
  135. if not playlist.videos.filter(video_id=video_id).exists(): # video DNE
  136. if (item['snippet']['title'] == "Deleted video" and
  137. item['snippet']['description'] == "This video is unavailable.") or (
  138. item['snippet']['title'] == "Private video" and item['snippet'][
  139. 'description'] == "This video is private."):
  140. video = Video(
  141. playlist_item_id=item["id"],
  142. video_id=video_id,
  143. name=item['snippet']['title'],
  144. is_unavailable_on_yt=True,
  145. video_position=item['snippet']['position'] + 1
  146. )
  147. video.save()
  148. else:
  149. video = Video(
  150. playlist_item_id=item["id"],
  151. video_id=video_id,
  152. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  153. item[
  154. 'contentDetails'] else None,
  155. name=item['snippet']['title'],
  156. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  157. channel_id=item['snippet']['videoOwnerChannelId'],
  158. channel_name=item['snippet']['videoOwnerChannelTitle'],
  159. description=item['snippet']['description'],
  160. video_position=item['snippet']['position'] + 1,
  161. )
  162. video.save()
  163. video_ids.append(video_id)
  164. else: # video found in db
  165. video = playlist.videos.get(video_id=video_id)
  166. # check if the video became unavailable on youtube
  167. if (item['snippet']['title'] == "Deleted video" and
  168. item['snippet']['description'] == "This video is unavailable.") or (
  169. item['snippet']['title'] == "Private video" and \
  170. item['snippet']['description'] == "This video is private."):
  171. video.was_deleted_on_yt = True
  172. video.is_duplicate = True
  173. playlist.has_duplicate_videos = True
  174. video.save()
  175. while True:
  176. try:
  177. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  178. pl_response = pl_request.execute()
  179. for item in pl_response['items']:
  180. video_id = item['contentDetails']['videoId']
  181. if not playlist.videos.filter(video_id=video_id).exists(): # video DNE
  182. if (item['snippet']['title'] == "Deleted video" and
  183. item['snippet']['description'] == "This video is unavailable.") or (
  184. item['snippet']['title'] == "Private video" and \
  185. item['snippet']['description'] == "This video is private."):
  186. video = Video(
  187. playlist_item_id=item["id"],
  188. video_id=video_id,
  189. published_at=item['contentDetails'][
  190. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  191. 'contentDetails'] else None,
  192. name=item['snippet']['title'],
  193. is_unavailable_on_yt=True,
  194. video_position=item['snippet']['position'] + 1
  195. )
  196. video.save()
  197. else:
  198. video = Video(
  199. playlist_item_id=item["id"],
  200. video_id=video_id,
  201. published_at=item['contentDetails'][
  202. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  203. 'contentDetails'] else None,
  204. name=item['snippet']['title'],
  205. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  206. channel_id=item['snippet']['videoOwnerChannelId'],
  207. channel_name=item['snippet']['videoOwnerChannelTitle'],
  208. video_position=item['snippet']['position'] + 1,
  209. )
  210. video.save()
  211. video_ids.append(video_id)
  212. else: # video found in db
  213. video = playlist.videos.get(video_id=video_id)
  214. # check if the video became unavailable on youtube
  215. if (item['snippet']['title'] == "Deleted video" and
  216. item['snippet']['description'] == "This video is unavailable.") or (
  217. item['snippet']['title'] == "Private video" and \
  218. item['snippet']['description'] == "This video is private."):
  219. video.was_deleted_on_yt = True
  220. video.is_duplicate = True
  221. playlist.has_duplicate_videos = True
  222. video.save()
  223. except AttributeError:
  224. break
  225. # API expects the video ids to be a string of comma seperated values, not a python list
  226. video_ids_strings = getVideoIdsStrings(video_ids)
  227. print(video_ids)
  228. print(video_ids_strings)
  229. # store duration of all the videos in the playlist
  230. vid_durations = []
  231. for video_ids_string in video_ids_strings:
  232. # query the videos resource using API with the string above
  233. vid_request = youtube.videos().list(
  234. part="contentDetails,player,snippet,statistics", # get details of eac video
  235. id=video_ids_string,
  236. maxResults=50
  237. )
  238. vid_response = vid_request.execute()
  239. print("Videos()", pl_response)
  240. for item in vid_response['items']:
  241. duration = item['contentDetails']['duration']
  242. vid = playlist.videos.get(video_id=item['id'])
  243. if (item['snippet']['title'] == "Deleted video" and
  244. item['snippet'][
  245. 'description'] == "This video is unavailable.") or (
  246. item['snippet']['title'] == "Private video" and item['snippet'][
  247. 'description'] == "This video is private."):
  248. playlist.has_unavailable_videos = True
  249. vid_durations.append(duration)
  250. vid.video_details_modified = True
  251. vid.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  252. vid.save(update_fields=['video_details_modified', 'video_details_modified_at',
  253. 'was_deleted_on_yt', 'is_unavailable_on_yt'])
  254. continue
  255. vid.name = item['snippet']['title']
  256. vid.description = item['snippet']['description']
  257. vid.thumbnail_url = getThumbnailURL(item['snippet']['thumbnails'])
  258. vid.duration = duration.replace("PT", "")
  259. vid.duration_in_seconds = calculateDuration([duration])
  260. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  261. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  262. 'statistics'] else -1
  263. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  264. 'statistics'] else -1
  265. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  266. 'statistics'] else -1
  267. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  268. vid.save()
  269. vid_durations.append(duration)
  270. playlist_duration_in_seconds = calculateDuration(vid_durations)
  271. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  272. playlist.playlist_duration = getHumanizedTimeString(playlist_duration_in_seconds)
  273. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  274. playlist.has_unavailable_videos = True
  275. playlist.is_in_db = True
  276. # playlist.is_user_owned = False
  277. playlist.save()
  278. if pl_id is None:
  279. user.profile.show_import_page = False
  280. user.profile.import_in_progress = False
  281. user.save()
  282. return 0
  283. # Set pl_id as None to retrive all the playlists from authenticated user. Playlists already imported will be skipped by default.
  284. # Set pl_id = <valid playlist id>, to import that specific playlist into the user's account
  285. def initializePlaylist(self, user, pl_id=None):
  286. '''
  287. Retrieves all of user's playlists from YT and stores them in the Playlist model. Note: only stores
  288. the few of the columns of each playlist in every row, and has is_in_db column as false as no videos will be
  289. saved yet.
  290. :param user: django User object
  291. :param pl_id:
  292. :return:
  293. '''
  294. result = {"status": 0,
  295. "num_of_playlists": 0,
  296. "first_playlist_name": "N/A",
  297. "playlist_ids": []}
  298. credentials = self.getCredentials(user)
  299. playlist_ids = []
  300. with build('youtube', 'v3', credentials=credentials) as youtube:
  301. if pl_id is not None:
  302. pl_request = youtube.playlists().list(
  303. part='contentDetails, snippet, id, player, status',
  304. id=pl_id, # get playlist details for this playlist id
  305. maxResults=50
  306. )
  307. else:
  308. print("GETTING ALL USER AUTH PLAYLISTS")
  309. pl_request = youtube.playlists().list(
  310. part='contentDetails, snippet, id, player, status',
  311. mine=True, # get playlist details for this playlist id
  312. maxResults=50
  313. )
  314. # execute the above request, and store the response
  315. try:
  316. pl_response = pl_request.execute()
  317. except googleapiclient.errors.HttpError:
  318. print("YouTube channel not found if mine=True")
  319. print("YouTube playlist not found if id=playlist_id")
  320. result["status"] = -1
  321. return result
  322. if pl_response["pageInfo"]["totalResults"] == 0:
  323. print("No playlists created yet on youtube.")
  324. result["status"] = -2
  325. return result
  326. playlist_items = []
  327. for item in pl_response["items"]:
  328. playlist_items.append(item)
  329. if pl_id is None:
  330. while True:
  331. try:
  332. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  333. pl_response = pl_request.execute()
  334. for item in pl_response["items"]:
  335. playlist_items.append(item)
  336. except AttributeError:
  337. break
  338. result["num_of_playlists"] = len(playlist_items)
  339. result["first_playlist_name"] = playlist_items[0]["snippet"]["title"]
  340. for item in playlist_items:
  341. playlist_id = item["id"]
  342. playlist_ids.append(playlist_id)
  343. # check if this playlist already exists in user's untube collection
  344. if user.playlists.filter(playlist_id=playlist_id).exists():
  345. playlist = user.playlists.get(playlist_id=playlist_id)
  346. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  347. # POSSIBLE CASES:
  348. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  349. # check if playlist count changed on youtube
  350. if playlist.video_count != item['contentDetails']['itemCount']:
  351. playlist.has_playlist_changed = True
  352. playlist.save()
  353. if pl_id is not None:
  354. result["status"] = -3
  355. return result
  356. else: # no such playlist in database
  357. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  358. playlist = Playlist( # create the playlist and link it to current user
  359. playlist_id=playlist_id,
  360. name=item['snippet']['title'],
  361. description=item['snippet']['description'],
  362. published_at=item['snippet']['publishedAt'],
  363. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  364. channel_id=item['snippet']['channelId'] if 'channelId' in
  365. item['snippet'] else '',
  366. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  367. item[
  368. 'snippet'] else '',
  369. video_count=item['contentDetails']['itemCount'],
  370. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  371. playlist_yt_player_HTML=item['player']['embedHtml'],
  372. untube_user=user,
  373. is_user_owned=True if item['snippet']['channelId'] == user.profile.yt_channel_id else False,
  374. is_yt_mix=True if ("My Mix" in item['snippet']['title'] or "Mix -" in item['snippet']['title']) and
  375. item['snippet']['channelId'] == "UCBR8-60-B28hp2BmDPdntcQ" else False
  376. )
  377. playlist.save()
  378. result["playlist_ids"] = playlist_ids
  379. return result
  380. def getAllVideosForPlaylist(self, user, playlist_id):
  381. credentials = self.getCredentials(user)
  382. playlist = user.playlists.get(playlist_id=playlist_id)
  383. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  384. video_ids = [] # stores list of all video ids for a given playlist
  385. with build('youtube', 'v3', credentials=credentials) as youtube:
  386. pl_request = youtube.playlistItems().list(
  387. part='contentDetails, snippet, status',
  388. playlistId=playlist_id, # get all playlist videos details for this playlist id
  389. maxResults=50
  390. )
  391. # execute the above request, and store the response
  392. pl_response = pl_request.execute()
  393. for item in pl_response['items']:
  394. playlist_item_id = item["id"]
  395. video_id = item['contentDetails']['videoId']
  396. video_ids.append(video_id)
  397. # video DNE in user's untube:
  398. # 1. create and save the video in user's untube
  399. # 2. add it to playlist
  400. # 3. make a playlist item which is linked to the video
  401. if not user.videos.filter(video_id=video_id).exists():
  402. if item['snippet']['title'] == "Deleted video" or item['snippet'][
  403. 'description'] == "This video is unavailable." or item['snippet']['title'] == "Private video" or \
  404. item['snippet']['description'] == "This video is private.":
  405. video = Video(
  406. video_id=video_id,
  407. name=item['snippet']['title'],
  408. description=item['snippet']['description'],
  409. is_unavailable_on_yt=True,
  410. untube_user=user
  411. )
  412. video.save()
  413. else:
  414. video = Video(
  415. video_id=video_id,
  416. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  417. item[
  418. 'contentDetails'] else None,
  419. name=item['snippet']['title'],
  420. description=item['snippet']['description'],
  421. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  422. channel_id=item['snippet']['videoOwnerChannelId'],
  423. channel_name=item['snippet']['videoOwnerChannelTitle'],
  424. untube_user=user
  425. )
  426. video.save()
  427. playlist.videos.add(video)
  428. playlist_item = PlaylistItem(
  429. playlist_item_id=playlist_item_id,
  430. published_at=item['snippet']['publishedAt'] if 'publishedAt' in
  431. item[
  432. 'snippet'] else None,
  433. channel_id=item['snippet']['channelId'],
  434. channel_name=item['snippet']['channelTitle'],
  435. video_position=item['snippet']['position'],
  436. playlist=playlist,
  437. video=video
  438. )
  439. playlist_item.save()
  440. else: # video found in user's db
  441. video = user.videos.get(video_id=video_id)
  442. # if video already in playlist.videos
  443. is_duplicate = False
  444. if playlist.videos.filter(video_id=video_id).exists():
  445. playlist.has_duplicate_videos = True
  446. is_duplicate = True
  447. else:
  448. playlist.videos.add(video)
  449. playlist_item = PlaylistItem(
  450. playlist_item_id=playlist_item_id,
  451. published_at=item['snippet']['publishedAt'] if 'publishedAt' in
  452. item[
  453. 'snippet'] else None,
  454. channel_id=item['snippet']['channelId'] if 'channelId' in
  455. item[
  456. 'snippet'] else None,
  457. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  458. item[
  459. 'snippet'] else None,
  460. video_position=item['snippet']['position'],
  461. playlist=playlist,
  462. video=video,
  463. is_duplicate=is_duplicate
  464. )
  465. playlist_item.save()
  466. # check if the video became unavailable on youtube
  467. if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" or
  468. item['snippet'][
  469. 'description'] == "This video is unavailable.") or (
  470. item['snippet']['title'] == "Private video" or item['snippet'][
  471. 'description'] == "This video is private."):
  472. video.was_deleted_on_yt = True
  473. playlist.has_unavailable_videos = True
  474. video.save(update_fields=['was_deleted_on_yt'])
  475. while True:
  476. try:
  477. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  478. pl_response = pl_request.execute()
  479. for item in pl_response['items']:
  480. playlist_item_id = item["id"]
  481. video_id = item['contentDetails']['videoId']
  482. video_ids.append(video_id)
  483. # video DNE in user's untube:
  484. # 1. create and save the video in user's untube
  485. # 2. add it to playlist
  486. # 3. make a playlist item which is linked to the video
  487. if not user.videos.filter(video_id=video_id).exists():
  488. if item['snippet']['title'] == "Deleted video" or item['snippet'][
  489. 'description'] == "This video is unavailable." or item['snippet'][
  490. 'title'] == "Private video" or \
  491. item['snippet']['description'] == "This video is private.":
  492. video = Video(
  493. video_id=video_id,
  494. name=item['snippet']['title'],
  495. description=item['snippet']['description'],
  496. is_unavailable_on_yt=True,
  497. untube_user=user
  498. )
  499. video.save()
  500. else:
  501. video = Video(
  502. video_id=video_id,
  503. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  504. item[
  505. 'contentDetails'] else None,
  506. name=item['snippet']['title'],
  507. description=item['snippet']['description'],
  508. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  509. channel_id=item['snippet']['videoOwnerChannelId'],
  510. channel_name=item['snippet']['videoOwnerChannelTitle'],
  511. untube_user=user
  512. )
  513. video.save()
  514. playlist.videos.add(video)
  515. playlist_item = PlaylistItem(
  516. playlist_item_id=playlist_item_id,
  517. published_at=item['snippet']['publishedAt'] if 'publishedAt' in
  518. item[
  519. 'snippet'] else None,
  520. channel_id=item['snippet']['channelId'],
  521. channel_name=item['snippet']['channelTitle'],
  522. video_position=item['snippet']['position'],
  523. playlist=playlist,
  524. video=video
  525. )
  526. playlist_item.save()
  527. else: # video found in user's db
  528. video = user.videos.get(video_id=video_id)
  529. # if video already in playlist.videos
  530. is_duplicate = False
  531. if playlist.videos.filter(video_id=video_id).exists():
  532. playlist.has_duplicate_videos = True
  533. is_duplicate = True
  534. else:
  535. playlist.videos.add(video)
  536. playlist_item = PlaylistItem(
  537. playlist_item_id=playlist_item_id,
  538. published_at=item['snippet']['publishedAt'] if 'publishedAt' in
  539. item[
  540. 'snippet'] else None,
  541. channel_id=item['snippet']['channelId'] if 'channelId' in
  542. item[
  543. 'snippet'] else None,
  544. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  545. item[
  546. 'snippet'] else None,
  547. video_position=item['snippet']['position'],
  548. playlist=playlist,
  549. video=video,
  550. is_duplicate=is_duplicate
  551. )
  552. playlist_item.save()
  553. # check if the video became unavailable on youtube
  554. if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" or
  555. item['snippet'][
  556. 'description'] == "This video is unavailable.") or (
  557. item['snippet']['title'] == "Private video" or item['snippet'][
  558. 'description'] == "This video is private."):
  559. video.was_deleted_on_yt = True
  560. playlist.has_unavailable_videos = True
  561. video.save(update_fields=['was_deleted_on_yt'])
  562. except AttributeError:
  563. break
  564. # API expects the video ids to be a string of comma seperated values, not a python list
  565. video_ids_strings = getVideoIdsStrings(video_ids)
  566. # store duration of all the videos in the playlist
  567. vid_durations = []
  568. for video_ids_string in video_ids_strings:
  569. # query the videos resource using API with the string above
  570. vid_request = youtube.videos().list(
  571. part="contentDetails,player,snippet,statistics", # get details of eac video
  572. id=video_ids_string,
  573. maxResults=50
  574. )
  575. vid_response = vid_request.execute()
  576. for item in vid_response['items']:
  577. duration = item['contentDetails']['duration']
  578. vid = playlist.videos.get(video_id=item['id'])
  579. vid.name = item['snippet']['title']
  580. vid.description = item['snippet']['description']
  581. vid.thumbnail_url = getThumbnailURL(item['snippet']['thumbnails'])
  582. vid.duration = duration.replace("PT", "")
  583. vid.duration_in_seconds = calculateDuration([duration])
  584. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  585. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  586. 'statistics'] else -1
  587. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  588. 'statistics'] else -1
  589. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  590. 'statistics'] else -1
  591. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  592. vid.save()
  593. vid_durations.append(duration)
  594. playlist_duration_in_seconds = calculateDuration(vid_durations)
  595. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  596. playlist.playlist_duration = getHumanizedTimeString(playlist_duration_in_seconds)
  597. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  598. playlist.has_unavailable_videos = True
  599. playlist.is_in_db = True
  600. playlist.save()
  601. # Returns True if the video count for a playlist on UnTube and video count on same playlist on YouTube is different
  602. def checkIfPlaylistChangedOnYT(self, user, pl_id):
  603. """
  604. If full_scan is true, the whole playlist (i.e each and every video from the PL on YT and PL on UT, is scanned and compared)
  605. is scanned to see if there are any missing/deleted/newly added videos. This will be only be done
  606. weekly by looking at the playlist.last_full_scan_at
  607. If full_scan is False, only the playlist count difference on YT and UT is checked on every visit
  608. to the playlist page. This is done everytime.
  609. """
  610. credentials = self.getCredentials(user)
  611. playlist = user.playlists.get(playlist_id=pl_id)
  612. # if its been a week since the last full scan, do a full playlist scan
  613. # basically checks all the playlist video for any updates
  614. if playlist.last_full_scan_at + datetime.timedelta(minutes=10) < datetime.datetime.now(pytz.utc):
  615. print("DOING A FULL SCAN")
  616. current_video_ids = [playlist_item.video_id for playlist_item in playlist.playlist_items.all()]
  617. current_playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in
  618. playlist.playlist_items.all()]
  619. deleted_videos, unavailable_videos, added_videos = 0, 0, 0
  620. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  621. video_ids = [] # stores list of all video ids for a given playlist
  622. with build('youtube', 'v3', credentials=credentials) as youtube:
  623. pl_request = youtube.playlistItems().list(
  624. part='contentDetails, snippet, status',
  625. playlistId=pl_id, # get all playlist videos details for this playlist id
  626. maxResults=50
  627. )
  628. # execute the above request, and store the response
  629. pl_response = pl_request.execute()
  630. print("PL ITEM", pl_response)
  631. for item in pl_response['items']:
  632. playlist_item_id = item['id']
  633. video_id = item['contentDetails']['videoId']
  634. if not playlist.playlist_items.filter(
  635. playlist_item_id=playlist_item_id).exists(): # if playlist item DNE in playlist, a new vid added to playlist
  636. added_videos += 1
  637. video_ids.append(video_id)
  638. else: # playlist_item found in playlist
  639. if playlist_item_id in current_playlist_item_ids:
  640. video_ids.append(video_id)
  641. current_playlist_item_ids.remove(playlist_item_id)
  642. video = playlist.videos.get(video_id=video_id)
  643. # check if the video became unavailable on youtube
  644. if not video.is_unavailable_on_yt and not video.was_deleted_on_yt:
  645. if (item['snippet']['title'] == "Deleted video" or
  646. item['snippet']['description'] == "This video is unavailable." or
  647. item['snippet']['title'] == "Private video" or item['snippet'][
  648. 'description'] == "This video is private."):
  649. unavailable_videos += 1
  650. while True:
  651. try:
  652. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  653. pl_response = pl_request.execute()
  654. print("PL ITEM", pl_response)
  655. for item in pl_response['items']:
  656. playlist_item_id = item['id']
  657. video_id = item['contentDetails']['videoId']
  658. if not playlist.playlist_items.filter(
  659. playlist_item_id=playlist_item_id).exists(): # if playlist item DNE in playlist, a new vid added to playlist
  660. added_videos += 1
  661. video_ids.append(video_id)
  662. else: # playlist_item found in playlist
  663. if playlist_item_id in current_playlist_item_ids:
  664. video_ids.append(video_id)
  665. current_playlist_item_ids.remove(playlist_item_id)
  666. video = playlist.videos.get(video_id=video_id)
  667. # check if the video became unavailable on youtube
  668. if not video.is_unavailable_on_yt and not video.was_deleted_on_yt:
  669. if (item['snippet']['title'] == "Deleted video" or
  670. item['snippet']['description'] == "This video is unavailable." or
  671. item['snippet']['title'] == "Private video" or item['snippet'][
  672. 'description'] == "This video is private."):
  673. unavailable_videos += 1
  674. except AttributeError:
  675. break
  676. playlist.last_full_scan_at = datetime.datetime.now(pytz.utc)
  677. playlist.save()
  678. deleted_videos = len(current_playlist_item_ids) # left out video ids
  679. return [1, deleted_videos, unavailable_videos, added_videos]
  680. """
  681. print("DOING A SMOL SCAN")
  682. with build('youtube', 'v3', credentials=credentials) as youtube:
  683. pl_request = youtube.playlists().list(
  684. part='contentDetails, snippet, id, status',
  685. id=pl_id, # get playlist details for this playlist id
  686. maxResults=50
  687. )
  688. # execute the above request, and store the response
  689. try:
  690. pl_response = pl_request.execute()
  691. except googleapiclient.errors.HttpError:
  692. print("YouTube channel not found if mine=True")
  693. print("YouTube playlist not found if id=playlist_id")
  694. return -1
  695. print("PLAYLIST", pl_response)
  696. playlist_items = []
  697. for item in pl_response["items"]:
  698. playlist_items.append(item)
  699. while True:
  700. try:
  701. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  702. pl_response = pl_request.execute()
  703. for item in pl_response["items"]:
  704. playlist_items.append(item)
  705. except AttributeError:
  706. break
  707. for item in playlist_items:
  708. playlist_id = item["id"]
  709. # check if this playlist already exists in database
  710. if user.playlists.filter(playlist_id=playlist_id).exists():
  711. playlist = user.playlists.get(playlist_id__exact=playlist_id)
  712. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  713. # POSSIBLE CASES:
  714. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  715. # check if playlist changed on youtube
  716. if playlist.video_count != item['contentDetails']['itemCount']:
  717. playlist.has_playlist_changed = True
  718. playlist.save()
  719. return [-1, item['contentDetails']['itemCount']]
  720. """
  721. return [0, "no change"]
  722. def updatePlaylist(self, user, playlist_id):
  723. credentials = self.getCredentials(user)
  724. playlist = user.playlists.get(playlist_id__exact=playlist_id)
  725. playlist.has_duplicate_videos = False # reset this to false for now
  726. has_duplicate_videos = False
  727. current_video_ids = [playlist_item.video.video_id for playlist_item in playlist.playlist_items.all()]
  728. current_playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in playlist.playlist_items.all()]
  729. updated_playlist_video_count = 0
  730. deleted_playlist_item_ids, unavailable_videos, added_videos = [], [], []
  731. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  732. video_ids = [] # stores list of all video ids for a given playlist
  733. with build('youtube', 'v3', credentials=credentials) as youtube:
  734. pl_request = youtube.playlistItems().list(
  735. part='contentDetails, snippet, status',
  736. playlistId=playlist_id, # get all playlist videos details for this playlist id
  737. maxResults=50
  738. )
  739. # execute the above request, and store the response
  740. try:
  741. pl_response = pl_request.execute()
  742. except googleapiclient.errors.HttpError:
  743. print("Playist was deleted on YouTube")
  744. return [-1, [], [], []]
  745. print("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
  746. updated_playlist_video_count += len(pl_response["items"])
  747. for item in pl_response['items']:
  748. playlist_item_id = item["id"]
  749. video_id = item['contentDetails']['videoId']
  750. video_ids.append(video_id)
  751. # check if new playlist item added
  752. if not playlist.playlist_items.filter(playlist_item_id=playlist_item_id).exists():
  753. # if video dne in user's db at all, create and save it
  754. if not user.videos.filter(video_id=video_id).exists():
  755. if (item['snippet']['title'] == "Deleted video" and item['snippet'][
  756. 'description'] == "This video is unavailable.") or (item['snippet'][
  757. 'title'] == "Private video" and item['snippet']['description'] == "This video is private."):
  758. video = Video(
  759. video_id=video_id,
  760. name=item['snippet']['title'],
  761. description=item['snippet']['description'],
  762. is_unavailable_on_yt=True,
  763. untube_user=user
  764. )
  765. video.save()
  766. else:
  767. video = Video(
  768. video_id=video_id,
  769. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  770. item[
  771. 'contentDetails'] else None,
  772. name=item['snippet']['title'],
  773. description=item['snippet']['description'],
  774. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  775. channel_id=item['snippet']['videoOwnerChannelId'],
  776. channel_name=item['snippet']['videoOwnerChannelTitle'],
  777. untube_user=user
  778. )
  779. video.save()
  780. video = user.videos.get(video_id=video_id)
  781. # check if the video became unavailable on youtube
  782. if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" and
  783. item['snippet'][
  784. 'description'] == "This video is unavailable.") or (
  785. item['snippet']['title'] == "Private video" and item['snippet'][
  786. 'description'] == "This video is private."):
  787. video.was_deleted_on_yt = True
  788. playlist.has_unavailable_videos = True
  789. is_duplicate = False
  790. if not playlist.videos.filter(video_id=video_id).exists():
  791. playlist.videos.add(video)
  792. else:
  793. is_duplicate = True
  794. has_duplicate_videos = True
  795. playlist_item = PlaylistItem(
  796. playlist_item_id=playlist_item_id,
  797. published_at=item['snippet']['publishedAt'] if 'publishedAt' in
  798. item[
  799. 'snippet'] else None,
  800. channel_id=item['snippet']['channelId'] if 'channelId' in
  801. item[
  802. 'snippet'] else None,
  803. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  804. item[
  805. 'snippet'] else None,
  806. video_position=item['snippet']['position'],
  807. playlist=playlist,
  808. video=video,
  809. is_duplicate=is_duplicate
  810. )
  811. playlist_item.save()
  812. video.video_details_modified = True
  813. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  814. video.save(update_fields=['video_details_modified', 'video_details_modified_at', 'was_deleted_on_yt'])
  815. added_videos.append(video)
  816. else: # if playlist item already in playlist
  817. current_playlist_item_ids.remove(playlist_item_id)
  818. playlist_item = playlist.playlist_items.get(playlist_item_id=playlist_item_id)
  819. playlist_item.video_position = item['snippet']['position']
  820. # check if the video became unavailable on youtube
  821. if not playlist_item.video.is_unavailable_on_yt and not playlist_item.video.was_deleted_on_yt:
  822. if (item['snippet']['title'] == "Deleted video" and
  823. item['snippet']['description'] == "This video is unavailable.") or (
  824. item['snippet']['title'] == "Private video" and item['snippet'][
  825. 'description'] == "This video is private."):
  826. playlist_item.video.was_deleted_on_yt = True # video went private on YouTube
  827. playlist_item.video.video_details_modified = True
  828. playlist_item.video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  829. playlist_item.video.save(update_fields=['was_deleted_on_yt', 'video_details_modified',
  830. 'video_details_modified_at'])
  831. unavailable_videos.append(playlist_item.video)
  832. while True:
  833. try:
  834. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  835. pl_response = pl_request.execute()
  836. updated_playlist_video_count += len(pl_response["items"])
  837. for item in pl_response['items']:
  838. playlist_item_id = item["id"]
  839. video_id = item['contentDetails']['videoId']
  840. video_ids.append(video_id)
  841. # check if new playlist item added
  842. if not playlist.playlist_items.filter(playlist_item_id=playlist_item_id).exists():
  843. # if video dne in user's db at all, create and save it
  844. if not user.videos.filter(video_id=video_id).exists():
  845. if (item['snippet']['title'] == "Deleted video" and item['snippet'][
  846. 'description'] == "This video is unavailable.") or (item['snippet'][
  847. 'title'] == "Private video" and
  848. item['snippet'][
  849. 'description'] == "This video is private."):
  850. video = Video(
  851. video_id=video_id,
  852. name=item['snippet']['title'],
  853. description=item['snippet']['description'],
  854. is_unavailable_on_yt=True,
  855. untube_user=user
  856. )
  857. video.save()
  858. else:
  859. video = Video(
  860. video_id=video_id,
  861. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  862. item[
  863. 'contentDetails'] else None,
  864. name=item['snippet']['title'],
  865. description=item['snippet']['description'],
  866. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  867. channel_id=item['snippet']['videoOwnerChannelId'],
  868. channel_name=item['snippet']['videoOwnerChannelTitle'],
  869. untube_user=user
  870. )
  871. video.save()
  872. video = user.videos.get(video_id=video_id)
  873. # check if the video became unavailable on youtube
  874. if not video.is_unavailable_on_yt and not video.was_deleted_on_yt and (item['snippet']['title'] == "Deleted video" and
  875. item['snippet'][
  876. 'description'] == "This video is unavailable.") or (
  877. item['snippet']['title'] == "Private video" and item['snippet'][
  878. 'description'] == "This video is private."):
  879. video.was_deleted_on_yt = True
  880. playlist.has_unavailable_videos = True
  881. is_duplicate = False
  882. if not playlist.videos.filter(video_id=video_id).exists():
  883. playlist.videos.add(video)
  884. else:
  885. is_duplicate = True
  886. has_duplicate_videos = True
  887. playlist_item = PlaylistItem(
  888. playlist_item_id=playlist_item_id,
  889. published_at=item['snippet']['publishedAt'] if 'publishedAt' in
  890. item[
  891. 'snippet'] else None,
  892. channel_id=item['snippet']['channelId'] if 'channelId' in
  893. item[
  894. 'snippet'] else None,
  895. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  896. item[
  897. 'snippet'] else None,
  898. video_position=item['snippet']['position'],
  899. playlist=playlist,
  900. video=video,
  901. is_duplicate=is_duplicate
  902. )
  903. playlist_item.save()
  904. video.video_details_modified = True
  905. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  906. video.save(update_fields=['video_details_modified', 'video_details_modified_at',
  907. 'was_deleted_on_yt'])
  908. added_videos.append(video)
  909. else: # if playlist item already in playlist
  910. current_playlist_item_ids.remove(playlist_item_id)
  911. playlist_item = playlist.playlist_items.get(playlist_item_id=playlist_item_id)
  912. playlist_item.video_position = item['snippet']['position']
  913. # check if the video became unavailable on youtube
  914. if not playlist_item.video.is_unavailable_on_yt and not playlist_item.video.was_deleted_on_yt:
  915. if (item['snippet']['title'] == "Deleted video" and
  916. item['snippet']['description'] == "This video is unavailable.") or (
  917. item['snippet']['title'] == "Private video" and item['snippet'][
  918. 'description'] == "This video is private."):
  919. playlist_item.video.was_deleted_on_yt = True # video went private on YouTube
  920. playlist_item.video.video_details_modified = True
  921. playlist_item.video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  922. playlist_item.video.save(
  923. update_fields=['was_deleted_on_yt', 'video_details_modified',
  924. 'video_details_modified_at'])
  925. unavailable_videos.append(playlist_item.video)
  926. except AttributeError:
  927. break
  928. # API expects the video ids to be a string of comma seperated values, not a python list
  929. video_ids_strings = getVideoIdsStrings(video_ids)
  930. # store duration of all the videos in the playlist
  931. vid_durations = []
  932. for video_ids_string in video_ids_strings:
  933. # query the videos resource using API with the string above
  934. vid_request = youtube.videos().list(
  935. part="contentDetails,player,snippet,statistics", # get details of eac video
  936. id=video_ids_string,
  937. maxResults=50
  938. )
  939. vid_response = vid_request.execute()
  940. for item in vid_response['items']:
  941. duration = item['contentDetails']['duration']
  942. vid = playlist.videos.get(video_id=item['id'])
  943. if (item['snippet']['title'] == "Deleted video" or
  944. item['snippet'][
  945. 'description'] == "This video is unavailable.") or (
  946. item['snippet']['title'] == "Private video" or item['snippet'][
  947. 'description'] == "This video is private."):
  948. playlist.has_unavailable_videos = True
  949. vid_durations.append(duration)
  950. vid.video_details_modified = True
  951. vid.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  952. vid.save(
  953. update_fields=['video_details_modified', 'video_details_modified_at', 'was_deleted_on_yt',
  954. 'is_unavailable_on_yt'])
  955. continue
  956. vid.name = item['snippet']['title']
  957. vid.description = item['snippet']['description']
  958. vid.thumbnail_url = getThumbnailURL(item['snippet']['thumbnails'])
  959. vid.duration = duration.replace("PT", "")
  960. vid.duration_in_seconds = calculateDuration([duration])
  961. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  962. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  963. 'statistics'] else -1
  964. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  965. 'statistics'] else -1
  966. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  967. 'statistics'] else -1
  968. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  969. vid.save()
  970. vid_durations.append(duration)
  971. playlist_duration_in_seconds = calculateDuration(vid_durations)
  972. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  973. playlist.playlist_duration = getHumanizedTimeString(playlist_duration_in_seconds)
  974. if len(video_ids) != len(vid_durations) or len(
  975. unavailable_videos) != 0: # that means some videos in the playlist became private/deleted
  976. playlist.has_unavailable_videos = True
  977. playlist.has_playlist_changed = False
  978. playlist.video_count = updated_playlist_video_count
  979. playlist.has_new_updates = True
  980. playlist.save()
  981. playlist.has_duplicate_videos = has_duplicate_videos
  982. deleted_playlist_item_ids = current_playlist_item_ids # left out playlist_item_ids
  983. return [0, deleted_playlist_item_ids, unavailable_videos, added_videos]
  984. def deletePlaylistItems(self, user, playlist_id, playlist_item_ids):
  985. """
  986. Takes in playlist itemids for the videos in a particular playlist
  987. """
  988. credentials = self.getCredentials(user)
  989. playlist = user.playlists.get(playlist_id=playlist_id)
  990. # new_playlist_duration_in_seconds = playlist.playlist_duration_in_seconds
  991. # new_playlist_video_count = playlist.video_count
  992. with build('youtube', 'v3', credentials=credentials) as youtube:
  993. for playlist_item_id in playlist_item_ids:
  994. pl_request = youtube.playlistItems().delete(
  995. id=playlist_item_id
  996. )
  997. print(pl_request)
  998. try:
  999. pl_response = pl_request.execute()
  1000. print(pl_response)
  1001. except googleapiclient.errors.HttpError as e: # failed to delete playlist item
  1002. # possible causes:
  1003. # playlistItemsNotAccessible (403)
  1004. # playlistItemNotFound (404)
  1005. # playlistOperationUnsupported (400)
  1006. print(e, e.error_details, e.status_code)
  1007. continue
  1008. # playlistItem was successfully deleted if no HttpError, so delete it from db
  1009. # video = playlist.videos.get(playlist_item_id=playlist_item_id)
  1010. # new_playlist_video_count -= 1
  1011. # new_playlist_duration_in_seconds -= video.duration_in_seconds
  1012. # video.delete()
  1013. # playlist.video_count = new_playlist_video_count
  1014. # playlist.playlist_duration_in_seconds = new_playlist_duration_in_seconds
  1015. # playlist.playlist_duration = getHumanizedTimeString(new_playlist_duration_in_seconds)
  1016. # playlist.save(update_fields=['video_count', 'playlist_duration', 'playlist_duration_in_seconds'])
  1017. # time.sleep(2)
  1018. def updatePlaylistDetails(self, user, playlist_id, details):
  1019. """
  1020. Takes in playlist itemids for the videos in a particular playlist
  1021. """
  1022. credentials = self.getCredentials(user)
  1023. playlist = user.playlists.get(playlist_id=playlist_id)
  1024. with build('youtube', 'v3', credentials=credentials) as youtube:
  1025. pl_request = youtube.playlists().update(
  1026. part="id,snippet,status",
  1027. body={
  1028. "id": playlist_id,
  1029. "snippet": {
  1030. "title": details["title"],
  1031. "description": details["description"],
  1032. },
  1033. "status": {
  1034. "privacyStatus": "private" if details["privacyStatus"] else "public"
  1035. }
  1036. },
  1037. )
  1038. print(details["description"])
  1039. try:
  1040. pl_response = pl_request.execute()
  1041. except googleapiclient.errors.HttpError as e: # failed to update playlist details
  1042. # possible causes:
  1043. # playlistItemsNotAccessible (403)
  1044. # playlistItemNotFound (404)
  1045. # playlistOperationUnsupported (400)
  1046. # errors i ran into:
  1047. # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
  1048. print("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
  1049. return -1
  1050. print(pl_response)
  1051. playlist.name = pl_response['snippet']['title']
  1052. playlist.description = pl_response['snippet']['description']
  1053. playlist.is_private_on_yt = True if pl_response['status']['privacyStatus'] == "private" else False
  1054. playlist.save(update_fields=['name', 'description', 'is_private_on_yt'])
  1055. return 0
  1056. class Tag(models.Model):
  1057. name = models.CharField(max_length=69)
  1058. created_by = models.ForeignKey(User, related_name="playlist_tags", on_delete=models.CASCADE)
  1059. times_viewed = models.IntegerField(default=0)
  1060. # type = models.CharField(max_length=10) # either 'playlist' or 'video'
  1061. created_at = models.DateTimeField(auto_now_add=True)
  1062. updated_at = models.DateTimeField(auto_now=True)
  1063. class Channel(models.Model):
  1064. channel_id = models.CharField(max_length=420, default="")
  1065. name = models.CharField(max_length=420, default="")
  1066. description = models.CharField(max_length=420, default="No description")
  1067. thumbnail_url = models.CharField(max_length=420, blank=True)
  1068. published_at = models.DateTimeField(blank=True)
  1069. # statistics
  1070. view_count = models.IntegerField(default=0)
  1071. subscriberCount = models.IntegerField(default=0)
  1072. hidden_subscriber_count = models.BooleanField(null=True)
  1073. video_ount = models.IntegerField(default=0)
  1074. is_private = models.BooleanField(null=True)
  1075. created_at = models.DateTimeField(auto_now_add=True)
  1076. updated_at = models.DateTimeField(auto_now=True)
  1077. class Video(models.Model):
  1078. untube_user = models.ForeignKey(User, related_name="videos", on_delete=models.CASCADE, null=True)
  1079. # video details
  1080. video_id = models.CharField(max_length=100)
  1081. name = models.CharField(max_length=100, blank=True)
  1082. duration = models.CharField(max_length=100, blank=True)
  1083. duration_in_seconds = models.IntegerField(default=0)
  1084. thumbnail_url = models.CharField(max_length=420, blank=True)
  1085. published_at = models.DateTimeField(blank=True, null=True)
  1086. description = models.CharField(max_length=420, default="")
  1087. has_cc = models.BooleanField(default=False, blank=True, null=True)
  1088. # video stats
  1089. public_stats_viewable = models.BooleanField(default=True)
  1090. view_count = models.IntegerField(default=0)
  1091. like_count = models.IntegerField(default=0)
  1092. dislike_count = models.IntegerField(default=0)
  1093. comment_count = models.IntegerField(default=0)
  1094. yt_player_HTML = models.CharField(max_length=420, blank=True)
  1095. # video is made by this channel
  1096. # channel = models.ForeignKey(Channel, related_name="videos", on_delete=models.CASCADE)
  1097. channel_id = models.CharField(max_length=420, blank=True)
  1098. channel_name = models.CharField(max_length=420, blank=True)
  1099. # which playlist this video belongs to, and position of that video in the playlist (i.e ALL videos belong to some pl)
  1100. # playlist = models.ForeignKey(Playlist, related_name="videos", on_delete=models.CASCADE)
  1101. # (moved to playlistItem)
  1102. # is_duplicate = models.BooleanField(default=False) # True if the same video exists more than once in the playlist
  1103. # video_position = models.IntegerField(blank=True)
  1104. # NOTE: For a video in db:
  1105. # 1.) if both is_unavailable_on_yt and was_deleted_on_yt are true,
  1106. # that means the video was originally fine, but then went unavailable when updatePlaylist happened
  1107. # 2.) if only is_unavailable_on_yt is true and was_deleted_on_yt is false,
  1108. # then that means the video was an unavaiable video when initPlaylist was happening
  1109. # 3.) if both is_unavailable_on_yt and was_deleted_on_yt are false, the video is fine, ie up on Youtube
  1110. is_unavailable_on_yt = models.BooleanField(
  1111. default=False) # True if the video was unavailable (private/deleted) when the API call was first made
  1112. was_deleted_on_yt = models.BooleanField(default=False) # True if video became unavailable on a subsequent API call
  1113. is_pinned = models.BooleanField(default=False)
  1114. is_marked_as_watched = models.BooleanField(default=False) # mark video as watched
  1115. is_favorite = models.BooleanField(default=False, blank=True) # mark video as favorite
  1116. num_of_accesses = models.CharField(max_length=69,
  1117. default="0") # tracks num of times this video was clicked on by user
  1118. user_label = models.CharField(max_length=100, default="") # custom user given name for this video
  1119. user_notes = models.CharField(max_length=420, default="") # user can take notes on the video and save them
  1120. created_at = models.DateTimeField(auto_now_add=True)
  1121. updated_at = models.DateTimeField(auto_now=True)
  1122. # for new videos added/modified/deleted in the playlist
  1123. video_details_modified = models.BooleanField(
  1124. default=False) # is true for videos whose details changed after playlist update
  1125. video_details_modified_at = models.DateTimeField(auto_now_add=True) # to set the above false after a day
  1126. class Playlist(models.Model):
  1127. tags = models.ManyToManyField(Tag, related_name="playlists")
  1128. untube_user = models.ForeignKey(User, related_name="playlists", on_delete=models.CASCADE, null=True)
  1129. # playlist is made by this channel
  1130. channel_id = models.CharField(max_length=420, blank=True)
  1131. channel_name = models.CharField(max_length=420, blank=True)
  1132. # playlist details
  1133. is_yt_mix = models.BooleanField(default=False)
  1134. playlist_id = models.CharField(max_length=150)
  1135. name = models.CharField(max_length=150, blank=True) # YT PLAYLIST NAMES CAN ONLY HAVE MAX OF 150 CHARS
  1136. thumbnail_url = models.CharField(max_length=420, blank=True)
  1137. description = models.CharField(max_length=420, default="No description")
  1138. video_count = models.IntegerField(default=0)
  1139. published_at = models.DateTimeField(blank=True)
  1140. is_private_on_yt = models.BooleanField(default=False)
  1141. videos = models.ManyToManyField(Video, related_name="playlists")
  1142. # eg. "<iframe width=\"640\" height=\"360\" src=\"http://www.youtube.com/embed/videoseries?list=PLFuZstFnF1jFwMDffUhV81h0xeff0TXzm\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>"
  1143. playlist_yt_player_HTML = models.CharField(max_length=420, blank=True)
  1144. playlist_duration = models.CharField(max_length=69, blank=True) # string version of playlist dureation
  1145. playlist_duration_in_seconds = models.IntegerField(default=0)
  1146. has_unavailable_videos = models.BooleanField(default=False) # if videos in playlist are private/deleted
  1147. has_duplicate_videos = models.BooleanField(default=False) # duplicate videos will not be shown on site
  1148. # watch playlist details
  1149. # watch_time_left = models.CharField(max_length=150, default="")
  1150. started_on = models.DateTimeField(auto_now_add=True, null=True)
  1151. last_watched = models.DateTimeField(auto_now_add=True, null=True)
  1152. # manage playlist
  1153. is_pinned = models.BooleanField(default=False)
  1154. user_notes = models.CharField(max_length=420, default="") # user can take notes on the playlist and save them
  1155. user_label = models.CharField(max_length=100, default="") # custom user given name for this playlist
  1156. marked_as = models.CharField(default="none",
  1157. max_length=100) # can be set to "none", "watching", "on-hold", "plan-to-watch"
  1158. is_favorite = models.BooleanField(default=False, blank=True) # to mark playlist as fav
  1159. num_of_accesses = models.IntegerField(default="0") # tracks num of times this playlist was opened by user
  1160. last_accessed_on = models.DateTimeField(default=datetime.datetime.now)
  1161. is_user_owned = models.BooleanField(default=True) # represents YouTube playlist owned by user
  1162. # set playlist manager
  1163. objects = PlaylistManager()
  1164. # playlist settings
  1165. hide_unavailable_videos = models.BooleanField(default=False)
  1166. confirm_before_deleting = models.BooleanField(default=True)
  1167. # for import
  1168. is_in_db = models.BooleanField(default=False) # is true when all the videos of a playlist have been imported
  1169. created_at = models.DateTimeField(auto_now_add=True)
  1170. updated_at = models.DateTimeField(auto_now=True)
  1171. # for updates
  1172. last_full_scan_at = models.DateTimeField(auto_now_add=True)
  1173. has_playlist_changed = models.BooleanField(default=False) # determines whether playlist was modified online or not
  1174. has_new_updates = models.BooleanField(default=False) # meant to keep track of newly added/unavailable videos
  1175. def __str__(self):
  1176. return str(self.playlist_id)
  1177. def get_channels_list(self):
  1178. channels_list = []
  1179. num_channels = 0
  1180. for video in self.videos.all():
  1181. channel = video.channel_name
  1182. if channel not in channels_list:
  1183. channels_list.append(channel)
  1184. num_channels += 1
  1185. return [num_channels, channels_list]
  1186. def generate_playlist_thumbnail_url(self):
  1187. pl_name = self.name
  1188. response = requests.get(
  1189. f'https://api.unsplash.com/search/photos/?client_id={SECRETS["UNSPLASH_API_ACCESS_KEY"]}&page=1&query={pl_name}')
  1190. image = response.json()["results"][0]["urls"]["small"]
  1191. print(image)
  1192. return image
  1193. def get_unavailable_videos_count(self):
  1194. return self.video_count - self.get_watchable_videos_count()
  1195. # return count of watchable videos, i.e # videos that are not private or deleted in the playlist
  1196. def get_watchable_videos_count(self):
  1197. return self.playlist_items.filter(Q(is_duplicate=False) & Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False)).count()
  1198. def get_watched_videos_count(self):
  1199. return self.playlist_items.filter(Q(is_duplicate=False) &
  1200. Q(video__is_marked_as_watched=True) & Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False)).count()
  1201. # diff of time from when playlist was first marked as watched and playlist reached 100% completion
  1202. def get_finish_time(self):
  1203. return self.last_watched - self.started_on
  1204. def get_watch_time_left(self):
  1205. unwatched_playlist_items_secs = self.playlist_items.filter(Q(is_duplicate=False) &
  1206. Q(video__is_marked_as_watched=False) &
  1207. Q(video__is_unavailable_on_yt=False) &
  1208. Q(video__was_deleted_on_yt=False)).aggregate(Sum('video__duration_in_seconds'))['video__duration_in_seconds__sum']
  1209. watch_time_left = getHumanizedTimeString(unwatched_playlist_items_secs) if unwatched_playlist_items_secs is not None else getHumanizedTimeString(0)
  1210. return watch_time_left
  1211. # return 0 if playlist empty or all videos in playlist are unavailable
  1212. def get_percent_complete(self):
  1213. total_playlist_video_count = self.get_watchable_videos_count()
  1214. watched_videos = self.playlist_items.filter(Q(is_duplicate=False) &
  1215. Q(video__is_marked_as_watched=True) & Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False))
  1216. num_videos_watched = watched_videos.count()
  1217. percent_complete = round((num_videos_watched / total_playlist_video_count) * 100,
  1218. 1) if total_playlist_video_count != 0 else 0
  1219. return percent_complete
  1220. def all_videos_unavailable(self):
  1221. all_vids_unavailable = False
  1222. if self.videos.filter(
  1223. Q(is_unavailable_on_yt=True) | Q(was_deleted_on_yt=True)).count() == self.video_count:
  1224. all_vids_unavailable = True
  1225. return all_vids_unavailable
  1226. class PlaylistItem(models.Model):
  1227. playlist = models.ForeignKey(Playlist, related_name="playlist_items",
  1228. on_delete=models.CASCADE) # playlist this pl item belongs to
  1229. video = models.ForeignKey(Video, on_delete=models.CASCADE)
  1230. # details
  1231. playlist_item_id = models.CharField(max_length=100) # the item id of the playlist this video beo
  1232. video_position = models.IntegerField(blank=True) # video position in the playlist
  1233. published_at = models.DateTimeField(
  1234. default=datetime.datetime.now) # snippet.publishedAt - The date and time that the item was added to the playlist
  1235. channel_id = models.CharField(null=True,
  1236. max_length=250) # snippet.channelId - The ID that YouTube uses to uniquely identify the user that added the item to the playlist.
  1237. channel_name = models.CharField(null=True,
  1238. max_length=250) # snippet.channelTitle - The channel title of the channel that the playlist item belongs to.
  1239. # video_owner_channel_id = models.CharField(max_length=100)
  1240. # video_owner_channel_title = models.CharField(max_length=100)
  1241. is_duplicate = models.BooleanField(default=False) # True if the same video exists more than once in the playlist
  1242. is_marked_as_watched = models.BooleanField(default=False, blank=True) # mark video as watched
  1243. num_of_accesses = models.IntegerField(default=0) # tracks num of times this video was clicked on by user
  1244. # for new videos added/modified/deleted in the playlist
  1245. # video_details_modified = models.BooleanField(
  1246. # default=False) # is true for videos whose details changed after playlist update
  1247. # video_details_modified_at = models.DateTimeField(auto_now_add=True) # to set the above false after a day
  1248. created_at = models.DateTimeField(auto_now_add=True)
  1249. updated_at = models.DateTimeField(auto_now=True)
  1250. class Pin(models.Model):
  1251. type = models.CharField(max_length=100) # "playlist", "video"
  1252. playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
  1253. video = models.ForeignKey(Video, on_delete=models.CASCADE)