models.py 80 KB

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