2
0

models.py 40 KB

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