models.py 77 KB

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