models.py 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. import datetime
  2. import googleapiclient.errors
  3. from django.db import models
  4. from django.db.models import Q
  5. from google.oauth2.credentials import Credentials
  6. from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
  7. from google.auth.transport.requests import Request
  8. from apps.users.models import Profile
  9. import re
  10. from datetime import timedelta
  11. from googleapiclient.discovery import build
  12. from UnTube.secrets import SECRETS
  13. import pytz
  14. # Create your models here.
  15. input = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
  16. def getVideoIdsStrings(video_ids):
  17. output = []
  18. i = 0
  19. while i < len(video_ids):
  20. output.append(",".join(video_ids[i:i + 10]))
  21. i += 10
  22. return output
  23. def calculateDuration(vid_durations):
  24. hours_pattern = re.compile(r'(\d+)H')
  25. minutes_pattern = re.compile(r'(\d+)M')
  26. seconds_pattern = re.compile(r'(\d+)S')
  27. total_seconds = 0
  28. for duration in vid_durations:
  29. hours = hours_pattern.search(duration) # returns matches in the form "24H"
  30. mins = minutes_pattern.search(duration) # "24M"
  31. secs = seconds_pattern.search(duration) # "24S"
  32. hours = int(hours.group(1)) if hours else 0 # returns 24
  33. mins = int(mins.group(1)) if mins else 0
  34. secs = int(secs.group(1)) if secs else 0
  35. video_seconds = timedelta(
  36. hours=hours,
  37. minutes=mins,
  38. seconds=secs
  39. ).total_seconds()
  40. total_seconds += video_seconds
  41. return total_seconds
  42. def getThumbnailURL(thumbnails):
  43. priority = ("maxres", "standard", "high", "medium", "default")
  44. for quality in priority:
  45. if quality in thumbnails:
  46. return thumbnails[quality]["url"]
  47. return ''
  48. class PlaylistManager(models.Manager):
  49. def getCredentials(self, user):
  50. credentials = Credentials(
  51. user.profile.access_token,
  52. refresh_token=user.profile.refresh_token,
  53. # id_token=session.token.get("id_token"),
  54. token_uri="https://oauth2.googleapis.com/token",
  55. client_id=SECRETS["GOOGLE_OAUTH_CLIENT_ID"],
  56. client_secret=SECRETS["GOOGLE_OAUTH_CLIENT_SECRET"],
  57. scopes=SECRETS["GOOGLE_OAUTH_SCOPES"]
  58. )
  59. credentials.expiry = user.profile.expires_at.replace(tzinfo=None)
  60. if not credentials.valid:
  61. # if credentials and credentials.expired and credentials.refresh_token:
  62. credentials.refresh(Request())
  63. user.profile.expires_at = credentials.expiry
  64. user.profile.access_token = credentials.token
  65. user.profile.refresh_token = credentials.refresh_token
  66. user.save()
  67. return credentials
  68. def getPlaylistId(self, video_link):
  69. temp = video_link.split("?")[-1].split("&")
  70. for el in temp:
  71. if "list=" in el:
  72. return el.split("list=")[-1]
  73. # Returns True if the video count for a playlist on UnTube and video count on same playlist on YouTube is different
  74. def checkIfPlaylistChangedOnYT(self, user, pl_id):
  75. """
  76. 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)
  77. is scanned to see if there are any missing/deleted/newly added videos. This will be only be done
  78. weekly by looking at the playlist.last_full_scan_at
  79. If full_scan is False, only the playlist count difference on YT and UT is checked on every visit
  80. to the playlist page. This is done everytime.
  81. """
  82. credentials = self.getCredentials(user)
  83. playlist = user.profile.playlists.get(playlist_id=pl_id)
  84. with build('youtube', 'v3', credentials=credentials) as youtube:
  85. pl_request = youtube.playlists().list(
  86. part='contentDetails, snippet, id, status',
  87. id=pl_id, # get playlist details for this playlist id
  88. maxResults=50
  89. )
  90. # execute the above request, and store the response
  91. try:
  92. pl_response = pl_request.execute()
  93. except googleapiclient.errors.HttpError:
  94. print("YouTube channel not found if mine=True")
  95. print("YouTube playlist not found if id=playlist_id")
  96. return -1
  97. playlist_items = []
  98. for item in pl_response["items"]:
  99. playlist_items.append(item)
  100. while True:
  101. try:
  102. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  103. pl_response = pl_request.execute()
  104. for item in pl_response["items"]:
  105. playlist_items.append(item)
  106. except AttributeError:
  107. break
  108. for item in playlist_items:
  109. playlist_id = item["id"]
  110. # check if this playlist already exists in database
  111. if user.profile.playlists.filter(playlist_id=playlist_id).count() != 0:
  112. playlist = user.profile.playlists.get(playlist_id__exact=playlist_id)
  113. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  114. # POSSIBLE CASES:
  115. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  116. # check if playlist changed on youtube
  117. if playlist.video_count != item['contentDetails']['itemCount']:
  118. playlist.has_playlist_changed = True
  119. playlist.save()
  120. return [-1, item['contentDetails']['itemCount']]
  121. # if its been a week since the last full scan, do a full playlist scan
  122. # basically checks all the playlist video for any updates
  123. if playlist.last_full_scan_at + datetime.timedelta(days=7) < datetime.datetime.now(pytz.utc):
  124. print("DOING A FULL SCAN")
  125. current_video_ids = [video.video_id for video in playlist.videos.all()]
  126. deleted_videos, unavailable_videos, added_videos = 0, 0, 0
  127. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  128. video_ids = [] # stores list of all video ids for a given playlist
  129. with build('youtube', 'v3', credentials=credentials) as youtube:
  130. pl_request = youtube.playlistItems().list(
  131. part='contentDetails, snippet, status',
  132. playlistId=playlist_id, # get all playlist videos details for this playlist id
  133. maxResults=50
  134. )
  135. # execute the above request, and store the response
  136. pl_response = pl_request.execute()
  137. for item in pl_response['items']:
  138. video_id = item['contentDetails']['videoId']
  139. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE in playlist, its a new vid
  140. added_videos += 1
  141. video_ids.append(video_id)
  142. else: # video found in db
  143. if video_id in current_video_ids:
  144. video_ids.append(video_id)
  145. current_video_ids.remove(video_id)
  146. video = playlist.videos.get(video_id=video_id)
  147. # check if the video became unavailable on youtube
  148. if not video.is_unavailable_on_yt:
  149. if (item['snippet']['title'] == "Deleted video" and
  150. item['snippet']['description'] == "This video is unavailable.") or (
  151. item['snippet']['title'] == "Private video" and item['snippet'][
  152. 'description'] == "This video is private."):
  153. unavailable_videos += 1
  154. while True:
  155. try:
  156. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  157. pl_response = pl_request.execute()
  158. for item in pl_response['items']:
  159. video_id = item['contentDetails']['videoId']
  160. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  161. added_videos += 1
  162. video_ids.append(video_id)
  163. else: # video found in db
  164. if video_id in current_video_ids:
  165. video_ids.append(video_id)
  166. current_video_ids.remove(video_id)
  167. video = playlist.videos.get(video_id=video_id)
  168. # check if the video became unavailable on youtube
  169. if not video.is_unavailable_on_yt:
  170. if (item['snippet']['title'] == "Deleted video" and
  171. item['snippet']['description'] == "This video is unavailable.") or (
  172. item['snippet']['title'] == "Private video" and item['snippet'][
  173. 'description'] == "This video is private."):
  174. unavailable_videos += 1
  175. except AttributeError:
  176. break
  177. playlist.last_full_scan_at = datetime.datetime.now(pytz.utc)
  178. playlist.save()
  179. deleted_videos = len(current_video_ids) # left out video ids
  180. return [1, deleted_videos, unavailable_videos, added_videos]
  181. return [0, "no change"]
  182. # Used to check if the user has a vaild YouTube channel
  183. # Will return -1 if user does not have a YouTube channel
  184. def getUserYTChannelID(self, user):
  185. credentials = self.getCredentials(user)
  186. with build('youtube', 'v3', credentials=credentials) as youtube:
  187. pl_request = youtube.channels().list(
  188. part='id',
  189. mine=True # get playlist details for this user's playlists
  190. )
  191. pl_response = pl_request.execute()
  192. if pl_response['pageInfo']['totalResults'] == 0:
  193. print("Looks like do not have a channel on youtube. Create one to import all of your playlists. Retry?")
  194. return -1
  195. else:
  196. user.profile.yt_channel_id = pl_response['items'][0]['id']
  197. user.save()
  198. return 0
  199. # Set pl_id as None to retrive all the playlists from authenticated user. Playlists already imported will be skipped by default.
  200. # Set pl_id = <valid playlist id>, to import that specific playlist into the user's account
  201. def initPlaylist(self, user, pl_id): # takes in playlist id and saves all of the vids in user's db
  202. current_user = user.profile
  203. credentials = self.getCredentials(user)
  204. with build('youtube', 'v3', credentials=credentials) as youtube:
  205. if pl_id is not None:
  206. pl_request = youtube.playlists().list(
  207. part='contentDetails, snippet, id, player, status',
  208. id=pl_id, # get playlist details for this playlist id
  209. maxResults=50
  210. )
  211. else:
  212. pl_request = youtube.playlists().list(
  213. part='contentDetails, snippet, id, player, status',
  214. mine=True, # get playlist details for this playlist id
  215. maxResults=50
  216. )
  217. # execute the above request, and store the response
  218. try:
  219. pl_response = pl_request.execute()
  220. except googleapiclient.errors.HttpError:
  221. print("YouTube channel not found if mine=True")
  222. print("YouTube playlist not found if id=playlist_id")
  223. return -1
  224. if pl_response["pageInfo"]["totalResults"] == 0:
  225. print("No playlists created yet on youtube.")
  226. return -2
  227. playlist_items = []
  228. for item in pl_response["items"]:
  229. playlist_items.append(item)
  230. while True:
  231. try:
  232. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  233. pl_response = pl_request.execute()
  234. for item in pl_response["items"]:
  235. playlist_items.append(item)
  236. except AttributeError:
  237. break
  238. for item in playlist_items:
  239. playlist_id = item["id"]
  240. # check if this playlist already exists in database
  241. if current_user.playlists.filter(playlist_id=playlist_id).count() != 0:
  242. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  243. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  244. # POSSIBLE CASES:
  245. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  246. # check if playlist count changed on youtube
  247. if playlist.video_count != item['contentDetails']['itemCount']:
  248. playlist.has_playlist_changed = True
  249. playlist.save()
  250. return -3
  251. else: # no such playlist in database
  252. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  253. playlist = Playlist( # create the playlist and link it to current user
  254. playlist_id=playlist_id,
  255. name=item['snippet']['title'],
  256. description=item['snippet']['description'],
  257. published_at=item['snippet']['publishedAt'],
  258. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  259. channel_id=item['snippet']['channelId'] if 'channelId' in
  260. item['snippet'] else '',
  261. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  262. item[
  263. 'snippet'] else '',
  264. video_count=item['contentDetails']['itemCount'],
  265. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  266. playlist_yt_player_HTML=item['player']['embedHtml'],
  267. user=current_user
  268. )
  269. playlist.save()
  270. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  271. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  272. video_ids = [] # stores list of all video ids for a given playlist
  273. with build('youtube', 'v3', credentials=credentials) as youtube:
  274. pl_request = youtube.playlistItems().list(
  275. part='contentDetails, snippet, status',
  276. playlistId=playlist_id, # get all playlist videos details for this playlist id
  277. maxResults=50
  278. )
  279. # execute the above request, and store the response
  280. pl_response = pl_request.execute()
  281. for item in pl_response['items']:
  282. video_id = item['contentDetails']['videoId']
  283. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  284. if (item['snippet']['title'] == "Deleted video" and
  285. item['snippet']['description'] == "This video is unavailable.") or (
  286. item['snippet']['title'] == "Private video" and item['snippet'][
  287. 'description'] == "This video is private."):
  288. video = Video(
  289. playlist_item_id=item["id"],
  290. video_id=video_id,
  291. name=item['snippet']['title'],
  292. is_unavailable_on_yt=True,
  293. playlist=playlist,
  294. video_position=item['snippet']['position'] + 1
  295. )
  296. video.save()
  297. else:
  298. video = Video(
  299. playlist_item_id=item["id"],
  300. video_id=video_id,
  301. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  302. item[
  303. 'contentDetails'] else None,
  304. name=item['snippet']['title'],
  305. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  306. channel_id=item['snippet']['channelId'],
  307. channel_name=item['snippet']['channelTitle'],
  308. description=item['snippet']['description'],
  309. video_position=item['snippet']['position'] + 1,
  310. playlist=playlist
  311. )
  312. video.save()
  313. video_ids.append(video_id)
  314. else: # video found in db
  315. video = playlist.videos.get(video_id=video_id)
  316. # check if the video became unavailable on youtube
  317. if (item['snippet']['title'] == "Deleted video" and
  318. item['snippet']['description'] == "This video is unavailable.") or (
  319. item['snippet']['title'] == "Private video" and \
  320. item['snippet']['description'] == "This video is private."):
  321. video.was_deleted_on_yt = True
  322. video.is_duplicate = True
  323. playlist.has_duplicate_videos = True
  324. video.save()
  325. while True:
  326. try:
  327. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  328. pl_response = pl_request.execute()
  329. for item in pl_response['items']:
  330. video_id = item['contentDetails']['videoId']
  331. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  332. if (item['snippet']['title'] == "Deleted video" and
  333. item['snippet']['description'] == "This video is unavailable.") or (
  334. item['snippet']['title'] == "Private video" and \
  335. item['snippet']['description'] == "This video is private."):
  336. video = Video(
  337. playlist_item_id=item["id"],
  338. video_id=video_id,
  339. published_at=item['contentDetails'][
  340. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  341. 'contentDetails'] else None,
  342. name=item['snippet']['title'],
  343. is_unavailable_on_yt=True,
  344. playlist=playlist,
  345. video_position=item['snippet']['position'] + 1
  346. )
  347. video.save()
  348. else:
  349. video = Video(
  350. playlist_item_id=item["id"],
  351. video_id=video_id,
  352. published_at=item['contentDetails'][
  353. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  354. 'contentDetails'] else None,
  355. name=item['snippet']['title'],
  356. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  357. channel_id=item['snippet']['channelId'],
  358. channel_name=item['snippet']['channelTitle'],
  359. video_position=item['snippet']['position'] + 1,
  360. playlist=playlist
  361. )
  362. video.save()
  363. video_ids.append(video_id)
  364. else: # video found in db
  365. video = playlist.videos.get(video_id=video_id)
  366. # check if the video became unavailable on youtube
  367. if (item['snippet']['title'] == "Deleted video" and
  368. item['snippet']['description'] == "This video is unavailable.") or (
  369. item['snippet']['title'] == "Private video" and \
  370. item['snippet']['description'] == "This video is private."):
  371. video.was_deleted_on_yt = True
  372. video.is_duplicate = True
  373. playlist.has_duplicate_videos = True
  374. video.save()
  375. except AttributeError:
  376. break
  377. # API expects the video ids to be a string of comma seperated values, not a python list
  378. video_ids_strings = getVideoIdsStrings(video_ids)
  379. # store duration of all the videos in the playlist
  380. vid_durations = []
  381. for video_ids_string in video_ids_strings:
  382. # query the videos resource using API with the string above
  383. vid_request = youtube.videos().list(
  384. part="contentDetails,player,snippet,statistics", # get details of eac video
  385. id=video_ids_string,
  386. maxResults=50
  387. )
  388. vid_response = vid_request.execute()
  389. for item in vid_response['items']:
  390. duration = item['contentDetails']['duration']
  391. vid = playlist.videos.get(video_id=item['id'])
  392. vid.duration = duration.replace("PT", "")
  393. vid.duration_in_seconds = calculateDuration([duration])
  394. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  395. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  396. 'statistics'] else -1
  397. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  398. 'statistics'] else -1
  399. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  400. 'statistics'] else -1
  401. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  402. vid.save()
  403. vid_durations.append(duration)
  404. playlist_duration_in_seconds = calculateDuration(vid_durations)
  405. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  406. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  407. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  408. playlist.has_unavailable_videos = True
  409. playlist.is_in_db = True
  410. playlist.is_user_owned = False
  411. playlist.save()
  412. if pl_id is None:
  413. user.profile.just_joined = False
  414. user.profile.import_in_progress = False
  415. user.save()
  416. return 0
  417. def getAllPlaylistsFromYT(self, user):
  418. '''
  419. Retrieves all of user's playlists from YT and stores them in the Playlist model. Note: only stores
  420. the few of the columns of each playlist in every row, and has is_in_db column as false as no videos will be
  421. saved.
  422. :param user:
  423. :return:
  424. '''
  425. result = {"status": 0, "num_of_playlists": 0, "first_playlist_name": "N/A"}
  426. current_user = user.profile
  427. credentials = self.getCredentials(user)
  428. with build('youtube', 'v3', credentials=credentials) as youtube:
  429. pl_request = youtube.playlists().list(
  430. part='contentDetails, snippet, id, player, status',
  431. mine=True, # get playlist details for this playlist id
  432. maxResults=50
  433. )
  434. # execute the above request, and store the response
  435. try:
  436. pl_response = pl_request.execute()
  437. except googleapiclient.errors.HttpError:
  438. print("YouTube channel not found if mine=True")
  439. print("YouTube playlist not found if id=playlist_id")
  440. result["status"] = -1
  441. return result
  442. if pl_response["pageInfo"]["totalResults"] == 0:
  443. print("No playlists created yet on youtube.")
  444. result["status"] = -2
  445. return result
  446. playlist_items = []
  447. for item in pl_response["items"]:
  448. playlist_items.append(item)
  449. while True:
  450. try:
  451. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  452. pl_response = pl_request.execute()
  453. for item in pl_response["items"]:
  454. playlist_items.append(item)
  455. except AttributeError:
  456. break
  457. result["num_of_playlists"] = len(playlist_items)
  458. result["first_playlist_name"] = playlist_items[0]["snippet"]["title"]
  459. for item in playlist_items:
  460. playlist_id = item["id"]
  461. # check if this playlist already exists in database
  462. if current_user.playlists.filter(playlist_id=playlist_id).count() != 0:
  463. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  464. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  465. # POSSIBLE CASES:
  466. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  467. # check if playlist count changed on youtube
  468. if playlist.video_count != item['contentDetails']['itemCount']:
  469. playlist.has_playlist_changed = True
  470. playlist.save()
  471. else: # no such playlist in database
  472. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  473. playlist = Playlist( # create the playlist and link it to current user
  474. playlist_id=playlist_id,
  475. name=item['snippet']['title'],
  476. description=item['snippet']['description'],
  477. published_at=item['snippet']['publishedAt'],
  478. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  479. channel_id=item['snippet']['channelId'] if 'channelId' in
  480. item['snippet'] else '',
  481. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  482. item[
  483. 'snippet'] else '',
  484. video_count=item['contentDetails']['itemCount'],
  485. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  486. playlist_yt_player_HTML=item['player']['embedHtml'],
  487. user=current_user
  488. )
  489. playlist.save()
  490. return result
  491. def getAllVideosForPlaylist(self, user, playlist_id):
  492. current_user = user.profile
  493. credentials = self.getCredentials(user)
  494. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  495. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  496. video_ids = [] # stores list of all video ids for a given playlist
  497. with build('youtube', 'v3', credentials=credentials) as youtube:
  498. pl_request = youtube.playlistItems().list(
  499. part='contentDetails, snippet, status',
  500. playlistId=playlist_id, # get all playlist videos details for this playlist id
  501. maxResults=50
  502. )
  503. # execute the above request, and store the response
  504. pl_response = pl_request.execute()
  505. for item in pl_response['items']:
  506. video_id = item['contentDetails']['videoId']
  507. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  508. if (item['snippet']['title'] == "Deleted video" and
  509. item['snippet']['description'] == "This video is unavailable.") or (
  510. item['snippet']['title'] == "Private video" and item['snippet'][
  511. 'description'] == "This video is private."):
  512. video = Video(
  513. playlist_item_id=item["id"],
  514. video_id=video_id,
  515. name=item['snippet']['title'],
  516. is_unavailable_on_yt=True,
  517. playlist=playlist,
  518. video_position=item['snippet']['position'] + 1
  519. )
  520. video.save()
  521. else:
  522. video = Video(
  523. playlist_item_id=item["id"],
  524. video_id=video_id,
  525. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  526. item[
  527. 'contentDetails'] else None,
  528. name=item['snippet']['title'],
  529. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  530. channel_id=item['snippet']['channelId'],
  531. channel_name=item['snippet']['channelTitle'],
  532. description=item['snippet']['description'],
  533. video_position=item['snippet']['position'] + 1,
  534. playlist=playlist
  535. )
  536. video.save()
  537. video_ids.append(video_id)
  538. else: # video found in db
  539. video = playlist.videos.get(video_id=video_id)
  540. # check if the video became unavailable on youtube
  541. if (item['snippet']['title'] == "Deleted video" and
  542. item['snippet']['description'] == "This video is unavailable.") or (
  543. item['snippet']['title'] == "Private video" and item['snippet'][
  544. 'description'] == "This video is private."):
  545. video.was_deleted_on_yt = True
  546. video.is_duplicate = True
  547. playlist.has_duplicate_videos = True
  548. video.save()
  549. while True:
  550. try:
  551. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  552. pl_response = pl_request.execute()
  553. for item in pl_response['items']:
  554. video_id = item['contentDetails']['videoId']
  555. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  556. if (item['snippet']['title'] == "Deleted video" and
  557. item['snippet']['description'] == "This video is unavailable.") or (
  558. item['snippet']['title'] == "Private video" and item['snippet'][
  559. 'description'] == "This video is private."):
  560. video = Video(
  561. playlist_item_id=item["id"],
  562. video_id=video_id,
  563. published_at=item['contentDetails'][
  564. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  565. 'contentDetails'] else None,
  566. name=item['snippet']['title'],
  567. is_unavailable_on_yt=True,
  568. playlist=playlist,
  569. video_position=item['snippet']['position'] + 1
  570. )
  571. video.save()
  572. else:
  573. video = Video(
  574. playlist_item_id=item["id"],
  575. video_id=video_id,
  576. published_at=item['contentDetails'][
  577. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  578. 'contentDetails'] else None,
  579. name=item['snippet']['title'],
  580. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  581. channel_id=item['snippet']['channelId'],
  582. channel_name=item['snippet']['channelTitle'],
  583. video_position=item['snippet']['position'] + 1,
  584. playlist=playlist
  585. )
  586. video.save()
  587. video_ids.append(video_id)
  588. else: # video found in db
  589. video = playlist.videos.get(video_id=video_id)
  590. # check if the video became unavailable on youtube
  591. if (item['snippet']['title'] == "Deleted video" and
  592. item['snippet']['description'] == "This video is unavailable.") or (
  593. item['snippet']['title'] == "Private video" and item['snippet'][
  594. 'description'] == "This video is private."):
  595. video.was_deleted_on_yt = True
  596. video.is_duplicate = True
  597. playlist.has_duplicate_videos = True
  598. video.save()
  599. except AttributeError:
  600. break
  601. # API expects the video ids to be a string of comma seperated values, not a python list
  602. video_ids_strings = getVideoIdsStrings(video_ids)
  603. # store duration of all the videos in the playlist
  604. vid_durations = []
  605. for video_ids_string in video_ids_strings:
  606. # query the videos resource using API with the string above
  607. vid_request = youtube.videos().list(
  608. part="contentDetails,player,snippet,statistics", # get details of eac video
  609. id=video_ids_string,
  610. maxResults=50
  611. )
  612. vid_response = vid_request.execute()
  613. for item in vid_response['items']:
  614. duration = item['contentDetails']['duration']
  615. vid = playlist.videos.get(video_id=item['id'])
  616. vid.duration = duration.replace("PT", "")
  617. vid.duration_in_seconds = calculateDuration([duration])
  618. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  619. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  620. 'statistics'] else -1
  621. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  622. 'statistics'] else -1
  623. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  624. 'statistics'] else -1
  625. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  626. vid.save()
  627. vid_durations.append(duration)
  628. playlist_duration_in_seconds = calculateDuration(vid_durations)
  629. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  630. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  631. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  632. playlist.has_unavailable_videos = True
  633. playlist.is_in_db = True
  634. playlist.save()
  635. def updatePlaylist(self, user, playlist_id):
  636. current_user = user.profile
  637. credentials = self.getCredentials(user)
  638. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  639. playlist.has_duplicate_videos = False # reset this to false for now
  640. current_video_ids = [video.video_id for video in playlist.videos.all()]
  641. updated_playlist_video_count = 0
  642. deleted_videos, unavailable_videos, added_videos = [], [], []
  643. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  644. video_ids = [] # stores list of all video ids for a given playlist
  645. with build('youtube', 'v3', credentials=credentials) as youtube:
  646. pl_request = youtube.playlistItems().list(
  647. part='contentDetails, snippet, status',
  648. playlistId=playlist_id, # get all playlist videos details for this playlist id
  649. maxResults=50
  650. )
  651. # execute the above request, and store the response
  652. pl_response = pl_request.execute()
  653. print("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
  654. updated_playlist_video_count += len(pl_response["items"])
  655. for item in pl_response['items']:
  656. video_id = item['contentDetails']['videoId']
  657. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE in playlist, add it
  658. if (item['snippet']['title'] == "Deleted video" and
  659. item['snippet']['description'] == "This video is unavailable.") or (
  660. item['snippet']['title'] == "Private video" and item['snippet'][
  661. 'description'] == "This video is private."):
  662. video = Video(
  663. playlist_item_id=item["id"],
  664. video_id=video_id,
  665. name=item['snippet']['title'],
  666. is_unavailable_on_yt=True,
  667. playlist=playlist,
  668. video_position=item['snippet']['position'] + 1
  669. )
  670. else:
  671. video = Video(
  672. playlist_item_id=item["id"],
  673. video_id=video_id,
  674. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  675. item[
  676. 'contentDetails'] else None,
  677. name=item['snippet']['title'],
  678. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  679. channel_id=item['snippet']['channelId'],
  680. channel_name=item['snippet']['channelTitle'],
  681. description=item['snippet']['description'],
  682. video_position=item['snippet']['position'] + 1,
  683. playlist=playlist
  684. )
  685. video.video_details_modified = True
  686. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  687. video.save()
  688. added_videos.append(video)
  689. video_ids.append(video_id)
  690. else: # video found in db
  691. video = playlist.videos.get(video_id=video_id)
  692. if video_id in current_video_ids:
  693. video.video_position = item['snippet']['position'] + 1 # update video position to the one on YT
  694. video_ids.append(video_id)
  695. current_video_ids.remove(video_id)
  696. else:
  697. video.is_duplicate = True
  698. playlist.has_duplicate_videos = True
  699. # check if the video became unavailable on youtube
  700. if not video.is_unavailable_on_yt:
  701. if (item['snippet']['title'] == "Deleted video" and
  702. item['snippet']['description'] == "This video is unavailable.") or (
  703. item['snippet']['title'] == "Private video" and item['snippet'][
  704. 'description'] == "This video is private."):
  705. video.was_deleted_on_yt = True # video went private on YouTube
  706. video.video_details_modified = True
  707. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  708. unavailable_videos.append(video)
  709. video.save()
  710. while True:
  711. try:
  712. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  713. pl_response = pl_request.execute()
  714. updated_playlist_video_count += len(pl_response["items"])
  715. for item in pl_response['items']:
  716. video_id = item['contentDetails']['videoId']
  717. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  718. if (item['snippet']['title'] == "Deleted video" and
  719. item['snippet']['description'] == "This video is unavailable.") or (
  720. item['snippet']['title'] == "Private video" and item['snippet'][
  721. 'description'] == "This video is private."):
  722. video = Video(
  723. playlist_item_id=item["id"],
  724. video_id=video_id,
  725. published_at=item['contentDetails'][
  726. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  727. 'contentDetails'] else None,
  728. name=item['snippet']['title'],
  729. is_unavailable_on_yt=True,
  730. playlist=playlist,
  731. video_position=item['snippet']['position'] + 1
  732. )
  733. else:
  734. video = Video(
  735. playlist_item_id=item["id"],
  736. video_id=video_id,
  737. published_at=item['contentDetails'][
  738. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  739. 'contentDetails'] else None,
  740. name=item['snippet']['title'],
  741. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  742. channel_id=item['snippet']['channelId'],
  743. channel_name=item['snippet']['channelTitle'],
  744. video_position=item['snippet']['position'] + 1,
  745. playlist=playlist
  746. )
  747. video.video_details_modified = True
  748. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  749. video.save()
  750. added_videos.append(video)
  751. video_ids.append(video_id)
  752. else: # video found in db
  753. video = playlist.videos.get(video_id=video_id)
  754. video.video_position = item['snippet']['position'] + 1 # update video position
  755. if video_id in current_video_ids:
  756. video.is_duplicate = False
  757. current_video_ids.remove(video_id)
  758. else:
  759. video.is_duplicate = True
  760. playlist.has_duplicate_videos = True
  761. # check if the video became unavailable on youtube
  762. if not video.is_unavailable_on_yt:
  763. if (item['snippet']['title'] == "Deleted video" and
  764. item['snippet']['description'] == "This video is unavailable.") or (
  765. item['snippet']['title'] == "Private video" and item['snippet'][
  766. 'description'] == "This video is private."):
  767. video.was_deleted_on_yt = True
  768. video.video_details_modified = True
  769. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  770. unavailable_videos.append(video)
  771. video.save()
  772. except AttributeError:
  773. break
  774. # API expects the video ids to be a string of comma seperated values, not a python list
  775. video_ids_strings = getVideoIdsStrings(video_ids)
  776. # store duration of all the videos in the playlist
  777. vid_durations = []
  778. for video_ids_string in video_ids_strings:
  779. # query the videos resource using API with the string above
  780. vid_request = youtube.videos().list(
  781. part="contentDetails,player,snippet,statistics", # get details of eac video
  782. id=video_ids_string,
  783. maxResults=50
  784. )
  785. vid_response = vid_request.execute()
  786. for item in vid_response['items']:
  787. duration = item['contentDetails']['duration']
  788. vid = playlist.videos.get(video_id=item['id'])
  789. vid.duration = duration.replace("PT", "")
  790. vid.duration_in_seconds = calculateDuration([duration])
  791. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  792. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  793. 'statistics'] else -1
  794. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  795. 'statistics'] else -1
  796. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  797. 'statistics'] else -1
  798. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  799. vid.save()
  800. vid_durations.append(duration)
  801. playlist_duration_in_seconds = calculateDuration(vid_durations)
  802. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  803. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  804. if len(video_ids) != len(vid_durations) or len(
  805. unavailable_videos) != 0: # that means some videos in the playlist became private/deleted
  806. playlist.has_unavailable_videos = True
  807. playlist.has_playlist_changed = False
  808. playlist.video_count = updated_playlist_video_count
  809. playlist.has_new_updates = True
  810. playlist.save()
  811. deleted_videos = current_video_ids # left out video ids
  812. return [deleted_videos, unavailable_videos, added_videos]
  813. class Playlist(models.Model):
  814. # playlist details
  815. playlist_id = models.CharField(max_length=150)
  816. name = models.CharField(max_length=150, blank=True)
  817. thumbnail_url = models.CharField(max_length=420, blank=True)
  818. description = models.CharField(max_length=420, default="No description")
  819. video_count = models.IntegerField(default=0)
  820. published_at = models.DateTimeField(blank=True)
  821. # 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>"
  822. playlist_yt_player_HTML = models.CharField(max_length=420, blank=True)
  823. user = models.ForeignKey(Profile, on_delete=models.CASCADE,
  824. related_name="playlists") # a user can have many playlists
  825. playlist_duration = models.CharField(max_length=69, blank=True) # string version of playlist dureation
  826. playlist_duration_in_seconds = models.IntegerField(default=0)
  827. has_unavailable_videos = models.BooleanField(default=False) # if videos in playlist are private/deleted
  828. # playlist is made by this channel
  829. channel_id = models.CharField(max_length=420, blank=True)
  830. channel_name = models.CharField(max_length=420, blank=True)
  831. user_notes = models.CharField(max_length=420, default="") # user can take notes on the playlist and save them
  832. user_label = models.CharField(max_length=100, default="") # custom user given name for this playlist
  833. # manage playlist
  834. marked_as = models.CharField(default="",
  835. max_length=100) # can be set to "none", "watching", "on-hold", "plan-to-watch"
  836. is_favorite = models.BooleanField(default=False, blank=True) # to mark playlist as fav
  837. num_of_accesses = models.IntegerField(default="0") # tracks num of times this playlist was opened by user
  838. is_private_on_yt = models.BooleanField(default=False)
  839. is_user_owned = models.BooleanField(default=True) # represents YouTube playlist owned by user
  840. has_duplicate_videos = models.BooleanField(default=False) # duplicate videos will not be shown on site
  841. has_playlist_changed = models.BooleanField(default=False) # determines whether playlist was modified online or not
  842. playlist_changed_text = models.CharField(max_length=420,
  843. default="") # user friendly text to display what changed and how much changed
  844. # for UI
  845. view_in_grid_mode = models.BooleanField(default=False) # if False, videso will be showed in a list
  846. # set playlist manager
  847. objects = PlaylistManager()
  848. # for import
  849. is_in_db = models.BooleanField(default=False) # is true when all the videos of a playlist have been imported
  850. created_at = models.DateTimeField(auto_now_add=True)
  851. updated_at = models.DateTimeField(auto_now=True)
  852. # for updates
  853. last_full_scan_at = models.DateTimeField(auto_now_add=True)
  854. has_new_updates = models.BooleanField(default=False)
  855. def __str__(self):
  856. return "Playlist Len " + str(self.video_count)
  857. class Video(models.Model):
  858. playlist_item_id = models.CharField(max_length=100) # the item id of the playlist this video beo
  859. # video details
  860. video_id = models.CharField(max_length=100)
  861. name = models.CharField(max_length=100, blank=True)
  862. duration = models.CharField(max_length=100, blank=True)
  863. duration_in_seconds = models.IntegerField(default=0)
  864. thumbnail_url = models.CharField(max_length=420, blank=True)
  865. published_at = models.DateTimeField(blank=True, null=True)
  866. description = models.CharField(max_length=420, default="")
  867. has_cc = models.BooleanField(default=False, blank=True, null=True)
  868. user_notes = models.CharField(max_length=420, default="") # user can take notes on the video and save them
  869. # video stats
  870. view_count = models.IntegerField(default=0)
  871. like_count = models.IntegerField(default=0)
  872. dislike_count = models.IntegerField(default=0)
  873. yt_player_HTML = models.CharField(max_length=420, blank=True)
  874. # video is made by this channel
  875. channel_id = models.CharField(max_length=420, blank=True)
  876. channel_name = models.CharField(max_length=420, blank=True)
  877. # which playlist this video belongs to, and position of that video in the playlist (i.e ALL videos belong to some pl)
  878. playlist = models.ForeignKey(Playlist, related_name="videos", on_delete=models.CASCADE)
  879. video_position = models.IntegerField(blank=True)
  880. # manage video
  881. is_duplicate = models.BooleanField(default=False) # True if the same video exists more than once in the playlist
  882. is_unavailable_on_yt = models.BooleanField(
  883. default=False) # True if the video was unavailable (private/deleted) when the API call was first made
  884. was_deleted_on_yt = models.BooleanField(default=False) # True if video became unavailable on a subsequent API call
  885. is_marked_as_watched = models.BooleanField(default=False, blank=True) # mark video as watched
  886. is_favorite = models.BooleanField(default=False, blank=True) # mark video as favorite
  887. num_of_accesses = models.CharField(max_length=69,
  888. default="0") # tracks num of times this video was clicked on by user
  889. user_label = models.CharField(max_length=100, default="") # custom user given name for this video
  890. created_at = models.DateTimeField(auto_now_add=True)
  891. updated_at = models.DateTimeField(auto_now=True)
  892. # for new videos added/modified/deleted in the playlist
  893. video_details_modified = models.BooleanField(
  894. default=False) # is true for videos whose details changed after playlist update
  895. video_details_modified_at = models.DateTimeField(auto_now_add=True) # to set the above false after a day