local_playlist.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. from youtube import util, yt_data_extract
  2. from youtube import yt_app
  3. import settings
  4. import os
  5. import json
  6. import html
  7. import gevent
  8. import urllib
  9. import math
  10. import flask
  11. from flask import request
  12. playlists_directory = os.path.join(settings.data_dir, "playlists")
  13. thumbnails_directory = os.path.join(settings.data_dir, "playlist_thumbnails")
  14. def video_ids_in_playlist(name):
  15. try:
  16. with open(os.path.join(playlists_directory, name + ".txt"), 'r', encoding='utf-8') as file:
  17. videos = file.read()
  18. return set(json.loads(video)['id'] for video in videos.splitlines())
  19. except FileNotFoundError:
  20. return set()
  21. def add_to_playlist(name, video_info_list):
  22. if not os.path.exists(playlists_directory):
  23. os.makedirs(playlists_directory)
  24. ids = video_ids_in_playlist(name)
  25. missing_thumbnails = []
  26. with open(os.path.join(playlists_directory, name + ".txt"), "a", encoding='utf-8') as file:
  27. for info in video_info_list:
  28. id = json.loads(info)['id']
  29. if id not in ids:
  30. file.write(info + "\n")
  31. missing_thumbnails.append(id)
  32. gevent.spawn(util.download_thumbnails, os.path.join(thumbnails_directory, name), missing_thumbnails)
  33. def add_extra_info_to_videos(videos, playlist_name):
  34. '''Adds extra information necessary for rendering the video item HTML
  35. Downloads missing thumbnails'''
  36. try:
  37. thumbnails = set(os.listdir(os.path.join(thumbnails_directory,
  38. playlist_name)))
  39. except FileNotFoundError:
  40. thumbnails = set()
  41. missing_thumbnails = []
  42. for video in videos:
  43. video['type'] = 'video'
  44. util.add_extra_html_info(video)
  45. if video['id'] + '.jpg' in thumbnails:
  46. video['thumbnail'] = (
  47. '/https://youtube.com/data/playlist_thumbnails/'
  48. + playlist_name
  49. + '/' + video['id'] + '.jpg')
  50. else:
  51. video['thumbnail'] = util.get_thumbnail_url(video['id'])
  52. missing_thumbnails.append(video['id'])
  53. gevent.spawn(util.download_thumbnails,
  54. os.path.join(thumbnails_directory, playlist_name),
  55. missing_thumbnails)
  56. def read_playlist(name):
  57. '''Returns a list of videos for the given playlist name'''
  58. playlist_path = os.path.join(playlists_directory, name + '.txt')
  59. with open(playlist_path, 'r', encoding='utf-8') as f:
  60. data = f.read()
  61. videos = []
  62. videos_json = data.splitlines()
  63. for video_json in videos_json:
  64. try:
  65. info = json.loads(video_json)
  66. videos.append(info)
  67. except json.decoder.JSONDecodeError:
  68. if not video_json.strip() == '':
  69. print('Corrupt playlist video entry: ' + video_json)
  70. return videos
  71. def get_local_playlist_videos(name, offset=0, amount=50):
  72. videos = read_playlist(name)
  73. add_extra_info_to_videos(videos, name)
  74. return videos[offset:offset+amount], len(videos)
  75. def get_playlist_names():
  76. try:
  77. items = os.listdir(playlists_directory)
  78. except FileNotFoundError:
  79. return
  80. for item in items:
  81. name, ext = os.path.splitext(item)
  82. if ext == '.txt':
  83. yield name
  84. def remove_from_playlist(name, video_info_list):
  85. ids = [json.loads(video)['id'] for video in video_info_list]
  86. with open(os.path.join(playlists_directory, name + ".txt"), 'r', encoding='utf-8') as file:
  87. videos = file.read()
  88. videos_in = videos.splitlines()
  89. videos_out = []
  90. for video in videos_in:
  91. if json.loads(video)['id'] not in ids:
  92. videos_out.append(video)
  93. with open(os.path.join(playlists_directory, name + ".txt"), 'w', encoding='utf-8') as file:
  94. file.write("\n".join(videos_out) + "\n")
  95. try:
  96. thumbnails = set(os.listdir(os.path.join(thumbnails_directory, name)))
  97. except FileNotFoundError:
  98. pass
  99. else:
  100. to_delete = thumbnails & set(id + ".jpg" for id in ids)
  101. for file in to_delete:
  102. os.remove(os.path.join(thumbnails_directory, name, file))
  103. return len(videos_out)
  104. @yt_app.route('/playlists', methods=['GET'])
  105. @yt_app.route('/playlists/<playlist_name>', methods=['GET'])
  106. def get_local_playlist_page(playlist_name=None):
  107. if playlist_name is None:
  108. playlists = [(name, util.URL_ORIGIN + '/playlists/' + name) for name in get_playlist_names()]
  109. return flask.render_template('local_playlists_list.html', playlists=playlists)
  110. else:
  111. page = int(request.args.get('page', 1))
  112. offset = 50*(page - 1)
  113. videos, num_videos = get_local_playlist_videos(playlist_name, offset=offset, amount=50)
  114. return flask.render_template(
  115. 'local_playlist.html',
  116. header_playlist_names=get_playlist_names(),
  117. playlist_name=playlist_name,
  118. videos=videos,
  119. num_pages=math.ceil(num_videos/50),
  120. parameters_dictionary=request.args,
  121. )
  122. @yt_app.route('/playlists/<playlist_name>', methods=['POST'])
  123. def path_edit_playlist(playlist_name):
  124. '''Called when making changes to the playlist from that playlist's page'''
  125. if request.values['action'] == 'remove':
  126. videos_to_remove = request.values.getlist('video_info_list')
  127. number_of_videos_remaining = remove_from_playlist(playlist_name, videos_to_remove)
  128. redirect_page_number = min(int(request.values.get('page', 1)), math.ceil(number_of_videos_remaining/50))
  129. return flask.redirect(util.URL_ORIGIN + request.path + '?page=' + str(redirect_page_number))
  130. elif request.values['action'] == 'remove_playlist':
  131. try:
  132. os.remove(os.path.join(playlists_directory, playlist_name + ".txt"))
  133. except OSError:
  134. pass
  135. return flask.redirect(util.URL_ORIGIN + '/playlists')
  136. elif request.values['action'] == 'export':
  137. videos = read_playlist(playlist_name)
  138. fmt = request.values['export_format']
  139. if fmt in ('ids', 'urls'):
  140. prefix = ''
  141. if fmt == 'urls':
  142. prefix = 'https://www.youtube.com/watch?v='
  143. id_list = '\n'.join(prefix + v['id'] for v in videos)
  144. id_list += '\n'
  145. resp = flask.Response(id_list, mimetype='text/plain')
  146. cd = 'attachment; filename="%s.txt"' % playlist_name
  147. resp.headers['Content-Disposition'] = cd
  148. return resp
  149. elif fmt == 'json':
  150. json_data = json.dumps({'videos': videos}, indent=2,
  151. sort_keys=True)
  152. resp = flask.Response(json_data, mimetype='text/json')
  153. cd = 'attachment; filename="%s.json"' % playlist_name
  154. resp.headers['Content-Disposition'] = cd
  155. return resp
  156. else:
  157. flask.abort(400)
  158. else:
  159. flask.abort(400)
  160. @yt_app.route('/edit_playlist', methods=['POST'])
  161. def edit_playlist():
  162. '''Called when adding videos to a playlist from elsewhere'''
  163. if request.values['action'] == 'add':
  164. add_to_playlist(request.values['playlist_name'], request.values.getlist('video_info_list'))
  165. return '', 204
  166. else:
  167. flask.abort(400)
  168. @yt_app.route('/data/playlist_thumbnails/<playlist_name>/<thumbnail>')
  169. def serve_thumbnail(playlist_name, thumbnail):
  170. # .. is necessary because flask always uses the application directory at ./youtube, not the working directory
  171. return flask.send_from_directory(
  172. os.path.join('..', thumbnails_directory, playlist_name), thumbnail)