models.py 62 KB

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