2
0

models.py 59 KB

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