commit bbc4e55e55a2990e0be177547ab4c9ce6315c5dd Author: milaq Date: Mon Jul 23 14:23:57 2018 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4e8138 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# YCast + +YCast is a self hosted replacement for the vTuner internet radio service which some Yamaha AVRs use. + +It was developed for and tested with the __RX-Vx73__ series. + +It _should_ also work for the following Yamaha AVR models: + * RX-Vx75 + * RX-Vx77 + * RX-Vx79 + * RX-Vx81 + +YCast is for you if: + * You do not want to use a proprietary streaming service + * You are sick of loading and/or downtimes of the vRadio server + * You are unsure about the vTuner service's future + +## Dependencies: +Python version: `3` + +Python packages: + * `PyYAML` + +## Usage + +YCast really does not need much computing power nor bandwidth. It just serves the information to the AVR. The streaming +itself gets handled by the AVR directly, i.e. you can run it on a low-spec RISC machine like a Raspberry Pi. + +* Create your initial `stations.yml`. The config follows a basic YAML structure (see below) +* Create a manual entry in your DNS server (read 'Router' for most home users) for: + + `radioyamaha.vtuner.com` + + to point to the local machine running YCast. + +* Run `ycast.py` on the target machine. + +### Station configuration +``` +Category one name: + First awesome station name: first.awesome/station/URL + Second awesome station name: second.awesome/station/URL + +Category two name: + Third awesome station name: third.awesome/station/URL + Fourth awesome station name: fourth.awesome/station/URL +``` + +You can also have a look at `stations.yml.example` for how it can be set up. + + +## Firewall rules + + * The server running YCast does __not__ need internet access + * The Yamaha AVR needs access to the internet (i.e. to the station URLs you defined) + * The Yamaha AVR needs to reach port `80` of the machine running YCast + +## Web redirects + +You can (__and should__) change the `listen_port` and `listen_address` variables in `ycast.py` if you are already running a HTTP server on the target machine +and/or want to proxy or encase YCast access. + +It is advised to use a proper webserver (e.g. Nginx) in front of YCast if you can. +Then, you also don't need to run YCast as root and can proxy the requests to YCast running on a higher port (>1024) listening only on `localhost`. + +You _need_ to redirect the following URLs from your webserver to YCast (of course listening to requests to `radioyamaha.vtuner.com`): + * `/setupapp` + * `/ycast` + + +## Caveats + +YCast was a quick and dirty project to lay the foundation for having a self hosted vTuner emulation. + +It is a barebone service at the moment. It provides your AVR with the basic info it needs to play internet radio stations. +Maybe this will change in the future. +But for now just station names and URLs. No web-based management interface, no coverart, no fancy stuff. + diff --git a/stations.yml.example b/stations.yml.example new file mode 100644 index 0000000..ee100e7 --- /dev/null +++ b/stations.yml.example @@ -0,0 +1,22 @@ +Electronic: + Deep House Lounge: http://198.15.94.34:8006 + Ibiza Sonica: http://s1.sonicabroadcast.com:7005/stream + Bassdrive: http://50.7.98.106:8200 + SomaFM Fluid: http://ice1.somafm.com/fluid-128-mp3 + +Chillout: + Joint Radio: http://radio.jointil.net:9998 + SomaFM DEF CON Radio: http://ice1.somafm.com/defcon-256-mp3 + SomaFM Drone Zone: http://ice1.somafm.com/dronezone-256-mp3 + SomaFM Mission Control: http://ice1.somafm.com/missioncontrol-128-mp3 + The Jazz Groove: http://west-mp3-128.streamthejazzgroove.com + Radionomy Downbeat: http://streaming.radionomy.com/TempoOfTheDownbeat1 + +Casual: + 76Radio: http://192.240.102.133:9566/stream + SomaFM Beat Blender: http://ice1.somafm.com/beatblender-128-mp3 + Jazz Radio Electro Swing: http://jazz-wr04.ice.infomaniak.ch/jazz-wr04-128.mp3 + SomaFM Groove Salad: http://ice1.somafm.com/groovesalad-256-mp3 + SomaFM Lush: http://ice1.somafm.com/lush-128-mp3 + Allzic Radio R&B: http://allzic10.ice.infomaniak.ch/allzic10.mp3 + The UK 1940s Radio Station: http://91.121.134.23:8100/1 diff --git a/ycast.py b/ycast.py new file mode 100755 index 0000000..bc01846 --- /dev/null +++ b/ycast.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import os +from http.server import BaseHTTPRequestHandler, HTTPServer +import xml.etree.cElementTree as etree + +import yaml + +listen_address = '0.0.0.0' +listen_port = 80 + +VTUNER_DNS = 'http://radioyamaha.vtuner.com' +VTUNER_INITURL = '/setupapp/Yamaha/asp/BrowseXML/loginXML.asp' +XMLHEADER = '' +YCAST_LOCATION = 'ycast' + +stations = {} + + +def get_stations(): + global stations + ycast_dir = os.path.dirname(os.path.realpath(__file__)) + with open(ycast_dir + '/stations.yml', 'r') as f: + stations = yaml.load(f) + + +def text_to_url(text): + return text.replace(' ', '%20') + + +def url_to_text(url): + return url.replace('%20', ' ') + + +class YCastServer(BaseHTTPRequestHandler): + def do_GET(self): + get_stations() + if self.path == '/' \ + or self.path == '/' + YCAST_LOCATION \ + or self.path == '/' + YCAST_LOCATION + '/'\ + or self.path.startswith(VTUNER_INITURL): + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(bytes(XMLHEADER, 'utf-8')) + xml = self.create_root() + for category in sorted(stations, key=str.lower): + self.add_dir(xml, category, + VTUNER_DNS + ':' + str(listen_port) + '/' + YCAST_LOCATION + '/' + text_to_url(category)) + self.wfile.write(bytes(etree.tostring(xml).decode('utf-8'), 'utf-8')) + elif self.path.startswith('/' + YCAST_LOCATION + '/'): + category = url_to_text(self.path[len(YCAST_LOCATION) + 2:].partition('?')[0]) + if category not in stations: + self.send_error(404) + return + xml = self.create_root() + for station in sorted(stations[category], key=str.lower): + self.add_station(xml, station, stations[category][station]) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(bytes(XMLHEADER, 'utf-8')) + self.wfile.write(bytes(etree.tostring(xml).decode('utf-8'), 'utf-8')) + else: + self.send_error(404) + + def create_root(self): + return etree.Element('ListOfItems') + + def add_dir(self, root, name, dest): + item = etree.SubElement(root, 'Item') + etree.SubElement(item, 'ItemType').text = 'Dir' + etree.SubElement(item, 'Title').text = name + etree.SubElement(item, 'UrlDir').text = dest + return item + + def add_station(self, root, name, url): + item = etree.SubElement(root, 'Item') + etree.SubElement(item, 'ItemType').text = 'Station' + etree.SubElement(item, 'StationName').text = name + etree.SubElement(item, 'StationUrl').text = url + return item + + +get_stations() +server = HTTPServer((listen_address, listen_port), YCastServer) +print('YCast server listening on %s:%s' % (listen_address, listen_port)) +try: + server.serve_forever() +except KeyboardInterrupt: + pass +print('YCast server shutting down') +server.server_close()