models.py 54 KB

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