initial commit
This commit is contained in:
commit
1358dc4e25
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
*.lock
|
15
Pipfile
Normal file
15
Pipfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
requests = "*"
|
||||||
|
py3dns = "*"
|
||||||
|
python-console-menu = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
pytest = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.8"
|
101
README.md
Normal file
101
README.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# radiostations python script
|
||||||
|
|
||||||
|
Simple python tool to grab radio stations from radio-browser.info, put them into a menu, and then use your local installation of mpg123 to play the station for you.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
* Linux (any flavor, but debian based is probably best)
|
||||||
|
* Python3
|
||||||
|
* Pipenv
|
||||||
|
* mpg123 (or mplayer, if you want to swap it in yourself)
|
||||||
|
|
||||||
|
|
||||||
|
### Install
|
||||||
|
```shell
|
||||||
|
pipenv install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execute
|
||||||
|
```shell
|
||||||
|
pipenv run python ./stations.py -h
|
||||||
|
|
||||||
|
usage: stations.py [-h] [-n NAME] [-c COUNTRY] [-s STATE] [-t TAGS]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-n NAME, --name NAME Name of station
|
||||||
|
-c COUNTRY, --country COUNTRY
|
||||||
|
Station country
|
||||||
|
-s STATE, --state STATE
|
||||||
|
Station state (if in US)
|
||||||
|
-t TAGS, --tags TAGS search tag or tags (if more than one, comma-separated
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Example (using mpg123):
|
||||||
|
```
|
||||||
|
> pipenv run python ./stations.py -n "wfmt"
|
||||||
|
|
||||||
|
Radio Player Menu
|
||||||
|
0. Exit menu
|
||||||
|
1. WFMT MP3 0 http://stream.wfmt.com/main-mp3
|
||||||
|
2. WFMT 98.7 Chicago - IL (AAC) AAC 256 http://wowza.wfmt.com/live/smil:wfmt.smil/playlist.m3u8
|
||||||
|
3. WFMT 98.7 Chicago - IL (MP3) MP3 0 http://stream.wfmt.com/main-mp3
|
||||||
|
Select Option: 3
|
||||||
|
High Performance MPEG 1.0/2.0/2.5 Audio Player for Layers 1, 2 and 3
|
||||||
|
version 1.26.3; written and copyright by Michael Hipp and others
|
||||||
|
free software (LGPL) without any warranty but with best wishes
|
||||||
|
|
||||||
|
Directory: http://stream.wfmt.com/
|
||||||
|
|
||||||
|
Terminal control enabled, press 'h' for listing of keys and functions.
|
||||||
|
|
||||||
|
Playing MPEG stream 1 of 1: main-mp3 ...
|
||||||
|
ICY-NAME: /main-mp3
|
||||||
|
ICY-URL: https://www.streamguys.com/
|
||||||
|
|
||||||
|
MPEG 1.0 L III cbr128 44100 j-s
|
||||||
|
|
||||||
|
ICY-META: StreamTitle='Virgil Thomson - Louisiana Story Suite - New London Orch/Ronald Corp - - Hyperion';
|
||||||
|
```
|
||||||
|
When I hit `Q`, mpg123 exits, and sends me back to my menu:
|
||||||
|
```
|
||||||
|
[1:56] Decoding of main-mp3 finished.
|
||||||
|
|
||||||
|
Radio Player Menu
|
||||||
|
0. Exit menu
|
||||||
|
1. WFMT MP3 0 http://stream.wfmt.com/main-mp3
|
||||||
|
2. WFMT 98.7 Chicago - IL (AAC) AAC 256 http://wowza.wfmt.com/live/smil:wfmt.smil/playlist.m3u8
|
||||||
|
3. WFMT 98.7 Chicago - IL (MP3) MP3 0 http://stream.wfmt.com/main-mp3
|
||||||
|
Select Option:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### CAVEATS
|
||||||
|
|
||||||
|
1. This is a VERY ROUGH script. If your search criteria are too broad, you'll get hundreds and hundres of menu items. I set the limit to 9999, and it seems to work ok. But don't push your luck.
|
||||||
|
2. mpg123 and mplayer are primitive at best. Not all the stations on the list will play correctly, without the propery config/plugins. So, just keep trying until you find one that works.
|
||||||
|
3. if you supply multiple tags in a comma-separated list, you may unintentionally filter out results. Unfortunately, the api at radio-info is such that the tag list you search for, has to be in precisely the order it is returned from the host. So, for example, if you search for "classical,chicago", your search will filter out WFMT, because their tags are "chicago,classical". So, best to keep your tags to a minimum (meaning 1 lol)
|
||||||
|
4. The country search is by country NAME, not CODE. So, "United States" will work, but "US" will not. Likewise for the United Kingdom.
|
||||||
|
5. It seems many of the stations put their city in the tag list. So, you can reduce the size of your results by doing something like this: `-c "United States" -t "atlanta"`, which makes more sense for radio stations anyway. Eg:
|
||||||
|
```
|
||||||
|
> pipenv run python ./stations.py -c "United States" -t "atlanta"
|
||||||
|
|
||||||
|
Radio Player Menu
|
||||||
|
0. Exit menu
|
||||||
|
1. WCLK HD2 Atlanta - GA MP3 64 http://provisioning.streamtheworld.com/pls/WCLKHD2.pls
|
||||||
|
2. WRAS 88.5 'Album 88' Atlanta - GA MP3 128 http://playerservices.streamtheworld.com/pls/WRASFM.pls
|
||||||
|
3. WREK 91.1 Atlanta - GA MP3 128 http://streaming.wrek.org:8000/main/128kb.mp3
|
||||||
|
4. WREK 91.1 Atlanta - GA -24 kbps MP3 MP3 24 http://streaming.wrek.org:8000/wrek_live-24kb-mono.m3u
|
||||||
|
5. WSB 750 & 95.5 Atlanta - GA AAC+ 49 http://oom-cmg.streamguys1.com/atl750/atl750-iheart.aac
|
||||||
|
6. WWWQ 99.7 'Q100' Atlanta - GA MP3 80 http://provisioning.streamtheworld.com/pls/WWWQFM.pls
|
||||||
|
7. WWWQ-HD2 '99X' Atlanta - GA MP3 80 http://provisioning.streamtheworld.com/pls/WWWQH2.pls
|
||||||
|
8. WWWQ-HD3 'OG 979' Atlanta - GA MP3 80 http://provisioning.streamtheworld.com/pls/WWWQH3.pls
|
||||||
|
Select Option:
|
||||||
|
|
||||||
|
```
|
||||||
|
### Potential next steps:
|
||||||
|
|
||||||
|
* Searching by tags would be powerful, if it didn't matter how many or which order they were in. So, one approach might be to do some sort of preprocessing on them, after capture. But this requires getting an unfiltered list to begin with. The server code itself is open source on github. So, one could potentially suggest an improvement to the tag searching, via pull request. You can find it here: https://github.com/segler-alex/radiobrowser-api-rust
|
||||||
|
|
||||||
|
* The hard-coded call to `mpg123` is in `radiomenu.py`. This could be pull out to a config file. Then, potentially, you could even use this on windows, if you had a sufficiently similar console binary for executing audio streams.
|
24
radiomenu.py
Normal file
24
radiomenu.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from python_console_menu import AbstractMenu, MenuItem
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class RadioMenu(AbstractMenu):
|
||||||
|
def __init__(self, station_list=None):
|
||||||
|
super().__init__("Radio Player Menu")
|
||||||
|
if station_list is None:
|
||||||
|
station_list = []
|
||||||
|
|
||||||
|
for i in range(len(station_list)):
|
||||||
|
self.add_menu_item(
|
||||||
|
MenuItem(
|
||||||
|
i,
|
||||||
|
station_list[i]["name"] + " " +
|
||||||
|
station_list[i]["codec"] + " " +
|
||||||
|
station_list[i]["bitrate"] + " " +
|
||||||
|
station_list[i]["url"],
|
||||||
|
lambda url=station_list[i]["url"]: subprocess.run(["mpg123", url])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def initialise(self):
|
||||||
|
self.add_menu_item(MenuItem(9999, "Exit menu").set_as_exit_option())
|
103
stations.py
Normal file
103
stations.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import argparse
|
||||||
|
import DNS
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from radiomenu import RadioMenu
|
||||||
|
|
||||||
|
|
||||||
|
def ipaddr(hostname, rectype='A'):
|
||||||
|
return DNS.dnslookup(hostname, rectype, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def nsname(ipaddrs, rectype='A'):
|
||||||
|
return DNS.revlookup(ipaddrs, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def get_host():
|
||||||
|
hosts = ipaddr('all.api.radio-browser.info')
|
||||||
|
preferred_host = secrets.choice(hosts)
|
||||||
|
return nsname(preferred_host)
|
||||||
|
|
||||||
|
|
||||||
|
def get_station_by_name(callsign, host):
|
||||||
|
resp = requests.get(f"https://{host}/json/stations/byname/" + callsign)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return json.loads(resp.text)
|
||||||
|
else:
|
||||||
|
return [{"response_code": resp.status_code, "reason": resp.reason}]
|
||||||
|
|
||||||
|
|
||||||
|
def get_stations(qstring, host):
|
||||||
|
# https://de1.api.radio-browser.info/json/stations/search?name=wfmt&limit=100
|
||||||
|
# https://de1.api.radio-browser.info/json/stations/search?tag=classical&limit=100
|
||||||
|
# https://de1.api.radio-browser.info/json/stations/search?state=Illinois&tag=classical&limit=100
|
||||||
|
# https://de1.api.radio-browser.info/json/stations/search?country=America&state=Illinois&tag=classical&limit=100
|
||||||
|
resp = requests.get(f"https://{host}/json/stations/search?{qstring}&limit=100000")
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return json.loads(resp.text)
|
||||||
|
else:
|
||||||
|
return [{"response_code": resp.status_code, "reason": resp.reason}]
|
||||||
|
|
||||||
|
|
||||||
|
def search_stations(name=None, country=None, state=None, tags=None, status="up"):
|
||||||
|
if tags is None:
|
||||||
|
tags = []
|
||||||
|
query = ""
|
||||||
|
|
||||||
|
if name:
|
||||||
|
if query != "":
|
||||||
|
query = query + "&"
|
||||||
|
query = query + "name=" + name
|
||||||
|
if country:
|
||||||
|
if query != "":
|
||||||
|
query = query + "&"
|
||||||
|
query = query + "country=" + country
|
||||||
|
if state:
|
||||||
|
if query != "":
|
||||||
|
query = query + "&"
|
||||||
|
query = query + "state=" + state
|
||||||
|
if len(tags) > 0:
|
||||||
|
if query != "":
|
||||||
|
query = query + "&"
|
||||||
|
tag_string = ','.join(tags) # Be careful here! the tag list is order-dependent in the http call :(
|
||||||
|
query = query + "tag=" + tag_string
|
||||||
|
|
||||||
|
stations = get_stations(query, get_host())
|
||||||
|
filtered_list = []
|
||||||
|
for station in stations:
|
||||||
|
station_stat = "down"
|
||||||
|
if str(station["lastchecktime"]) == str(station["lastcheckoktime"]):
|
||||||
|
station_stat = "up"
|
||||||
|
|
||||||
|
if len(tags) > 1 and station["tags"] == "": # If searching with tags, but no tags, don't include the station.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if station_stat == status: # only add the entry if it matches the up/down status specified.
|
||||||
|
station_entry = {
|
||||||
|
"name": str(station["name"]).replace('"', "'").replace(",", " - "),
|
||||||
|
"url": str(station["url"].replace(",", "%2C")),
|
||||||
|
"codec": str(station["codec"]),
|
||||||
|
"bitrate": str(station["bitrate"]),
|
||||||
|
"countrycode": str(station["countrycode"]),
|
||||||
|
"favicon": str(station["favicon"]).replace(" ", "%20").replace("(", "%28").replace(")", "%29"),
|
||||||
|
"tags": str(station["tags"].split(",")).replace(",", ";"),
|
||||||
|
"status": str(station_stat)
|
||||||
|
}
|
||||||
|
filtered_list.append(station_entry)
|
||||||
|
return filtered_list
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-n", "--name", type=str, help="Name of station", default=None)
|
||||||
|
parser.add_argument("-c", "--country", type=str, help="Station country", default=None)
|
||||||
|
parser.add_argument("-s", "--state", type=str, help="Station state (if in US)", default=None)
|
||||||
|
parser.add_argument("-t", "--tags", type=str, help="search tag or tags (if more than one, comma-separated", default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
station_list = search_stations(name=args.name, country=args.country, state=args.state, tags=args.tags.split(","))
|
||||||
|
|
||||||
|
mainMenu = RadioMenu(station_list)
|
||||||
|
mainMenu.display()
|
Loading…
Reference in New Issue
Block a user