models.py 59 KB

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