models.py 76 KB

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