models.py 70 KB

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