models.py 76 KB

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