server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import logging
  2. import re
  3. from flask import Flask, request, url_for, redirect, abort, make_response
  4. import ycast.vtuner as vtuner
  5. import ycast.radiobrowser as radiobrowser
  6. import ycast.my_stations as my_stations
  7. import ycast.generic as generic
  8. import ycast.station_icons as station_icons
  9. PATH_ROOT = 'ycast'
  10. PATH_PLAY = 'play'
  11. PATH_STATION = 'station'
  12. PATH_SEARCH = 'search'
  13. PATH_ICON = 'icon'
  14. PATH_MY_STATIONS = 'my_stations'
  15. PATH_RADIOBROWSER = 'radiobrowser'
  16. PATH_RADIOBROWSER_COUNTRY = 'country'
  17. PATH_RADIOBROWSER_LANGUAGE = 'language'
  18. PATH_RADIOBROWSER_GENRE = 'genre'
  19. PATH_RADIOBROWSER_POPULAR = 'popular'
  20. station_tracking = False
  21. my_stations_enabled = False
  22. app = Flask(__name__)
  23. def run(config, address='0.0.0.0', port=8010):
  24. try:
  25. check_my_stations_feature(config)
  26. app.run(host=address, port=port)
  27. except PermissionError:
  28. logging.error("No permission to create socket. Are you trying to use ports below 1024 without elevated rights?")
  29. def check_my_stations_feature(config):
  30. global my_stations_enabled
  31. my_stations_enabled = my_stations.set_config(config)
  32. def get_directories_page(subdir, directories, request):
  33. page = vtuner.Page()
  34. if len(directories) == 0:
  35. page.add(vtuner.Display("No entries found"))
  36. page.set_count(1)
  37. return page
  38. for directory in get_paged_elements(directories, request.args):
  39. vtuner_directory = vtuner.Directory(directory.displayname,
  40. url_for(subdir, _external=True, directory=directory.name),
  41. directory.item_count)
  42. page.add(vtuner_directory)
  43. page.set_count(len(directories))
  44. return page
  45. def get_stations_page(stations, request):
  46. page = vtuner.Page()
  47. if len(stations) == 0:
  48. page.add(vtuner.Display("No stations found"))
  49. page.set_count(1)
  50. return page
  51. for station in get_paged_elements(stations, request.args):
  52. vtuner_station = station.to_vtuner()
  53. if station_tracking:
  54. vtuner_station.set_trackurl(request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid)
  55. vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid
  56. page.add(vtuner_station)
  57. page.set_count(len(stations))
  58. return page
  59. def get_paged_elements(items, requestargs):
  60. if requestargs.get('startitems'):
  61. offset = int(requestargs.get('startitems')) - 1
  62. elif requestargs.get('startItems'):
  63. offset = int(requestargs.get('startItems')) - 1
  64. elif requestargs.get('start'):
  65. offset = int(requestargs.get('start')) - 1
  66. else:
  67. offset = 0
  68. if offset > len(items):
  69. logging.warning("Paging offset larger than item count")
  70. return []
  71. if requestargs.get('enditems'):
  72. limit = int(requestargs.get('enditems'))
  73. elif requestargs.get('endItems'):
  74. limit = int(requestargs.get('endItems'))
  75. elif requestargs.get('start') and requestargs.get('howmany'):
  76. limit = int(requestargs.get('start')) - 1 + int(requestargs.get('howmany'))
  77. else:
  78. limit = len(items)
  79. if limit < offset:
  80. logging.warning("Paging limit smaller than offset")
  81. return []
  82. if limit > len(items):
  83. limit = len(items)
  84. return items[offset:limit]
  85. def get_station_by_id(stationid, additional_info=False):
  86. station_id_prefix = generic.get_stationid_prefix(stationid)
  87. if station_id_prefix == my_stations.ID_PREFIX:
  88. return my_stations.get_station_by_id(generic.get_stationid_without_prefix(stationid))
  89. elif station_id_prefix == radiobrowser.ID_PREFIX:
  90. station = radiobrowser.get_station_by_id(generic.get_stationid_without_prefix(stationid))
  91. if additional_info:
  92. station.get_playable_url()
  93. return station
  94. return None
  95. def vtuner_redirect(url):
  96. if request and request.host and not re.search("^[A-Za-z0-9]+\.vtuner\.com$", request.host):
  97. logging.warning("You are not accessing a YCast redirect with a whitelisted host url (*.vtuner.com). "
  98. "Some AVRs have problems with this. The requested host was: %s", request.host)
  99. return redirect(url, code=302)
  100. @app.route('/setupapp/<path:path>',
  101. methods=['GET', 'POST'])
  102. def upstream(path):
  103. if request.args.get('token') == '0':
  104. return vtuner.get_init_token()
  105. if request.args.get('search') or request.args.get('Search'):
  106. return station_search()
  107. if 'statxml.asp' in path and request.args.get('id'):
  108. return get_station_info()
  109. if 'navXML.asp' in path:
  110. return radiobrowser_landing()
  111. if 'FavXML.asp' in path:
  112. return my_stations_landing()
  113. if 'loginXML.asp' in path:
  114. return landing()
  115. logging.error("Unhandled upstream query (/setupapp/%s)", path)
  116. abort(404)
  117. @app.route('/',
  118. defaults={'path': ''},
  119. methods=['GET', 'POST'])
  120. @app.route('/' + PATH_ROOT + '/',
  121. defaults={'path': ''},
  122. methods=['GET', 'POST'])
  123. def landing(path=''):
  124. page = vtuner.Page()
  125. page.add(vtuner.Directory('Radiobrowser', url_for('radiobrowser_landing', _external=True), 4))
  126. if my_stations_enabled:
  127. page.add(vtuner.Directory('My Stations', url_for('my_stations_landing', _external=True),
  128. len(my_stations.get_category_directories())))
  129. else:
  130. page.add(vtuner.Display("'My Stations' feature not configured."))
  131. page.set_count(1)
  132. return page.to_string()
  133. @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/',
  134. methods=['GET', 'POST'])
  135. def my_stations_landing():
  136. directories = my_stations.get_category_directories()
  137. return get_directories_page('my_stations_category', directories, request).to_string()
  138. @app.route('/' + PATH_ROOT + '/' + PATH_MY_STATIONS + '/<directory>',
  139. methods=['GET', 'POST'])
  140. def my_stations_category(directory):
  141. stations = my_stations.get_stations_by_category(directory)
  142. return get_stations_page(stations, request).to_string()
  143. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/',
  144. methods=['GET', 'POST'])
  145. def radiobrowser_landing():
  146. page = vtuner.Page()
  147. page.add(vtuner.Directory('Genres', url_for('radiobrowser_genres', _external=True),
  148. len(radiobrowser.get_genre_directories())))
  149. page.add(vtuner.Directory('Countries', url_for('radiobrowser_countries', _external=True),
  150. len(radiobrowser.get_country_directories())))
  151. page.add(vtuner.Directory('Languages', url_for('radiobrowser_languages', _external=True),
  152. len(radiobrowser.get_language_directories())))
  153. page.add(vtuner.Directory('Most Popular', url_for('radiobrowser_popular', _external=True),
  154. len(radiobrowser.get_stations_by_votes())))
  155. page.set_count(4)
  156. return page.to_string()
  157. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/',
  158. methods=['GET', 'POST'])
  159. def radiobrowser_countries():
  160. directories = radiobrowser.get_country_directories()
  161. return get_directories_page('radiobrowser_country_stations', directories, request).to_string()
  162. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_COUNTRY + '/<directory>',
  163. methods=['GET', 'POST'])
  164. def radiobrowser_country_stations(directory):
  165. stations = radiobrowser.get_stations_by_country(directory)
  166. return get_stations_page(stations, request).to_string()
  167. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/',
  168. methods=['GET', 'POST'])
  169. def radiobrowser_languages():
  170. directories = radiobrowser.get_language_directories()
  171. return get_directories_page('radiobrowser_language_stations', directories, request).to_string()
  172. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_LANGUAGE + '/<directory>',
  173. methods=['GET', 'POST'])
  174. def radiobrowser_language_stations(directory):
  175. stations = radiobrowser.get_stations_by_language(directory)
  176. return get_stations_page(stations, request).to_string()
  177. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/',
  178. methods=['GET', 'POST'])
  179. def radiobrowser_genres():
  180. directories = radiobrowser.get_genre_directories()
  181. return get_directories_page('radiobrowser_genre_stations', directories, request).to_string()
  182. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_GENRE + '/<directory>',
  183. methods=['GET', 'POST'])
  184. def radiobrowser_genre_stations(directory):
  185. stations = radiobrowser.get_stations_by_genre(directory)
  186. return get_stations_page(stations, request).to_string()
  187. @app.route('/' + PATH_ROOT + '/' + PATH_RADIOBROWSER + '/' + PATH_RADIOBROWSER_POPULAR + '/',
  188. methods=['GET', 'POST'])
  189. def radiobrowser_popular():
  190. stations = radiobrowser.get_stations_by_votes()
  191. return get_stations_page(stations, request).to_string()
  192. @app.route('/' + PATH_ROOT + '/' + PATH_SEARCH + '/',
  193. methods=['GET', 'POST'])
  194. def station_search():
  195. query = request.args.get('search')
  196. if not query:
  197. query = request.args.get('Search')
  198. if not query or len(query) < 3:
  199. page = vtuner.Page()
  200. page.add(vtuner.Display("Search query too short"))
  201. page.set_count(1)
  202. return page.to_string()
  203. else:
  204. # TODO: we also need to include 'my station' elements
  205. stations = radiobrowser.search(query)
  206. return get_stations_page(stations, request).to_string()
  207. @app.route('/' + PATH_ROOT + '/' + PATH_PLAY,
  208. methods=['GET', 'POST'])
  209. def get_stream_url():
  210. stationid = request.args.get('id')
  211. if not stationid:
  212. logging.error("Stream URL without station ID requested")
  213. abort(400)
  214. station = get_station_by_id(stationid, additional_info=True)
  215. if not station:
  216. logging.error("Could not get station with id '%s'", stationid)
  217. abort(404)
  218. logging.debug("Station with ID '%s' requested", station.id)
  219. return vtuner_redirect(station.url)
  220. @app.route('/' + PATH_ROOT + '/' + PATH_STATION,
  221. methods=['GET', 'POST'])
  222. def get_station_info():
  223. stationid = request.args.get('id')
  224. if not stationid:
  225. logging.error("Station info without station ID requested")
  226. abort(400)
  227. station = get_station_by_id(stationid, additional_info=(not station_tracking))
  228. if not station:
  229. logging.error("Could not get station with id '%s'", stationid)
  230. page = vtuner.Page()
  231. page.add(vtuner.Display("Station not found"))
  232. page.set_count(1)
  233. return page.to_string()
  234. vtuner_station = station.to_vtuner()
  235. if station_tracking:
  236. vtuner_station.set_trackurl(request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid)
  237. vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid
  238. page = vtuner.Page()
  239. page.add(vtuner_station)
  240. page.set_count(1)
  241. return page.to_string()
  242. @app.route('/' + PATH_ROOT + '/' + PATH_ICON,
  243. methods=['GET', 'POST'])
  244. def get_station_icon():
  245. stationid = request.args.get('id')
  246. if not stationid:
  247. logging.error("Station icon without station ID requested")
  248. abort(400)
  249. station = get_station_by_id(stationid)
  250. if not station:
  251. logging.error("Could not get station with id '%s'", stationid)
  252. abort(404)
  253. if not hasattr(station, 'icon') or not station.icon:
  254. logging.warning("No icon information found for station with id '%s'", stationid)
  255. abort(404)
  256. station_icon = station_icons.get_icon(station)
  257. if not station_icon:
  258. logging.error("Could not get station icon for station with id '%s'", stationid)
  259. abort(404)
  260. response = make_response(station_icon)
  261. response.headers.set('Content-Type', 'image/jpeg')
  262. return response