models.py 75 KB

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