Add initial support for station icon proxying and conversion
Station icons now get converted to a JPEG with a maxmimum dimension of 290 on icon request. Although we need to implement caching and aspect ratio keeping, this should fix issues with incompatible station icons and HTTPS icon URLs. Adds a new package dependency: Pillow
This commit is contained in:
parent
f1a9885e53
commit
72a8df3ed9
6 changed files with 68 additions and 7 deletions
|
@ -60,6 +60,7 @@ Python packages:
|
|||
* `requests`
|
||||
* `flask`
|
||||
* `PyYAML`
|
||||
* `Pillow`
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -126,5 +127,4 @@ You can also have a look at the provided [example](examples/stations.yml.example
|
|||
|
||||
* vTuner compatible AVRs don't do HTTPS. As such, YCast blindly rewrites every HTTPS station URL to HTTP. Most station
|
||||
providers which utilize HTTPS for their stations also provide an HTTP stream. Thus, most HTTPS stations should work.
|
||||
* Some station logos are not compatible with the vTuner frontend.
|
||||
* The built-in bookmark function does not work at the moment. You need to manually add your favourite stations for now.
|
||||
|
|
2
setup.py
2
setup.py
|
@ -40,6 +40,6 @@ setup(
|
|||
'onkyo',
|
||||
'denon'
|
||||
],
|
||||
install_requires=['requests', 'flask', 'PyYAML'],
|
||||
install_requires=['requests', 'flask', 'PyYAML', 'Pillow'],
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests'])
|
||||
)
|
||||
|
|
|
@ -17,9 +17,10 @@ class Station:
|
|||
self.name = name
|
||||
self.url = url
|
||||
self.tag = category
|
||||
self.icon = None
|
||||
|
||||
def to_vtuner(self):
|
||||
return vtuner.Station(self.id, self.name, self.tag, self.url, None, self.tag, None, None, None, None)
|
||||
return vtuner.Station(self.id, self.name, self.tag, self.url, self.icon, self.tag, None, None, None, None)
|
||||
|
||||
|
||||
def set_config(config):
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import logging
|
||||
|
||||
from flask import Flask, request, url_for, redirect, abort
|
||||
from flask import Flask, request, url_for, redirect, abort, make_response
|
||||
|
||||
import ycast.vtuner as vtuner
|
||||
import ycast.radiobrowser as radiobrowser
|
||||
import ycast.my_stations as my_stations
|
||||
import ycast.generic as generic
|
||||
import ycast.station_icons as station_icons
|
||||
|
||||
|
||||
PATH_ROOT = 'ycast'
|
||||
PATH_PLAY = 'play'
|
||||
PATH_STATION = 'station'
|
||||
PATH_SEARCH = 'search'
|
||||
PATH_ICON = 'icon'
|
||||
PATH_MY_STATIONS = 'my_stations'
|
||||
PATH_RADIOBROWSER = 'radiobrowser'
|
||||
PATH_RADIOBROWSER_COUNTRY = 'country'
|
||||
|
@ -60,6 +62,7 @@ def get_stations_page(stations, request, tracked=True):
|
|||
vtuner_station = station.to_vtuner()
|
||||
if tracked:
|
||||
vtuner_station.set_trackurl(request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid)
|
||||
vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid
|
||||
page.add(vtuner_station)
|
||||
page.set_count(len(stations))
|
||||
return page
|
||||
|
@ -239,7 +242,30 @@ def get_station_info(tracked=True):
|
|||
vtuner_station = station.to_vtuner()
|
||||
if tracked:
|
||||
vtuner_station.set_trackurl(request.host_url + PATH_ROOT + '/' + PATH_PLAY + '?id=' + vtuner_station.uid)
|
||||
vtuner_station.icon = request.host_url + PATH_ROOT + '/' + PATH_ICON + '?id=' + vtuner_station.uid
|
||||
page = vtuner.Page()
|
||||
page.add(vtuner_station)
|
||||
page.set_count(1)
|
||||
return page.to_string()
|
||||
|
||||
|
||||
@app.route('/' + PATH_ROOT + '/' + PATH_ICON)
|
||||
def get_station_icon():
|
||||
stationid = request.args.get('id')
|
||||
if not stationid:
|
||||
logging.error("Station icon without station ID requested")
|
||||
abort(400)
|
||||
station = get_station_by_id(stationid)
|
||||
if not station:
|
||||
logging.error("Could not get station with id '%s'", stationid)
|
||||
abort(404)
|
||||
if not hasattr(station, 'icon') or not station.icon:
|
||||
logging.warning("No icon information found for station with id '%s'", stationid)
|
||||
abort(404)
|
||||
station_icon = station_icons.get_icon_from_url(station.icon)
|
||||
if not station_icon:
|
||||
logging.error("Could not convert station icon for station with id '%s'", stationid)
|
||||
abort(404)
|
||||
response = make_response(station_icon)
|
||||
response.headers.set('Content-Type', 'image/jpeg')
|
||||
return response
|
||||
|
|
34
ycast/station_icons.py
Normal file
34
ycast/station_icons.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import logging
|
||||
import requests
|
||||
import io
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import ycast.generic as generic
|
||||
from ycast import __version__
|
||||
|
||||
MAX_SIZE = 290
|
||||
|
||||
|
||||
def get_icon_from_url(iconurl):
|
||||
# TODO cache icons on disk
|
||||
headers = {'User-Agent': generic.USER_AGENT + '/' + __version__}
|
||||
try:
|
||||
response = requests.get(iconurl, headers=headers)
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
logging.error("Connection to station icon URL failed (%s)", err)
|
||||
return None
|
||||
if response.status_code != 200:
|
||||
logging.error("Could not get station icon data from %s (HTML status %s)", iconurl, response.status_code)
|
||||
return None
|
||||
try:
|
||||
image = Image.open(io.BytesIO(response.content))
|
||||
image = image.convert("RGB")
|
||||
image = image.resize((MAX_SIZE, MAX_SIZE), Image.ANTIALIAS) # TODO: keep aspect ratio
|
||||
with io.BytesIO() as output_img:
|
||||
image.save(output_img, format="JPEG")
|
||||
image_conv = output_img.getvalue()
|
||||
except Exception as e:
|
||||
logging.error("Station icon conversion error (%s)", e)
|
||||
return None
|
||||
return image_conv
|
|
@ -108,13 +108,13 @@ class Directory:
|
|||
|
||||
|
||||
class Station:
|
||||
def __init__(self, uid, name, description, url, logo, genre, location, mime, bitrate, bookmark):
|
||||
def __init__(self, uid, name, description, url, icon, genre, location, mime, bitrate, bookmark):
|
||||
self.uid = uid
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.url = strip_https(url)
|
||||
self.trackurl = None
|
||||
self.logo = logo
|
||||
self.icon = icon
|
||||
self.genre = genre
|
||||
self.location = location
|
||||
self.mime = mime
|
||||
|
@ -134,7 +134,7 @@ class Station:
|
|||
else:
|
||||
ET.SubElement(item, 'StationUrl').text = self.url
|
||||
ET.SubElement(item, 'StationDesc').text = self.description
|
||||
ET.SubElement(item, 'Logo').text = self.logo
|
||||
ET.SubElement(item, 'Logo').text = self.icon
|
||||
ET.SubElement(item, 'StationFormat').text = self.genre
|
||||
ET.SubElement(item, 'StationLocation').text = self.location
|
||||
ET.SubElement(item, 'StationBandWidth').text = self.bitrate
|
||||
|
|
Loading…
Reference in a new issue