models.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. import googleapiclient.errors
  2. from django.db import models
  3. from django.db.models import Q
  4. from google.oauth2.credentials import Credentials
  5. from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
  6. from google.auth.transport.requests import Request
  7. from apps.users.models import Profile
  8. import re
  9. from datetime import timedelta
  10. from googleapiclient.discovery import build
  11. from UnTube.secrets import SECRETS
  12. import pytz
  13. # Create your models here.
  14. input = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
  15. def getVideoIdsStrings(video_ids):
  16. output = []
  17. i = 0
  18. while i < len(video_ids):
  19. output.append(",".join(video_ids[i:i + 10]))
  20. i += 10
  21. return output
  22. def calculateDuration(vid_durations):
  23. hours_pattern = re.compile(r'(\d+)H')
  24. minutes_pattern = re.compile(r'(\d+)M')
  25. seconds_pattern = re.compile(r'(\d+)S')
  26. total_seconds = 0
  27. for duration in vid_durations:
  28. hours = hours_pattern.search(duration) # returns matches in the form "24H"
  29. mins = minutes_pattern.search(duration) # "24M"
  30. secs = seconds_pattern.search(duration) # "24S"
  31. hours = int(hours.group(1)) if hours else 0 # returns 24
  32. mins = int(mins.group(1)) if mins else 0
  33. secs = int(secs.group(1)) if secs else 0
  34. video_seconds = timedelta(
  35. hours=hours,
  36. minutes=mins,
  37. seconds=secs
  38. ).total_seconds()
  39. total_seconds += video_seconds
  40. return total_seconds
  41. def getThumbnailURL(thumbnails):
  42. priority = ("maxres", "standard", "high", "medium", "default")
  43. for quality in priority:
  44. if quality in thumbnails:
  45. return thumbnails[quality]["url"]
  46. return ''
  47. class PlaylistManager(models.Manager):
  48. def getCredentials(self, user):
  49. credentials = Credentials(
  50. user.profile.access_token,
  51. refresh_token=user.profile.refresh_token,
  52. # id_token=session.token.get("id_token"),
  53. token_uri="https://oauth2.googleapis.com/token",
  54. client_id=SECRETS["GOOGLE_OAUTH_CLIENT_ID"],
  55. client_secret=SECRETS["GOOGLE_OAUTH_CLIENT_SECRET"],
  56. scopes=SECRETS["GOOGLE_OAUTH_SCOPES"]
  57. )
  58. credentials.expiry = user.profile.expires_at.replace(tzinfo=None)
  59. if not credentials.valid:
  60. # if credentials and credentials.expired and credentials.refresh_token:
  61. credentials.refresh(Request())
  62. user.profile.expires_at = credentials.expiry
  63. user.profile.access_token = credentials.token
  64. user.profile.refresh_token = credentials.refresh_token
  65. user.save()
  66. return credentials
  67. def getPlaylistId(self, video_link):
  68. temp = video_link.split("?")[-1].split("&")
  69. for el in temp:
  70. if "list=" in el:
  71. return el.split("list=")[-1]
  72. # Returns True if the video count for a playlist on UnTube and video count on same playlist on YouTube is different
  73. def checkIfPlaylistChangedOnYT(self, user, pl_id):
  74. credentials = self.getCredentials(user)
  75. with build('youtube', 'v3', credentials=credentials) as youtube:
  76. pl_request = youtube.playlists().list(
  77. part='contentDetails, snippet, id, status',
  78. id=pl_id, # get playlist details for this playlist id
  79. maxResults=50
  80. )
  81. # execute the above request, and store the response
  82. try:
  83. pl_response = pl_request.execute()
  84. except googleapiclient.errors.HttpError:
  85. print("YouTube channel not found if mine=True")
  86. print("YouTube playlist not found if id=playlist_id")
  87. return -1
  88. playlist_items = []
  89. for item in pl_response["items"]:
  90. playlist_items.append(item)
  91. while True:
  92. try:
  93. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  94. pl_response = pl_request.execute()
  95. for item in pl_response["items"]:
  96. playlist_items.append(item)
  97. except AttributeError:
  98. break
  99. for item in playlist_items:
  100. playlist_id = item["id"]
  101. # check if this playlist already exists in database
  102. if user.profile.playlists.filter(playlist_id=playlist_id).count() != 0:
  103. playlist = user.profile.playlists.get(playlist_id__exact=playlist_id)
  104. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  105. # POSSIBLE CASES:
  106. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  107. # check if playlist count changed on youtube
  108. if playlist.video_count != item['contentDetails']['itemCount']:
  109. return [-1, item['contentDetails']['itemCount']]
  110. return [0, "no change"]
  111. # Used to check if the user has a vaild YouTube channel
  112. # Will return -1 if user does not have a YouTube channel
  113. def getUserYTChannelID(self, user):
  114. credentials = self.getCredentials(user)
  115. with build('youtube', 'v3', credentials=credentials) as youtube:
  116. pl_request = youtube.channels().list(
  117. part='id',
  118. mine=True # get playlist details for this user's playlists
  119. )
  120. pl_response = pl_request.execute()
  121. if pl_response['pageInfo']['totalResults'] == 0:
  122. print("Looks like do not have a channel on youtube. Create one to import all of your playlists. Retry?")
  123. return -1
  124. else:
  125. user.profile.yt_channel_id = pl_response['items'][0]['id']
  126. user.save()
  127. return 0
  128. # Set pl_id as None to retrive all the playlists from authenticated user. Playlists already imported will be skipped by default.
  129. # Set pl_id = <valid playlist id>, to import that specific playlist into the user's account
  130. def initPlaylist(self, user, pl_id): # takes in playlist id and saves all of the vids in user's db
  131. current_user = user.profile
  132. credentials = self.getCredentials(user)
  133. with build('youtube', 'v3', credentials=credentials) as youtube:
  134. if pl_id is not None:
  135. pl_request = youtube.playlists().list(
  136. part='contentDetails, snippet, id, player, status',
  137. id=pl_id, # get playlist details for this playlist id
  138. maxResults=50
  139. )
  140. else:
  141. pl_request = youtube.playlists().list(
  142. part='contentDetails, snippet, id, player, status',
  143. mine=True, # get playlist details for this playlist id
  144. maxResults=50
  145. )
  146. # execute the above request, and store the response
  147. try:
  148. pl_response = pl_request.execute()
  149. except googleapiclient.errors.HttpError:
  150. print("YouTube channel not found if mine=True")
  151. print("YouTube playlist not found if id=playlist_id")
  152. return -1
  153. if pl_response["pageInfo"]["totalResults"] == 0:
  154. print("No playlists created yet on youtube.")
  155. return -2
  156. playlist_items = []
  157. for item in pl_response["items"]:
  158. playlist_items.append(item)
  159. while True:
  160. try:
  161. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  162. pl_response = pl_request.execute()
  163. for item in pl_response["items"]:
  164. playlist_items.append(item)
  165. except AttributeError:
  166. break
  167. for item in playlist_items:
  168. playlist_id = item["id"]
  169. # check if this playlist already exists in database
  170. if current_user.playlists.filter(playlist_id=playlist_id).count() != 0:
  171. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  172. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  173. # POSSIBLE CASES:
  174. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  175. # check if playlist count changed on youtube
  176. if playlist.video_count != item['contentDetails']['itemCount']:
  177. playlist.has_playlist_changed = True
  178. playlist.save()
  179. return -3
  180. else: # no such playlist in database
  181. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  182. playlist = Playlist( # create the playlist and link it to current user
  183. playlist_id=playlist_id,
  184. name=item['snippet']['title'],
  185. description=item['snippet']['description'],
  186. published_at=item['snippet']['publishedAt'],
  187. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  188. channel_id=item['snippet']['channelId'] if 'channelId' in
  189. item['snippet'] else '',
  190. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  191. item[
  192. 'snippet'] else '',
  193. video_count=item['contentDetails']['itemCount'],
  194. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  195. playlist_yt_player_HTML=item['player']['embedHtml'],
  196. user=current_user
  197. )
  198. playlist.save()
  199. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  200. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  201. video_ids = [] # stores list of all video ids for a given playlist
  202. with build('youtube', 'v3', credentials=credentials) as youtube:
  203. pl_request = youtube.playlistItems().list(
  204. part='contentDetails, snippet, status',
  205. playlistId=playlist_id, # get all playlist videos details for this playlist id
  206. maxResults=50
  207. )
  208. # execute the above request, and store the response
  209. pl_response = pl_request.execute()
  210. for item in pl_response['items']:
  211. video_id = item['contentDetails']['videoId']
  212. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  213. if (item['snippet']['title'] == "Deleted video" and
  214. item['snippet']['description'] == "This video is unavailable.") or (
  215. item['snippet']['title'] == "Private video" and item['snippet'][
  216. 'description'] == "This video is private."):
  217. video = Video(
  218. video_id=video_id,
  219. name=item['snippet']['title'],
  220. is_unavailable_on_yt=True,
  221. playlist=playlist,
  222. video_position=item['snippet']['position'] + 1
  223. )
  224. video.save()
  225. else:
  226. video = Video(
  227. video_id=video_id,
  228. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  229. item[
  230. 'contentDetails'] else None,
  231. name=item['snippet']['title'],
  232. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  233. channel_id=item['snippet']['channelId'],
  234. channel_name=item['snippet']['channelTitle'],
  235. description=item['snippet']['description'],
  236. video_position=item['snippet']['position'] + 1,
  237. playlist=playlist
  238. )
  239. video.save()
  240. video_ids.append(video_id)
  241. else: # video found in db
  242. video = playlist.videos.get(video_id=video_id)
  243. # check if the video became unavailable on youtube
  244. if (item['snippet']['title'] == "Deleted video" and
  245. item['snippet']['description'] == "This video is unavailable.") or (
  246. item['snippet']['title'] == "Private video" and \
  247. item['snippet']['description'] == "This video is private."):
  248. video.was_deleted_on_yt = True
  249. video.is_duplicate = True
  250. playlist.has_duplicate_videos = True
  251. video.save()
  252. while True:
  253. try:
  254. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  255. pl_response = pl_request.execute()
  256. for item in pl_response['items']:
  257. video_id = item['contentDetails']['videoId']
  258. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  259. if (item['snippet']['title'] == "Deleted video" and
  260. item['snippet']['description'] == "This video is unavailable.") or (
  261. item['snippet']['title'] == "Private video" and \
  262. item['snippet']['description'] == "This video is private."):
  263. video = Video(
  264. video_id=video_id,
  265. published_at=item['contentDetails'][
  266. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  267. 'contentDetails'] else None,
  268. name=item['snippet']['title'],
  269. is_unavailable_on_yt=True,
  270. playlist=playlist,
  271. video_position=item['snippet']['position'] + 1
  272. )
  273. video.save()
  274. else:
  275. video = Video(
  276. video_id=video_id,
  277. published_at=item['contentDetails'][
  278. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  279. 'contentDetails'] else None,
  280. name=item['snippet']['title'],
  281. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  282. channel_id=item['snippet']['channelId'],
  283. channel_name=item['snippet']['channelTitle'],
  284. video_position=item['snippet']['position'] + 1,
  285. playlist=playlist
  286. )
  287. video.save()
  288. video_ids.append(video_id)
  289. else: # video found in db
  290. video = playlist.videos.get(video_id=video_id)
  291. # check if the video became unavailable on youtube
  292. if (item['snippet']['title'] == "Deleted video" and
  293. item['snippet']['description'] == "This video is unavailable.") or (
  294. item['snippet']['title'] == "Private video" and \
  295. item['snippet']['description'] == "This video is private."):
  296. video.was_deleted_on_yt = True
  297. video.is_duplicate = True
  298. playlist.has_duplicate_videos = True
  299. video.save()
  300. except AttributeError:
  301. break
  302. # API expects the video ids to be a string of comma seperated values, not a python list
  303. video_ids_strings = getVideoIdsStrings(video_ids)
  304. # store duration of all the videos in the playlist
  305. vid_durations = []
  306. for video_ids_string in video_ids_strings:
  307. # query the videos resource using API with the string above
  308. vid_request = youtube.videos().list(
  309. part="contentDetails,player,snippet,statistics", # get details of eac video
  310. id=video_ids_string,
  311. maxResults=50
  312. )
  313. vid_response = vid_request.execute()
  314. for item in vid_response['items']:
  315. duration = item['contentDetails']['duration']
  316. vid = playlist.videos.get(video_id=item['id'])
  317. vid.duration = duration.replace("PT", "")
  318. vid.duration_in_seconds = calculateDuration([duration])
  319. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  320. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  321. 'statistics'] else -1
  322. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  323. 'statistics'] else -1
  324. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  325. 'statistics'] else -1
  326. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  327. vid.save()
  328. vid_durations.append(duration)
  329. playlist_duration_in_seconds = calculateDuration(vid_durations)
  330. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  331. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  332. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  333. playlist.has_unavailable_videos = True
  334. playlist.is_in_db = True
  335. playlist.is_user_owned = False
  336. playlist.save()
  337. if pl_id is None:
  338. user.profile.just_joined = False
  339. user.profile.import_in_progress = False
  340. user.save()
  341. return 0
  342. def getAllPlaylistsFromYT(self, user):
  343. '''
  344. Retrieves all of user's playlists from YT and stores them in the Playlist model. Note: only stores
  345. the few of the columns of each playlist in every row, and has is_in_db column as false as no videos will be
  346. saved.
  347. :param user:
  348. :return:
  349. '''
  350. result = {"status": 0, "num_of_playlists": 0, "first_playlist_name": "N/A"}
  351. current_user = user.profile
  352. credentials = self.getCredentials(user)
  353. with build('youtube', 'v3', credentials=credentials) as youtube:
  354. pl_request = youtube.playlists().list(
  355. part='contentDetails, snippet, id, player, status',
  356. mine=True, # get playlist details for this playlist id
  357. maxResults=50
  358. )
  359. # execute the above request, and store the response
  360. try:
  361. pl_response = pl_request.execute()
  362. except googleapiclient.errors.HttpError:
  363. print("YouTube channel not found if mine=True")
  364. print("YouTube playlist not found if id=playlist_id")
  365. result["status"] = -1
  366. return result
  367. if pl_response["pageInfo"]["totalResults"] == 0:
  368. print("No playlists created yet on youtube.")
  369. result["status"] = -2
  370. return result
  371. playlist_items = []
  372. for item in pl_response["items"]:
  373. playlist_items.append(item)
  374. while True:
  375. try:
  376. pl_request = youtube.playlists().list_next(pl_request, pl_response)
  377. pl_response = pl_request.execute()
  378. for item in pl_response["items"]:
  379. playlist_items.append(item)
  380. except AttributeError:
  381. break
  382. result["num_of_playlists"] = len(playlist_items)
  383. result["first_playlist_name"] = playlist_items[0]["snippet"]["title"]
  384. for item in playlist_items:
  385. playlist_id = item["id"]
  386. # check if this playlist already exists in database
  387. if current_user.playlists.filter(playlist_id=playlist_id).count() != 0:
  388. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  389. print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
  390. # POSSIBLE CASES:
  391. # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
  392. # check if playlist count changed on youtube
  393. if playlist.video_count != item['contentDetails']['itemCount']:
  394. playlist.has_playlist_changed = True
  395. playlist.save()
  396. else: # no such playlist in database
  397. ### MAKE THE PLAYLIST AND LINK IT TO CURRENT_USER
  398. playlist = Playlist( # create the playlist and link it to current user
  399. playlist_id=playlist_id,
  400. name=item['snippet']['title'],
  401. description=item['snippet']['description'],
  402. published_at=item['snippet']['publishedAt'],
  403. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  404. channel_id=item['snippet']['channelId'] if 'channelId' in
  405. item['snippet'] else '',
  406. channel_name=item['snippet']['channelTitle'] if 'channelTitle' in
  407. item[
  408. 'snippet'] else '',
  409. video_count=item['contentDetails']['itemCount'],
  410. is_private_on_yt=True if item['status']['privacyStatus'] == 'private' else False,
  411. playlist_yt_player_HTML=item['player']['embedHtml'],
  412. user=current_user
  413. )
  414. playlist.save()
  415. return result
  416. def getAllVideosForPlaylist(self, user, playlist_id):
  417. current_user = user.profile
  418. credentials = self.getCredentials(user)
  419. playlist = current_user.playlists.get(playlist_id__exact=playlist_id)
  420. ### GET ALL VIDEO IDS FROM THE PLAYLIST
  421. video_ids = [] # stores list of all video ids for a given playlist
  422. with build('youtube', 'v3', credentials=credentials) as youtube:
  423. pl_request = youtube.playlistItems().list(
  424. part='contentDetails, snippet, status',
  425. playlistId=playlist_id, # get all playlist videos details for this playlist id
  426. maxResults=50
  427. )
  428. # execute the above request, and store the response
  429. pl_response = pl_request.execute()
  430. for item in pl_response['items']:
  431. video_id = item['contentDetails']['videoId']
  432. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  433. if (item['snippet']['title'] == "Deleted video" and
  434. item['snippet']['description'] == "This video is unavailable.") or (
  435. item['snippet']['title'] == "Private video" and item['snippet'][
  436. 'description'] == "This video is private."):
  437. video = Video(
  438. video_id=video_id,
  439. name=item['snippet']['title'],
  440. is_unavailable_on_yt=True,
  441. playlist=playlist,
  442. video_position=item['snippet']['position'] + 1
  443. )
  444. video.save()
  445. else:
  446. video = Video(
  447. video_id=video_id,
  448. published_at=item['contentDetails']['videoPublishedAt'] if 'videoPublishedAt' in
  449. item[
  450. 'contentDetails'] else None,
  451. name=item['snippet']['title'],
  452. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  453. channel_id=item['snippet']['channelId'],
  454. channel_name=item['snippet']['channelTitle'],
  455. description=item['snippet']['description'],
  456. video_position=item['snippet']['position'] + 1,
  457. playlist=playlist
  458. )
  459. video.save()
  460. video_ids.append(video_id)
  461. else: # video found in db
  462. video = playlist.videos.get(video_id=video_id)
  463. # check if the video became unavailable on youtube
  464. if (item['snippet']['title'] == "Deleted video" and
  465. item['snippet']['description'] == "This video is unavailable.") or (
  466. item['snippet']['title'] == "Private video" and item['snippet'][
  467. 'description'] == "This video is private."):
  468. video.was_deleted_on_yt = True
  469. video.is_duplicate = True
  470. playlist.has_duplicate_videos = True
  471. video.save()
  472. while True:
  473. try:
  474. pl_request = youtube.playlistItems().list_next(pl_request, pl_response)
  475. pl_response = pl_request.execute()
  476. for item in pl_response['items']:
  477. video_id = item['contentDetails']['videoId']
  478. if playlist.videos.filter(video_id=video_id).count() == 0: # video DNE
  479. if (item['snippet']['title'] == "Deleted video" and
  480. item['snippet']['description'] == "This video is unavailable.") or (
  481. item['snippet']['title'] == "Private video" and item['snippet'][
  482. 'description'] == "This video is private."):
  483. video = Video(
  484. video_id=video_id,
  485. published_at=item['contentDetails'][
  486. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  487. 'contentDetails'] else None,
  488. name=item['snippet']['title'],
  489. is_unavailable_on_yt=True,
  490. playlist=playlist,
  491. video_position=item['snippet']['position'] + 1
  492. )
  493. video.save()
  494. else:
  495. video = Video(
  496. video_id=video_id,
  497. published_at=item['contentDetails'][
  498. 'videoPublishedAt'] if 'videoPublishedAt' in item[
  499. 'contentDetails'] else None,
  500. name=item['snippet']['title'],
  501. thumbnail_url=getThumbnailURL(item['snippet']['thumbnails']),
  502. channel_id=item['snippet']['channelId'],
  503. channel_name=item['snippet']['channelTitle'],
  504. video_position=item['snippet']['position'] + 1,
  505. playlist=playlist
  506. )
  507. video.save()
  508. video_ids.append(video_id)
  509. else: # video found in db
  510. video = playlist.videos.get(video_id=video_id)
  511. # check if the video became unavailable on youtube
  512. if (item['snippet']['title'] == "Deleted video" and
  513. item['snippet']['description'] == "This video is unavailable.") or (
  514. item['snippet']['title'] == "Private video" and item['snippet'][
  515. 'description'] == "This video is private."):
  516. video.was_deleted_on_yt = True
  517. video.is_duplicate = True
  518. playlist.has_duplicate_videos = True
  519. video.save()
  520. except AttributeError:
  521. break
  522. # API expects the video ids to be a string of comma seperated values, not a python list
  523. video_ids_strings = getVideoIdsStrings(video_ids)
  524. # store duration of all the videos in the playlist
  525. vid_durations = []
  526. for video_ids_string in video_ids_strings:
  527. # query the videos resource using API with the string above
  528. vid_request = youtube.videos().list(
  529. part="contentDetails,player,snippet,statistics", # get details of eac video
  530. id=video_ids_string,
  531. maxResults=50
  532. )
  533. vid_response = vid_request.execute()
  534. for item in vid_response['items']:
  535. duration = item['contentDetails']['duration']
  536. vid = playlist.videos.get(video_id=item['id'])
  537. vid.duration = duration.replace("PT", "")
  538. vid.duration_in_seconds = calculateDuration([duration])
  539. vid.has_cc = True if item['contentDetails']['caption'].lower() == 'true' else False
  540. vid.view_count = item['statistics']['viewCount'] if 'viewCount' in item[
  541. 'statistics'] else -1
  542. vid.like_count = item['statistics']['likeCount'] if 'likeCount' in item[
  543. 'statistics'] else -1
  544. vid.dislike_count = item['statistics']['dislikeCount'] if 'dislikeCount' in item[
  545. 'statistics'] else -1
  546. vid.yt_player_HTML = item['player']['embedHtml'] if 'embedHtml' in item['player'] else ''
  547. vid.save()
  548. vid_durations.append(duration)
  549. playlist_duration_in_seconds = calculateDuration(vid_durations)
  550. playlist.playlist_duration_in_seconds = playlist_duration_in_seconds
  551. playlist.playlist_duration = str(timedelta(seconds=playlist_duration_in_seconds))
  552. if len(video_ids) != len(vid_durations): # that means some videos in the playlist are deleted
  553. playlist.has_unavailable_videos = True
  554. playlist.is_in_db = True
  555. playlist.save()
  556. class Playlist(models.Model):
  557. # playlist details
  558. playlist_id = models.CharField(max_length=150)
  559. name = models.CharField(max_length=150, blank=True)
  560. thumbnail_url = models.CharField(max_length=420, blank=True)
  561. description = models.CharField(max_length=420, default="No description")
  562. video_count = models.IntegerField(default=0)
  563. published_at = models.DateTimeField(blank=True, null=True)
  564. # 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>"
  565. playlist_yt_player_HTML = models.CharField(max_length=420, blank=True)
  566. user = models.ForeignKey(Profile, on_delete=models.CASCADE,
  567. related_name="playlists") # a user can have many playlists
  568. playlist_duration = models.CharField(max_length=69, blank=True) # string version of playlist dureation
  569. playlist_duration_in_seconds = models.IntegerField(default=0)
  570. has_unavailable_videos = models.BooleanField(default=False) # if videos in playlist are private/deleted
  571. # playlist is made by this channel
  572. channel_id = models.CharField(max_length=420, blank=True)
  573. channel_name = models.CharField(max_length=420, blank=True)
  574. user_notes = models.CharField(max_length=420, default="") # user can take notes on the playlist and save them
  575. user_label = models.CharField(max_length=100, default="") # custom user given name for this playlist
  576. # manage playlist
  577. marked_as = models.CharField(default="",
  578. max_length=100) # can be set to "none", "watching", "on-hold", "plan-to-watch"
  579. is_favorite = models.BooleanField(default=False, blank=True) # to mark playlist as fav
  580. num_of_accesses = models.IntegerField(default="0") # tracks num of times this playlist was opened by user
  581. is_private_on_yt = models.BooleanField(default=False)
  582. is_user_owned = models.BooleanField(default=True) # represents YouTube playlist owned by user
  583. has_duplicate_videos = models.BooleanField(default=False) # duplicate videos will not be shown on site
  584. has_playlist_changed = models.BooleanField(default=False) # determines whether playlist was modified online or not
  585. playlist_changed_text = models.CharField(max_length=420, default="") # user friendly text to display what changed and how much changed
  586. # for UI
  587. view_in_grid_mode = models.BooleanField(default=False) # if False, videso will be showed in a list
  588. # set playlist manager
  589. objects = PlaylistManager()
  590. # for import
  591. is_in_db = models.BooleanField(default=False) # is true when all the videos of a playlist have been imported
  592. created_at = models.DateTimeField(auto_now_add=True)
  593. updated_at = models.DateTimeField(auto_now=True)
  594. def __str__(self):
  595. return "Playlist Len " + str(self.video_count)
  596. class Video(models.Model):
  597. # video details
  598. video_id = models.CharField(max_length=100)
  599. name = models.CharField(max_length=100, blank=True)
  600. duration = models.CharField(max_length=100, blank=True)
  601. duration_in_seconds = models.IntegerField(default=0)
  602. thumbnail_url = models.CharField(max_length=420, blank=True)
  603. published_at = models.DateTimeField(blank=True, null=True)
  604. description = models.CharField(max_length=420, default="")
  605. has_cc = models.BooleanField(default=False, blank=True, null=True)
  606. user_notes = models.CharField(max_length=420, default="") # user can take notes on the video and save them
  607. # video stats
  608. view_count = models.IntegerField(default=0)
  609. like_count = models.IntegerField(default=0)
  610. dislike_count = models.IntegerField(default=0)
  611. yt_player_HTML = models.CharField(max_length=420, blank=True)
  612. # video is made by this channel
  613. channel_id = models.CharField(max_length=420, blank=True)
  614. channel_name = models.CharField(max_length=420, blank=True)
  615. # which playlist this video belongs to, and position of that video in the playlist (i.e ALL videos belong to some pl)
  616. playlist = models.ForeignKey(Playlist, related_name="videos", on_delete=models.CASCADE)
  617. video_position = models.CharField(max_length=69, blank=True)
  618. # manage video
  619. is_duplicate = models.BooleanField(default=False) # True if the same video exists more than once in the playlist
  620. is_unavailable_on_yt = models.BooleanField(
  621. default=False) # True if the video was unavailable (private/deleted) when the API call was first made
  622. was_deleted_on_yt = models.BooleanField(default=False) # True if video became unavailable on a subsequent API call
  623. is_marked_as_watched = models.BooleanField(default=False, blank=True) # mark video as watched
  624. is_favorite = models.BooleanField(default=False, blank=True) # mark video as favorite
  625. num_of_accesses = models.CharField(max_length=69,
  626. default="0") # tracks num of times this video was clicked on by user
  627. user_label = models.CharField(max_length=100, default="") # custom user given name for this video