initial commit
This commit is contained in:
commit
bbc4e55e55
78
README.md
Normal file
78
README.md
Normal file
|
@ -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.
|
||||||
|
|
22
stations.yml.example
Normal file
22
stations.yml.example
Normal file
|
@ -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
|
93
ycast.py
Executable file
93
ycast.py
Executable file
|
@ -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 = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'
|
||||||
|
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()
|
Loading…
Reference in a new issue