models.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  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 + 50]))
  21. i += 50
  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. print("Playlist", pl_response)
  225. if pl_response["pageInfo"]["totalResults"] == 0:
  226. print("No playlists created yet on youtube.")
  227. return -2
  228. playlist_items = []
  229. for item in pl_response["items"]:
  230. playlist_items.append(item)
  231. while True:
  232. try:
  233. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  234. pl_response = pl_request.execute()
  235. for item in pl_response["items"]:
  236. playlist_items.append(item)
  237. except AttributeError:
  238. break
  239. for item in playlist_items:
  240. playlist_id = item["id"]
  241. # check if this playlist already exists in database
  242. if current_user.playlists.filter(playlist_id=playlist_id).count() != 0:
  243. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  244. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  245. # POSSIBLE CASES:
  246. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  247. # check if playlist count changed on youtube
  248. if playlist.video_count != item['contentDetails']['itemCount']:
  249. playlist.has_playlist_changed = True
  250. playlist.save()
  251. return -3
  252. else: # no such playlist in database
  253. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  254. playlist = Playlist( # create the playlist and link it to current user
  255. playlist_id=playlist_id,
  256. name=item['snippet']['title'],
  257. description=item['snippet']['description'],
  258. published_at=item['snippet']['publishedAt'],
  259. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  260. video_count=item['contentDetails']['itemCount'],
  261. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  262. playlist_yt_player_HTML=item['player']['embedHtml'],
  263. user=current_user
  264. )
  265. playlist.save()
  266. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  267. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  268. video_ids = [] # stores list of all video ids for a given playlist
  269. with build('youtube', 'v3', credentials=credentials) as youtube:
  270. pl_request = youtube.playlistItems().list(
  271. part='contentDetails, snippet, status',
  272. playlistId=playlist_id, # get all playlist videos details for this playlist id
  273. maxResults=50
  274. )
  275. # execute the above request, and store the response
  276. pl_response = pl_request.execute()
  277. print("Playlist Items", pl_response)
  278. for item in pl_response['items']:
  279. video_id = item['contentDetails']['videoId']
  280. if playlist.channel_id == "":
  281. playlist.channel_id = item['snippet']['channelId']
  282. playlist.channel_name = item['snippet']['channelTitle']
  283. if user.profile.yt_channel_id.strip() != item['snippet']['channelId']:
  284. playlist.is_user_owned = False
  285. playlist.save()
  286. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  287. if (item['snippet']['title'] == "Deleted video" and
  288. item['snippet']['description'] == "This video is unavailable.") or (
  289. item['snippet']['title'] == "Private video" and item['snippet'][
  290. 'description'] == "This video is private."):
  291. video = Video(
  292. playlist_item_id=item["id"],
  293. video_id=video_id,
  294. name=item['snippet']['title'],
  295. is_unavailable_on_yt=True,
  296. playlist=playlist,
  297. video_position=item['snippet']['position'] + 1
  298. )
  299. video.save()
  300. else:
  301. video = Video(
  302. playlist_item_id=item["id"],
  303. video_id=video_id,
  304. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  305. item[
  306. 'contentDetails'] else None,
  307. name=item['snippet']['title'],
  308. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  309. channel_id=item['snippet']['videoOwnerChannelId'],
  310. channel_name=item['snippet']['videoOwnerChannelTitle'],
  311. description=item['snippet']['description'],
  312. video_position=item['snippet']['position'] + 1,
  313. playlist=playlist
  314. )
  315. video.save()
  316. video_ids.append(video_id)
  317. else: # video found in db
  318. video = playlist.videos.get(video_id=video_id)
  319. # check if the video became unavailable on youtube
  320. if (item['snippet']['title'] == "Deleted video" and
  321. item['snippet']['description'] == "This video is unavailable.") or (
  322. item['snippet']['title'] == "Private video" and \
  323. item['snippet']['description'] == "This video is private."):
  324. video.was_deleted_on_yt = True
  325. video.is_duplicate = True
  326. playlist.has_duplicate_videos = True
  327. video.save()
  328. while True:
  329. try:
  330. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  331. pl_response = pl_request.execute()
  332. for item in pl_response['items']:
  333. video_id = item['contentDetails']['videoId']
  334. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  335. if (item['snippet']['title'] == "Deleted video" and
  336. item['snippet']['description'] == "This video is unavailable.") or (
  337. item['snippet']['title'] == "Private video" and \
  338. item['snippet']['description'] == "This video is private."):
  339. video = Video(
  340. playlist_item_id=item["id"],
  341. video_id=video_id,
  342. published_at=item['contentDetails'][
  343. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  344. 'contentDetails'] else None,
  345. name=item['snippet']['title'],
  346. is_unavailable_on_yt=True,
  347. playlist=playlist,
  348. video_position=item['snippet']['position'] + 1
  349. )
  350. video.save()
  351. else:
  352. video = Video(
  353. playlist_item_id=item["id"],
  354. video_id=video_id,
  355. published_at=item['contentDetails'][
  356. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  357. 'contentDetails'] else None,
  358. name=item['snippet']['title'],
  359. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  360. channel_id=item['snippet']['videoOwnerChannelId'],
  361. channel_name=item['snippet']['videoOwnerChannelTitle'],
  362. video_position=item['snippet']['position'] + 1,
  363. playlist=playlist
  364. )
  365. video.save()
  366. video_ids.append(video_id)
  367. else: # video found in db
  368. video = playlist.videos.get(video_id=video_id)
  369. # check if the video became unavailable on youtube
  370. if (item['snippet']['title'] == "Deleted video" and
  371. item['snippet']['description'] == "This video is unavailable.") or (
  372. item['snippet']['title'] == "Private video" and \
  373. item['snippet']['description'] == "This video is private."):
  374. video.was_deleted_on_yt = True
  375. video.is_duplicate = True
  376. playlist.has_duplicate_videos = True
  377. video.save()
  378. except AttributeError:
  379. break
  380. # API expects the video ids to be a string of comma seperated values, not a python list
  381. video_ids_strings = getVideoIdsStrings(video_ids)
  382. print(video_ids)
  383. print(video_ids_strings)
  384. # store duration of all the videos in the playlist
  385. vid_durations = []
  386. for video_ids_string in video_ids_strings:
  387. # query the videos resource using API with the string above
  388. vid_request = youtube.videos().list(
  389. part="contentDetails,player,snippet,statistics", # get details of eac video
  390. id=video_ids_string,
  391. maxResults=50
  392. )
  393. vid_response = vid_request.execute()
  394. print("Videos()", pl_response)
  395. for item in vid_response['items']:
  396. duration = item['contentDetails']['duration']
  397. vid = playlist.videos.get(video_id=item['id'])
  398. vid.duration = duration.replace("PT", "")
  399. vid.duration_in_seconds = calculateDuration([duration])
  400. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  401. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  402. 'statistics'] else -1
  403. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  404. 'statistics'] else -1
  405. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  406. 'statistics'] else -1
  407. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  408. vid.save()
  409. vid_durations.append(duration)
  410. playlist_duration_in_seconds = calculateDuration(vid_durations)
  411. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  412. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  413. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  414. playlist.has_unavailable_videos = True
  415. playlist.is_in_db = True
  416. # playlist.is_user_owned = False
  417. playlist.save()
  418. if pl_id is None:
  419. user.profile.just_joined = False
  420. user.profile.import_in_progress = False
  421. user.save()
  422. return 0
  423. def getAllPlaylistsFromYT(self, user):
  424. '''
  425. Retrieves all of user's playlists from YT and stores them in the Playlist model. Note: only stores
  426. the few of the columns of each playlist in every row, and has is_in_db column as false as no videos will be
  427. saved.
  428. :param user:
  429. :return:
  430. '''
  431. result = {"status": 0,
  432. "num_of_playlists": 0,
  433. "first_playlist_name": "N/A",
  434. "playlist_ids": []}
  435. current_user = user.profile
  436. credentials = self.getCredentials(user)
  437. playlist_ids = []
  438. with build('youtube', 'v3', credentials=credentials) as youtube:
  439. pl_request = youtube.playlists().list(
  440. part='contentDetails, snippet, id, player, status',
  441. mine=True, # get playlist details for this playlist id
  442. maxResults=50
  443. )
  444. # execute the above request, and store the response
  445. try:
  446. pl_response = pl_request.execute()
  447. except googleapiclient.errors.HttpError:
  448. print("YouTube channel not found if mine=True")
  449. print("YouTube playlist not found if id=playlist_id")
  450. result["status"] = -1
  451. return result
  452. if pl_response["pageInfo"]["totalResults"] == 0:
  453. print("No playlists created yet on youtube.")
  454. result["status"] = -2
  455. return result
  456. playlist_items = []
  457. for item in pl_response["items"]:
  458. playlist_items.append(item)
  459. while True:
  460. try:
  461. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  462. pl_response = pl_request.execute()
  463. for item in pl_response["items"]:
  464. playlist_items.append(item)
  465. except AttributeError:
  466. break
  467. result["num_of_playlists"] = len(playlist_items)
  468. result["first_playlist_name"] = playlist_items[0]["snippet"]["title"]
  469. for item in playlist_items:
  470. playlist_id = item["id"]
  471. playlist_ids.append(playlist_id)
  472. # check if this playlist already exists in database
  473. if current_user.playlists.filter(playlist_id=playlist_id).count() != 0:
  474. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  475. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  476. # POSSIBLE CASES:
  477. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  478. # check if playlist count changed on youtube
  479. #if playlist.video_count != item['contentDetails']['itemCount']:
  480. # playlist.has_playlist_changed = True
  481. # playlist.save()
  482. else: # no such playlist in database
  483. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  484. playlist = Playlist( # create the playlist and link it to current user
  485. playlist_id=playlist_id,
  486. name=item['snippet']['title'],
  487. description=item['snippet']['description'],
  488. published_at=item['snippet']['publishedAt'],
  489. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  490. channel_id=item['snippet']['channelId'] if 'channelId' in
  491. item['snippet'] else '',
  492. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  493. item[
  494. 'snippet'] else '',
  495. video_count=item['contentDetails']['itemCount'],
  496. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  497. playlist_yt_player_HTML=item['player']['embedHtml'],
  498. user=current_user
  499. )
  500. playlist.save()
  501. result["playlist_ids"] = playlist_ids
  502. return result
  503. def getAllVideosForPlaylist(self, user, playlist_id):
  504. current_user = user.profile
  505. credentials = self.getCredentials(user)
  506. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  507. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  508. video_ids = [] # stores list of all video ids for a given playlist
  509. with build('youtube', 'v3', credentials=credentials) as youtube:
  510. pl_request = youtube.playlistItems().list(
  511. part='contentDetails, snippet, status',
  512. playlistId=playlist_id, # get all playlist videos details for this playlist id
  513. maxResults=50
  514. )
  515. # execute the above request, and store the response
  516. pl_response = pl_request.execute()
  517. for item in pl_response['items']:
  518. video_id = item['contentDetails']['videoId']
  519. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  520. if (item['snippet']['title'] == "Deleted video" and
  521. item['snippet']['description'] == "This video is unavailable.") or (
  522. item['snippet']['title'] == "Private video" and item['snippet'][
  523. 'description'] == "This video is private."):
  524. video = Video(
  525. playlist_item_id=item["id"],
  526. video_id=video_id,
  527. name=item['snippet']['title'],
  528. is_unavailable_on_yt=True,
  529. playlist=playlist,
  530. video_position=item['snippet']['position'] + 1
  531. )
  532. video.save()
  533. else:
  534. video = Video(
  535. playlist_item_id=item["id"],
  536. video_id=video_id,
  537. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  538. item[
  539. 'contentDetails'] else None,
  540. name=item['snippet']['title'],
  541. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  542. channel_id=item['snippet']['videoOwnerChannelId'],
  543. channel_name=item['snippet']['videoOwnerChannelTitle'],
  544. description=item['snippet']['description'],
  545. video_position=item['snippet']['position'] + 1,
  546. playlist=playlist
  547. )
  548. video.save()
  549. video_ids.append(video_id)
  550. else: # video found in db
  551. video = playlist.videos.get(video_id=video_id)
  552. # check if the video became unavailable on youtube
  553. if (item['snippet']['title'] == "Deleted video" and
  554. item['snippet']['description'] == "This video is unavailable.") or (
  555. item['snippet']['title'] == "Private video" and item['snippet'][
  556. 'description'] == "This video is private."):
  557. video.was_deleted_on_yt = True
  558. video.is_duplicate = True
  559. playlist.has_duplicate_videos = True
  560. video.save()
  561. while True:
  562. try:
  563. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  564. pl_response = pl_request.execute()
  565. for item in pl_response['items']:
  566. video_id = item['contentDetails']['videoId']
  567. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  568. if (item['snippet']['title'] == "Deleted video" and
  569. item['snippet']['description'] == "This video is unavailable.") or (
  570. item['snippet']['title'] == "Private video" and item['snippet'][
  571. 'description'] == "This video is private."):
  572. video = Video(
  573. playlist_item_id=item["id"],
  574. video_id=video_id,
  575. published_at=item['contentDetails'][
  576. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  577. 'contentDetails'] else None,
  578. name=item['snippet']['title'],
  579. is_unavailable_on_yt=True,
  580. playlist=playlist,
  581. video_position=item['snippet']['position'] + 1
  582. )
  583. video.save()
  584. else:
  585. video = Video(
  586. playlist_item_id=item["id"],
  587. video_id=video_id,
  588. published_at=item['contentDetails'][
  589. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  590. 'contentDetails'] else None,
  591. name=item['snippet']['title'],
  592. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  593. channel_id=item['snippet']['videoOwnerChannelId'],
  594. channel_name=item['snippet']['videoOwnerChannelTitle'],
  595. video_position=item['snippet']['position'] + 1,
  596. playlist=playlist
  597. )
  598. video.save()
  599. video_ids.append(video_id)
  600. else: # video found in db
  601. video = playlist.videos.get(video_id=video_id)
  602. # check if the video became unavailable on youtube
  603. if (item['snippet']['title'] == "Deleted video" and
  604. item['snippet']['description'] == "This video is unavailable.") or (
  605. item['snippet']['title'] == "Private video" and item['snippet'][
  606. 'description'] == "This video is private."):
  607. video.was_deleted_on_yt = True
  608. video.is_duplicate = True
  609. playlist.has_duplicate_videos = True
  610. video.save()
  611. except AttributeError:
  612. break
  613. # API expects the video ids to be a string of comma seperated values, not a python list
  614. video_ids_strings = getVideoIdsStrings(video_ids)
  615. # store duration of all the videos in the playlist
  616. vid_durations = []
  617. for video_ids_string in video_ids_strings:
  618. # query the videos resource using API with the string above
  619. vid_request = youtube.videos().list(
  620. part="contentDetails,player,snippet,statistics", # get details of eac video
  621. id=video_ids_string,
  622. maxResults=50
  623. )
  624. vid_response = vid_request.execute()
  625. for item in vid_response['items']:
  626. duration = item['contentDetails']['duration']
  627. vid = playlist.videos.get(video_id=item['id'])
  628. vid.duration = duration.replace("PT", "")
  629. vid.duration_in_seconds = calculateDuration([duration])
  630. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  631. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  632. 'statistics'] else -1
  633. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  634. 'statistics'] else -1
  635. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  636. 'statistics'] else -1
  637. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  638. vid.save()
  639. vid_durations.append(duration)
  640. playlist_duration_in_seconds = calculateDuration(vid_durations)
  641. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  642. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  643. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  644. playlist.has_unavailable_videos = True
  645. playlist.is_in_db = True
  646. playlist.save()
  647. def updatePlaylist(self, user, playlist_id):
  648. current_user = user.profile
  649. credentials = self.getCredentials(user)
  650. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  651. playlist.has_duplicate_videos = False # reset this to false for now
  652. current_video_ids = [video.video_id for video in playlist.videos.all()]
  653. updated_playlist_video_count = 0
  654. deleted_videos, unavailable_videos, added_videos = [], [], []
  655. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  656. video_ids = [] # stores list of all video ids for a given playlist
  657. with build('youtube', 'v3', credentials=credentials) as youtube:
  658. pl_request = youtube.playlistItems().list(
  659. part='contentDetails, snippet, status',
  660. playlistId=playlist_id, # get all playlist videos details for this playlist id
  661. maxResults=50
  662. )
  663. # execute the above request, and store the response
  664. try:
  665. pl_response = pl_request.execute()
  666. except googleapiclient.errors.HttpError:
  667. print("Playist was deleted on YouTube")
  668. return [-1, [], [], []]
  669. print("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
  670. updated_playlist_video_count += len(pl_response["items"])
  671. for item in pl_response['items']:
  672. video_id = item['contentDetails']['videoId']
  673. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE in playlist, add it
  674. if (item['snippet']['title'] == "Deleted video" and
  675. item['snippet']['description'] == "This video is unavailable.") or (
  676. item['snippet']['title'] == "Private video" and item['snippet'][
  677. 'description'] == "This video is private."):
  678. video = Video(
  679. playlist_item_id=item["id"],
  680. video_id=video_id,
  681. name=item['snippet']['title'],
  682. is_unavailable_on_yt=True,
  683. playlist=playlist,
  684. video_position=item['snippet']['position'] + 1
  685. )
  686. else:
  687. video = Video(
  688. playlist_item_id=item["id"],
  689. video_id=video_id,
  690. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  691. item[
  692. 'contentDetails'] else None,
  693. name=item['snippet']['title'],
  694. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  695. channel_id=item['snippet']['channelId'],
  696. channel_name=item['snippet']['channelTitle'],
  697. description=item['snippet']['description'],
  698. video_position=item['snippet']['position'] + 1,
  699. playlist=playlist
  700. )
  701. video.video_details_modified = True
  702. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  703. video.save()
  704. added_videos.append(video)
  705. video_ids.append(video_id)
  706. else: # video found in db
  707. video = playlist.videos.get(video_id=video_id)
  708. if video_id in current_video_ids:
  709. video.video_position = item['snippet']['position'] + 1 # update video position to the one on YT
  710. video_ids.append(video_id)
  711. current_video_ids.remove(video_id)
  712. else:
  713. video.is_duplicate = True
  714. playlist.has_duplicate_videos = True
  715. # check if the video became unavailable on youtube
  716. if not video.is_unavailable_on_yt:
  717. if (item['snippet']['title'] == "Deleted video" and
  718. item['snippet']['description'] == "This video is unavailable.") or (
  719. item['snippet']['title'] == "Private video" and item['snippet'][
  720. 'description'] == "This video is private."):
  721. video.is_unavailable_on_yt = True
  722. video.was_deleted_on_yt = True # video went private on YouTube
  723. video.video_details_modified = True
  724. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  725. unavailable_videos.append(video)
  726. video.save()
  727. while True:
  728. try:
  729. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  730. pl_response = pl_request.execute()
  731. updated_playlist_video_count += len(pl_response["items"])
  732. for item in pl_response['items']:
  733. video_id = item['contentDetails']['videoId']
  734. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  735. if (item['snippet']['title'] == "Deleted video" and
  736. item['snippet']['description'] == "This video is unavailable.") or (
  737. item['snippet']['title'] == "Private video" and item['snippet'][
  738. 'description'] == "This video is private."):
  739. video = Video(
  740. playlist_item_id=item["id"],
  741. video_id=video_id,
  742. published_at=item['contentDetails'][
  743. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  744. 'contentDetails'] else None,
  745. name=item['snippet']['title'],
  746. is_unavailable_on_yt=True,
  747. playlist=playlist,
  748. video_position=item['snippet']['position'] + 1
  749. )
  750. else:
  751. video = Video(
  752. playlist_item_id=item["id"],
  753. video_id=video_id,
  754. published_at=item['contentDetails'][
  755. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  756. 'contentDetails'] else None,
  757. name=item['snippet']['title'],
  758. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  759. channel_id=item['snippet']['channelId'],
  760. channel_name=item['snippet']['channelTitle'],
  761. video_position=item['snippet']['position'] + 1,
  762. playlist=playlist
  763. )
  764. video.video_details_modified = True
  765. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  766. video.save()
  767. added_videos.append(video)
  768. video_ids.append(video_id)
  769. else: # video found in db
  770. video = playlist.videos.get(video_id=video_id)
  771. video.video_position = item['snippet']['position'] + 1 # update video position
  772. if video_id in current_video_ids:
  773. video.is_duplicate = False
  774. current_video_ids.remove(video_id)
  775. else:
  776. video.is_duplicate = True
  777. playlist.has_duplicate_videos = True
  778. # check if the video became unavailable on youtube
  779. if not video.is_unavailable_on_yt:
  780. if (item['snippet']['title'] == "Deleted video" and
  781. item['snippet']['description'] == "This video is unavailable.") or (
  782. item['snippet']['title'] == "Private video" and item['snippet'][
  783. 'description'] == "This video is private."):
  784. video.is_unavailable_on_yt = True
  785. video.was_deleted_on_yt = True
  786. video.video_details_modified = True
  787. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  788. unavailable_videos.append(video)
  789. video.save()
  790. except AttributeError:
  791. break
  792. # API expects the video ids to be a string of comma seperated values, not a python list
  793. video_ids_strings = getVideoIdsStrings(video_ids)
  794. # store duration of all the videos in the playlist
  795. vid_durations = []
  796. for video_ids_string in video_ids_strings:
  797. # query the videos resource using API with the string above
  798. vid_request = youtube.videos().list(
  799. part="contentDetails,player,snippet,statistics", # get details of eac video
  800. id=video_ids_string,
  801. maxResults=50
  802. )
  803. vid_response = vid_request.execute()
  804. for item in vid_response['items']:
  805. duration = item['contentDetails']['duration']
  806. vid = playlist.videos.get(video_id=item['id'])
  807. vid.duration = duration.replace("PT", "")
  808. vid.duration_in_seconds = calculateDuration([duration])
  809. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  810. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  811. 'statistics'] else -1
  812. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  813. 'statistics'] else -1
  814. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  815. 'statistics'] else -1
  816. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  817. vid.save()
  818. vid_durations.append(duration)
  819. playlist_duration_in_seconds = calculateDuration(vid_durations)
  820. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  821. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  822. if len(video_ids) != len(vid_durations) or len(
  823. unavailable_videos) != 0: # that means some videos in the playlist became private/deleted
  824. playlist.has_unavailable_videos = True
  825. playlist.has_playlist_changed = False
  826. playlist.video_count = updated_playlist_video_count
  827. playlist.has_new_updates = True
  828. playlist.save()
  829. deleted_videos = current_video_ids # left out video ids
  830. return [0, deleted_videos, unavailable_videos, added_videos]
  831. class Playlist(models.Model):
  832. # playlist details
  833. playlist_id = models.CharField(max_length=150)
  834. name = models.CharField(max_length=150, blank=True) # YT PLAYLIST NAMES CAN ONLY HAVE MAX OF 150 CHARS
  835. thumbnail_url = models.CharField(max_length=420, blank=True)
  836. description = models.CharField(max_length=420, default="No description")
  837. video_count = models.IntegerField(default=0)
  838. published_at = models.DateTimeField(blank=True)
  839. # 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>"
  840. playlist_yt_player_HTML = models.CharField(max_length=420, blank=True)
  841. user = models.ForeignKey(Profile, on_delete=models.CASCADE,
  842. related_name="playlists") # a user can have many playlists
  843. playlist_duration = models.CharField(max_length=69, blank=True) # string version of playlist dureation
  844. playlist_duration_in_seconds = models.IntegerField(default=0)
  845. has_unavailable_videos = models.BooleanField(default=False) # if videos in playlist are private/deleted
  846. # playlist is made by this channel
  847. channel_id = models.CharField(max_length=420, default="")
  848. channel_name = models.CharField(max_length=420, default="")
  849. user_notes = models.CharField(max_length=420, default="") # user can take notes on the playlist and save them
  850. user_label = models.CharField(max_length=100, default="") # custom user given name for this playlist
  851. # manage playlist
  852. marked_as = models.CharField(default="",
  853. max_length=100) # can be set to "none", "watching", "on-hold", "plan-to-watch"
  854. is_favorite = models.BooleanField(default=False, blank=True) # to mark playlist as fav
  855. num_of_accesses = models.IntegerField(default="0") # tracks num of times this playlist was opened by user
  856. is_private_on_yt = models.BooleanField(default=False)
  857. is_user_owned = models.BooleanField(default=True) # represents YouTube playlist owned by user
  858. has_duplicate_videos = models.BooleanField(default=False) # duplicate videos will not be shown on site
  859. has_playlist_changed = models.BooleanField(default=False) # determines whether playlist was modified online or not
  860. playlist_changed_text = models.CharField(max_length=420,
  861. default="") # user friendly text to display what changed and how much changed
  862. # for UI
  863. view_in_grid_mode = models.BooleanField(default=False) # if False, videso will be showed in a list
  864. # set playlist manager
  865. objects = PlaylistManager()
  866. # for import
  867. is_in_db = models.BooleanField(default=False) # is true when all the videos of a playlist have been imported
  868. created_at = models.DateTimeField(auto_now_add=True)
  869. updated_at = models.DateTimeField(auto_now=True)
  870. # for updates
  871. last_full_scan_at = models.DateTimeField(auto_now_add=True)
  872. has_new_updates = models.BooleanField(default=False) # meant to keep track of newly added/unavailable videos
  873. def __str__(self):
  874. return "Playlist Len " + str(self.video_count)
  875. class Video(models.Model):
  876. playlist_item_id = models.CharField(max_length=100) # the item id of the playlist this video beo
  877. # video details
  878. video_id = models.CharField(max_length=100)
  879. name = models.CharField(max_length=100, blank=True)
  880. duration = models.CharField(max_length=100, blank=True)
  881. duration_in_seconds = models.IntegerField(default=0)
  882. thumbnail_url = models.CharField(max_length=420, blank=True)
  883. published_at = models.DateTimeField(blank=True, null=True)
  884. description = models.CharField(max_length=420, default="")
  885. has_cc = models.BooleanField(default=False, blank=True, null=True)
  886. user_notes = models.CharField(max_length=420, default="") # user can take notes on the video and save them
  887. # video stats
  888. view_count = models.IntegerField(default=0)
  889. like_count = models.IntegerField(default=0)
  890. dislike_count = models.IntegerField(default=0)
  891. yt_player_HTML = models.CharField(max_length=420, blank=True)
  892. # video is made by this channel
  893. channel_id = models.CharField(max_length=420, blank=True)
  894. channel_name = models.CharField(max_length=420, blank=True)
  895. # which playlist this video belongs to, and position of that video in the playlist (i.e ALL videos belong to some pl)
  896. playlist = models.ForeignKey(Playlist, related_name="videos", on_delete=models.CASCADE)
  897. video_position = models.IntegerField(blank=True)
  898. # manage video
  899. is_duplicate = models.BooleanField(default=False) # True if the same video exists more than once in the playlist
  900. # NOTE: For a video in db:
  901. # 1.) if both is_unavailable_on_yt and was_deleted_on_yt are true,
  902. # that means the video was originally fine, but then went unavailable when updatePlaylist happened
  903. # 2.) if only is_unavailable_on_yt is true and was_deleted_on_yt is false,
  904. # then that means the video was an unavaiable video when initPlaylist was happening
  905. # 3.) if both is_unavailable_on_yt and was_deleted_on_yt are false, the video is fine, ie up on Youtube
  906. is_unavailable_on_yt = models.BooleanField(
  907. default=False) # True if the video was unavailable (private/deleted) when the API call was first made
  908. was_deleted_on_yt = models.BooleanField(default=False) # True if video became unavailable on a subsequent API call
  909. is_marked_as_watched = models.BooleanField(default=False, blank=True) # mark video as watched
  910. is_favorite = models.BooleanField(default=False, blank=True) # mark video as favorite
  911. num_of_accesses = models.CharField(max_length=69,
  912. default="0") # tracks num of times this video was clicked on by user
  913. user_label = models.CharField(max_length=100, default="") # custom user given name for this video
  914. created_at = models.DateTimeField(auto_now_add=True)
  915. updated_at = models.DateTimeField(auto_now=True)
  916. # for new videos added/modified/deleted in the playlist
  917. video_details_modified = models.BooleanField(
  918. default=False) # is true for videos whose details changed after playlist update
  919. video_details_modified_at = models.DateTimeField(auto_now_add=True) # to set the above false after a day