2
0

models.py 76 KB

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