models.py 70 KB

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