models.py 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254
  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_ids.append(video_id)
  528. video.save()
  529. while True:
  530. try:
  531. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  532. pl_response = pl_request.execute()
  533. for item in pl_response['items']:
  534. video_id = item['contentDetails']['videoId']
  535. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  536. if (item['snippet']['title'] == "Deleted video" and
  537. item['snippet']['description'] == "This video is unavailable.") or (
  538. item['snippet']['title'] == "Private video" and item['snippet'][
  539. 'description'] == "This video is private."):
  540. video = Video(
  541. playlist_item_id=item["id"],
  542. video_id=video_id,
  543. published_at=item['contentDetails'][
  544. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  545. 'contentDetails'] else None,
  546. name=item['snippet']['title'],
  547. is_unavailable_on_yt=True,
  548. playlist=playlist,
  549. video_position=item['snippet']['position'] + 1
  550. )
  551. video.save()
  552. else:
  553. video = Video(
  554. playlist_item_id=item["id"],
  555. video_id=video_id,
  556. published_at=item['contentDetails'][
  557. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  558. 'contentDetails'] else None,
  559. name=item['snippet']['title'],
  560. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  561. channel_id=item['snippet']['videoOwnerChannelId'],
  562. channel_name=item['snippet']['videoOwnerChannelTitle'],
  563. video_position=item['snippet']['position'] + 1,
  564. playlist=playlist
  565. )
  566. video.save()
  567. video_ids.append(video_id)
  568. else: # video found in db
  569. video = playlist.videos.get(video_id=video_id)
  570. # check if the video became unavailable on youtube
  571. if (item['snippet']['title'] == "Deleted video" and
  572. item['snippet']['description'] == "This video is unavailable.") or (
  573. item['snippet']['title'] == "Private video" and item['snippet'][
  574. 'description'] == "This video is private."):
  575. video.was_deleted_on_yt = True
  576. video.is_duplicate = True
  577. playlist.has_duplicate_videos = True
  578. video_ids.append(video_id)
  579. video.save()
  580. except AttributeError:
  581. break
  582. # API expects the video ids to be a string of comma seperated values, not a python list
  583. video_ids_strings = getVideoIdsStrings(video_ids)
  584. # store duration of all the videos in the playlist
  585. vid_durations = []
  586. for video_ids_string in video_ids_strings:
  587. # query the videos resource using API with the string above
  588. vid_request = youtube.videos().list(
  589. part="contentDetails,player,snippet,statistics", # get details of eac video
  590. id=video_ids_string,
  591. maxResults=50
  592. )
  593. vid_response = vid_request.execute()
  594. for item in vid_response['items']:
  595. duration = item['contentDetails']['duration']
  596. vid = playlist.videos.get(video_id=item['id'])
  597. vid.duration = duration.replace("PT", "")
  598. vid.duration_in_seconds = calculateDuration([duration])
  599. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  600. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  601. 'statistics'] else -1
  602. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  603. 'statistics'] else -1
  604. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  605. 'statistics'] else -1
  606. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  607. vid.save()
  608. vid_durations.append(duration)
  609. playlist_duration_in_seconds = calculateDuration(vid_durations)
  610. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  611. playlist.playlist_duration = getHumanizedTimeString(playlist_duration_in_seconds)
  612. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  613. playlist.has_unavailable_videos = True
  614. playlist.is_in_db = True
  615. playlist.save()
  616. def updatePlaylist(self, user, playlist_id):
  617. current_user = user.profile
  618. credentials = self.getCredentials(user)
  619. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  620. playlist.has_duplicate_videos = False # reset this to false for now
  621. current_video_ids = [video.video_id for video in playlist.videos.all()]
  622. updated_playlist_video_count = 0
  623. deleted_videos, unavailable_videos, added_videos = [], [], []
  624. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  625. video_ids = [] # stores list of all video ids for a given playlist
  626. with build('youtube', 'v3', credentials=credentials) as youtube:
  627. pl_request = youtube.playlistItems().list(
  628. part='contentDetails, snippet, status',
  629. playlistId=playlist_id, # get all playlist videos details for this playlist id
  630. maxResults=50
  631. )
  632. # execute the above request, and store the response
  633. try:
  634. pl_response = pl_request.execute()
  635. except googleapiclient.errors.HttpError:
  636. print("Playist was deleted on YouTube")
  637. return [-1, [], [], []]
  638. print("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
  639. updated_playlist_video_count += len(pl_response["items"])
  640. for item in pl_response['items']:
  641. video_id = item['contentDetails']['videoId']
  642. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE in playlist, add it
  643. if (item['snippet']['title'] == "Deleted video" and
  644. item['snippet']['description'] == "This video is unavailable.") or (
  645. item['snippet']['title'] == "Private video" and item['snippet'][
  646. 'description'] == "This video is private."):
  647. video = Video(
  648. playlist_item_id=item["id"],
  649. video_id=video_id,
  650. name=item['snippet']['title'],
  651. is_unavailable_on_yt=True,
  652. playlist=playlist,
  653. video_position=item['snippet']['position'] + 1
  654. )
  655. else:
  656. video = Video(
  657. playlist_item_id=item["id"],
  658. video_id=video_id,
  659. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  660. item[
  661. 'contentDetails'] else None,
  662. name=item['snippet']['title'],
  663. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  664. channel_id=item['snippet']['channelId'],
  665. channel_name=item['snippet']['channelTitle'],
  666. description=item['snippet']['description'],
  667. video_position=item['snippet']['position'] + 1,
  668. playlist=playlist
  669. )
  670. video.video_details_modified = True
  671. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  672. video.save()
  673. added_videos.append(video)
  674. video_ids.append(video_id)
  675. else: # video found in db
  676. video = playlist.videos.get(video_id=video_id)
  677. if video_id in current_video_ids:
  678. video.video_position = item['snippet']['position'] + 1 # update video position to the one on YT
  679. video_ids.append(video_id)
  680. current_video_ids.remove(video_id)
  681. else:
  682. video_ids.append(video_id)
  683. video.is_duplicate = True
  684. playlist.has_duplicate_videos = True
  685. # check if the video became unavailable on youtube
  686. if not video.is_unavailable_on_yt:
  687. if (item['snippet']['title'] == "Deleted video" and
  688. item['snippet']['description'] == "This video is unavailable.") or (
  689. item['snippet']['title'] == "Private video" and item['snippet'][
  690. 'description'] == "This video is private."):
  691. video.is_unavailable_on_yt = True
  692. video.was_deleted_on_yt = True # video went private on YouTube
  693. video.video_details_modified = True
  694. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  695. unavailable_videos.append(video)
  696. video.save()
  697. while True:
  698. try:
  699. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  700. pl_response = pl_request.execute()
  701. updated_playlist_video_count += len(pl_response["items"])
  702. for item in pl_response['items']:
  703. video_id = item['contentDetails']['videoId']
  704. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  705. if (item['snippet']['title'] == "Deleted video" and
  706. item['snippet']['description'] == "This video is unavailable.") or (
  707. item['snippet']['title'] == "Private video" and item['snippet'][
  708. 'description'] == "This video is private."):
  709. video = Video(
  710. playlist_item_id=item["id"],
  711. video_id=video_id,
  712. published_at=item['contentDetails'][
  713. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  714. 'contentDetails'] else None,
  715. name=item['snippet']['title'],
  716. is_unavailable_on_yt=True,
  717. playlist=playlist,
  718. video_position=item['snippet']['position'] + 1
  719. )
  720. else:
  721. video = Video(
  722. playlist_item_id=item["id"],
  723. video_id=video_id,
  724. published_at=item['contentDetails'][
  725. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  726. 'contentDetails'] else None,
  727. name=item['snippet']['title'],
  728. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  729. channel_id=item['snippet']['channelId'],
  730. channel_name=item['snippet']['channelTitle'],
  731. video_position=item['snippet']['position'] + 1,
  732. playlist=playlist
  733. )
  734. video.video_details_modified = True
  735. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  736. video.save()
  737. added_videos.append(video)
  738. video_ids.append(video_id)
  739. else: # video found in db
  740. video = playlist.videos.get(video_id=video_id)
  741. video.video_position = item['snippet']['position'] + 1 # update video position
  742. if video_id in current_video_ids:
  743. video.is_duplicate = False
  744. current_video_ids.remove(video_id)
  745. video_ids.append(video_id)
  746. else:
  747. video_ids.append(video_id)
  748. video.is_duplicate = True
  749. playlist.has_duplicate_videos = True
  750. # check if the video became unavailable on youtube
  751. if not video.is_unavailable_on_yt:
  752. if (item['snippet']['title'] == "Deleted video" and
  753. item['snippet']['description'] == "This video is unavailable.") or (
  754. item['snippet']['title'] == "Private video" and item['snippet'][
  755. 'description'] == "This video is private."):
  756. video.is_unavailable_on_yt = True
  757. video.was_deleted_on_yt = True
  758. video.video_details_modified = True
  759. video.video_details_modified_at = datetime.datetime.now(tz=pytz.utc)
  760. unavailable_videos.append(video)
  761. video.save()
  762. except AttributeError:
  763. break
  764. # API expects the video ids to be a string of comma seperated values, not a python list
  765. video_ids_strings = getVideoIdsStrings(video_ids)
  766. # store duration of all the videos in the playlist
  767. vid_durations = []
  768. for video_ids_string in video_ids_strings:
  769. # query the videos resource using API with the string above
  770. vid_request = youtube.videos().list(
  771. part="contentDetails,player,snippet,statistics", # get details of eac video
  772. id=video_ids_string,
  773. maxResults=50
  774. )
  775. vid_response = vid_request.execute()
  776. for item in vid_response['items']:
  777. duration = item['contentDetails']['duration']
  778. vid = playlist.videos.get(video_id=item['id'])
  779. vid.duration = duration.replace("PT", "")
  780. vid.duration_in_seconds = calculateDuration([duration])
  781. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  782. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  783. 'statistics'] else -1
  784. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  785. 'statistics'] else -1
  786. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  787. 'statistics'] else -1
  788. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  789. vid.save()
  790. vid_durations.append(duration)
  791. playlist_duration_in_seconds = calculateDuration(vid_durations)
  792. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  793. playlist.playlist_duration = getHumanizedTimeString(playlist_duration_in_seconds)
  794. if len(video_ids) != len(vid_durations) or len(
  795. unavailable_videos) != 0: # that means some videos in the playlist became private/deleted
  796. playlist.has_unavailable_videos = True
  797. playlist.has_playlist_changed = False
  798. playlist.video_count = updated_playlist_video_count
  799. playlist.has_new_updates = True
  800. playlist.save()
  801. deleted_videos = current_video_ids # left out video ids
  802. return [0, deleted_videos, unavailable_videos, added_videos]
  803. def deletePlaylistItems(self, user, playlist_id, playlist_item_ids):
  804. """
  805. Takes in playlist itemids for the videos in a particular playlist
  806. """
  807. credentials = self.getCredentials(user)
  808. playlist = Playlist.objects.get(playlist_id=playlist_id)
  809. # new_playlist_duration_in_seconds = playlist.playlist_duration_in_seconds
  810. # new_playlist_video_count = playlist.video_count
  811. with build('youtube', 'v3', credentials=credentials) as youtube:
  812. for playlist_item_id in playlist_item_ids:
  813. pl_request = youtube.playlistItems().delete(
  814. id=playlist_item_id
  815. )
  816. print(pl_request)
  817. try:
  818. pl_response = pl_request.execute()
  819. print(pl_response)
  820. except googleapiclient.errors.HttpError as e: # failed to delete playlist item
  821. # possible causes:
  822. # playlistItemsNotAccessible (403)
  823. # playlistItemNotFound (404)
  824. # playlistOperationUnsupported (400)
  825. print(e, e.error_details, e.status_code)
  826. continue
  827. # playlistItem was successfully deleted if no HttpError, so delete it from db
  828. # video = playlist.videos.get(playlist_item_id=playlist_item_id)
  829. # new_playlist_video_count -= 1
  830. # new_playlist_duration_in_seconds -= video.duration_in_seconds
  831. # video.delete()
  832. # playlist.video_count = new_playlist_video_count
  833. # playlist.playlist_duration_in_seconds = new_playlist_duration_in_seconds
  834. # playlist.playlist_duration = getHumanizedTimeString(new_playlist_duration_in_seconds)
  835. # playlist.save(update_fields=['video_count', 'playlist_duration', 'playlist_duration_in_seconds'])
  836. # time.sleep(2)
  837. def updatePlaylistDetails(self, user, playlist_id, details):
  838. """
  839. Takes in playlist itemids for the videos in a particular playlist
  840. """
  841. credentials = self.getCredentials(user)
  842. playlist = user.profile.playlists.get(playlist_id=playlist_id)
  843. with build('youtube', 'v3', credentials=credentials) as youtube:
  844. pl_request = youtube.playlists().update(
  845. part="id,snippet,status",
  846. body={
  847. "id": playlist_id,
  848. "snippet": {
  849. "title": details["title"],
  850. "description": details["description"],
  851. },
  852. "status": {
  853. "privacyStatus": "private" if details["privacyStatus"] else "public"
  854. }
  855. },
  856. )
  857. print(details["description"])
  858. try:
  859. pl_response = pl_request.execute()
  860. except googleapiclient.errors.HttpError as e: # failed to update playlist details
  861. # possible causes:
  862. # playlistItemsNotAccessible (403)
  863. # playlistItemNotFound (404)
  864. # playlistOperationUnsupported (400)
  865. # errors i ran into:
  866. # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
  867. print("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
  868. return -1
  869. print(pl_response)
  870. playlist.name = pl_response['snippet']['title']
  871. playlist.description = pl_response['snippet']['description']
  872. playlist.is_private_on_yt = True if pl_response['status']['privacyStatus'] == "private" else False
  873. playlist.save(update_fields=['name', 'description', 'is_private_on_yt'])
  874. return 0
  875. class Tag(models.Model):
  876. name = models.CharField(max_length=69)
  877. created_by = models.ForeignKey(User, related_name="playlist_tags", on_delete=models.CASCADE)
  878. times_viewed = models.IntegerField(default=0)
  879. # type = models.CharField(max_length=10) # either 'playlist' or 'video'
  880. created_at = models.DateTimeField(auto_now_add=True)
  881. updated_at = models.DateTimeField(auto_now=True)
  882. class Playlist(models.Model):
  883. tags = models.ManyToManyField(Tag, related_name="playlists")
  884. # playlist details
  885. playlist_id = models.CharField(max_length=150)
  886. name = models.CharField(max_length=150, blank=True) # YT PLAYLIST NAMES CAN ONLY HAVE MAX OF 150 CHARS
  887. thumbnail_url = models.CharField(max_length=420, blank=True)
  888. description = models.CharField(max_length=420, default="No description")
  889. video_count = models.IntegerField(default=0)
  890. published_at = models.DateTimeField(blank=True)
  891. # 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>"
  892. playlist_yt_player_HTML = models.CharField(max_length=420, blank=True)
  893. user = models.ForeignKey(Profile, on_delete=models.CASCADE,
  894. related_name="playlists") # a user can have many playlists
  895. playlist_duration = models.CharField(max_length=69, blank=True) # string version of playlist dureation
  896. playlist_duration_in_seconds = models.IntegerField(default=0)
  897. has_unavailable_videos = models.BooleanField(default=False) # if videos in playlist are private/deleted
  898. # playlist is made by this channel
  899. channel_id = models.CharField(max_length=420, default="")
  900. channel_name = models.CharField(max_length=420, default="")
  901. user_notes = models.CharField(max_length=420, default="") # user can take notes on the playlist and save them
  902. user_label = models.CharField(max_length=100, default="") # custom user given name for this playlist
  903. # watch playlist details
  904. # watch_time_left = models.CharField(max_length=150, default="")
  905. started_on = models.DateTimeField(auto_now_add=True, null=True)
  906. last_watched = models.DateTimeField(auto_now_add=True, null=True)
  907. # manage playlist
  908. marked_as = models.CharField(default="none",
  909. max_length=100) # can be set to "none", "watching", "on-hold", "plan-to-watch"
  910. is_favorite = models.BooleanField(default=False, blank=True) # to mark playlist as fav
  911. num_of_accesses = models.IntegerField(default="0") # tracks num of times this playlist was opened by user
  912. last_accessed_on = models.DateTimeField(default=datetime.datetime.now)
  913. is_private_on_yt = models.BooleanField(default=False)
  914. is_user_owned = models.BooleanField(default=True) # represents YouTube playlist owned by user
  915. has_duplicate_videos = models.BooleanField(default=False) # duplicate videos will not be shown on site
  916. has_playlist_changed = models.BooleanField(default=False) # determines whether playlist was modified online or not
  917. # set playlist manager
  918. objects = PlaylistManager()
  919. # playlist settings
  920. hide_unavailable_videos = models.BooleanField(default=False)
  921. confirm_before_deleting = models.BooleanField(default=True)
  922. # for import
  923. is_in_db = models.BooleanField(default=False) # is true when all the videos of a playlist have been imported
  924. created_at = models.DateTimeField(auto_now_add=True)
  925. updated_at = models.DateTimeField(auto_now=True)
  926. # for updates
  927. last_full_scan_at = models.DateTimeField(auto_now_add=True)
  928. has_new_updates = models.BooleanField(default=False) # meant to keep track of newly added/unavailable videos
  929. def __str__(self):
  930. return str(self.playlist_id)
  931. def get_unavailable_videos_count(self):
  932. return self.video_count - self.get_watchable_videos_count()
  933. # return count of watchable videos, i.e # videos that are not private or deleted in the playlist
  934. def get_watchable_videos_count(self):
  935. return self.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=False)).count()
  936. def get_watched_videos_count(self):
  937. return self.videos.filter(
  938. Q(is_marked_as_watched=True) & Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=False)).count()
  939. # diff of time from when playlist was first marked as watched and playlist reached 100% completion
  940. def get_finish_time(self):
  941. return self.last_watched - self.started_on
  942. def get_watch_time_left(self):
  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. watched_seconds = 0
  946. for video in watched_videos:
  947. watched_seconds += video.duration_in_seconds
  948. watch_time_left = getHumanizedTimeString(self.playlist_duration_in_seconds - watched_seconds)
  949. return watch_time_left
  950. # return 0 if playlist empty or all videos in playlist are unavailable
  951. def get_percent_complete(self):
  952. total_playlist_video_count = self.get_watchable_videos_count()
  953. watched_videos = self.videos.filter(
  954. Q(is_marked_as_watched=True) & Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=False))
  955. num_videos_watched = watched_videos.count()
  956. percent_complete = round((num_videos_watched / total_playlist_video_count) * 100,
  957. 1) if total_playlist_video_count != 0 else 0
  958. return percent_complete
  959. def all_videos_unavailable(self):
  960. all_vids_unavailable = False
  961. if self.videos.filter(
  962. Q(is_unavailable_on_yt=True) | Q(was_deleted_on_yt=True)).count() == self.video_count:
  963. all_vids_unavailable = True
  964. return all_vids_unavailable
  965. class Video(models.Model):
  966. playlist_item_id = models.CharField(max_length=100) # the item id of the playlist this video beo
  967. # video details
  968. video_id = models.CharField(max_length=100)
  969. name = models.CharField(max_length=100, blank=True)
  970. duration = models.CharField(max_length=100, blank=True)
  971. duration_in_seconds = models.IntegerField(default=0)
  972. thumbnail_url = models.CharField(max_length=420, blank=True)
  973. published_at = models.DateTimeField(blank=True, null=True)
  974. description = models.CharField(max_length=420, default="")
  975. has_cc = models.BooleanField(default=False, blank=True, null=True)
  976. user_notes = models.CharField(max_length=420, default="") # user can take notes on the video and save them
  977. # video stats
  978. view_count = models.IntegerField(default=0)
  979. like_count = models.IntegerField(default=0)
  980. dislike_count = models.IntegerField(default=0)
  981. yt_player_HTML = models.CharField(max_length=420, blank=True)
  982. # video is made by this channel
  983. channel_id = models.CharField(max_length=420, blank=True)
  984. channel_name = models.CharField(max_length=420, blank=True)
  985. # which playlist this video belongs to, and position of that video in the playlist (i.e ALL videos belong to some pl)
  986. playlist = models.ForeignKey(Playlist, related_name="videos", on_delete=models.CASCADE)
  987. video_position = models.IntegerField(blank=True)
  988. # manage video
  989. is_duplicate = models.BooleanField(default=False) # True if the same video exists more than once in the playlist
  990. # NOTE: For a video in db:
  991. # 1.) if both is_unavailable_on_yt and was_deleted_on_yt are true,
  992. # that means the video was originally fine, but then went unavailable when updatePlaylist happened
  993. # 2.) if only is_unavailable_on_yt is true and was_deleted_on_yt is false,
  994. # then that means the video was an unavaiable video when initPlaylist was happening
  995. # 3.) if both is_unavailable_on_yt and was_deleted_on_yt are false, the video is fine, ie up on Youtube
  996. is_unavailable_on_yt = models.BooleanField(
  997. default=False) # True if the video was unavailable (private/deleted) when the API call was first made
  998. was_deleted_on_yt = models.BooleanField(default=False) # True if video became unavailable on a subsequent API call
  999. is_marked_as_watched = models.BooleanField(default=False) # mark video as watched
  1000. is_favorite = models.BooleanField(default=False, blank=True) # mark video as favorite
  1001. num_of_accesses = models.CharField(max_length=69,
  1002. default="0") # tracks num of times this video was clicked on by user
  1003. user_label = models.CharField(max_length=100, default="") # custom user given name for this video
  1004. created_at = models.DateTimeField(auto_now_add=True)
  1005. updated_at = models.DateTimeField(auto_now=True)
  1006. # for new videos added/modified/deleted in the playlist
  1007. video_details_modified = models.BooleanField(
  1008. default=False) # is true for videos whose details changed after playlist update
  1009. video_details_modified_at = models.DateTimeField(auto_now_add=True) # to set the above false after a day
  1010. class PlaylistItem(models.Model):
  1011. playlist = models.ForeignKey(Playlist, related_name="playlist_items",
  1012. on_delete=models.CASCADE) # playlist this pl item belongs to
  1013. video = models.ForeignKey(Video, related_name="playlists", on_delete=models.CASCADE)
  1014. # details
  1015. playlist_item_id = models.CharField(max_length=100) # the item id of the playlist this video beo
  1016. video_position = models.IntegerField(blank=True) # video position in the playlist
  1017. user_notes = models.CharField(max_length=420, default="") # i.e user can take notes on the video and save them
  1018. is_duplicate = models.BooleanField(default=False) # True if the same video exists more than once in the playlist
  1019. is_marked_as_watched = models.BooleanField(default=False, blank=True) # mark video as watched
  1020. is_favorite = models.BooleanField(default=False, blank=True) # mark video as favorite
  1021. num_of_accesses = models.CharField(max_length=69,
  1022. default="0") # tracks num of times this video was clicked on by user
  1023. user_label = models.CharField(max_length=100, default="") # custom user given name for this video
  1024. # for new videos added/modified/deleted in the playlist
  1025. video_details_modified = models.BooleanField(
  1026. default=False) # is true for videos whose details changed after playlist update
  1027. video_details_modified_at = models.DateTimeField(auto_now_add=True) # to set the above false after a day
  1028. created_at = models.DateTimeField(auto_now_add=True)
  1029. updated_at = models.DateTimeField(auto_now=True)
  1030. class Pin(models.Model):
  1031. type = models.CharField(max_length=100) # "playlist", "video"
  1032. playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)
  1033. video = models.ForeignKey(Video, on_delete=models.CASCADE)