2
0

models.py 76 KB

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