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:
milaq 2019-08-25 17:02:41 +02:00
parent f1a9885e53
commit 72a8df3ed9
6 changed files with 68 additions and 7 deletions

View file

@ -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.

View file

@ -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'])
)

View file

@ -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):

View file

@ -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
View 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

View file

@ -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