models.py 74 KB

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