radio

a command line radio player, using mpd/mpv as a backend
git clone git://git.alex.balgavy.eu/radio.git
Log | Files | Refs | README | LICENSE

commit 18d5d90346c5b0a95e381f53e63af1e6f3a223e2
parent 5135da33849fc5c7d64e4d173ed0dd0b2626b88e
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Tue, 16 Feb 2021 11:39:14 +0100

Re-add config file reading, other improvements

Features are now the same as before refactoring. But the code is better,
and so is the functionality (i.e. interrupt handling and others).

Diffstat:
Mradio | 210++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
1 file changed, 118 insertions(+), 92 deletions(-)

diff --git a/radio b/radio @@ -1,6 +1,9 @@ #!/usr/bin/env ruby # frozen_string_literal: true +DEFAULT_CONFIG_FILE = '/usr/local/etc/radio/urls' +CONFIG_FILE = "#{ENV['HOME']}/.config/radio/urls" + def clear_screen puts "\e[H\e[2J" end @@ -19,21 +22,18 @@ end def choose_from_list(list, names) clear_screen - user_selection = false - begin - until user_selection - user_selection = get_user_choice(names) - puts 'Invalid selection, please try again.' if user_selection && !list[user_selection] - end - - list[user_selection] - rescue Interrupt - nil + puts "== Internet Radio Player ==" + until (user_selection = get_user_choice(names)) + puts 'Invalid selection, please try again.' if user_selection && !list[user_selection] end + + list[user_selection] end # Radio: the base radio class class Radio + attr_reader :channel + def initialize if system('command -v mpc 1>/dev/null 2>&1') @player = 'mpc' @@ -59,6 +59,8 @@ end # SomaFM radio subclass class SomaFM < Radio + HOSTNAME = 'somafm.com' + def initialize(selected_channel) super() @channel = selected_channel[:link] @@ -66,7 +68,7 @@ class SomaFM < Radio def play if @player == 'mpc' - system 'mpc', 'clear' + system 'mpc', 'clear', 1 => '/dev/null' system('mpc', 'load', @channel) end super @channel @@ -82,8 +84,12 @@ class OtherRadio < Radio def play if @player == 'mpc' - system 'mpc', 'clear' - system 'mpc', 'add', @channel + system 'mpc', 'clear', 1 => '/dev/null' + if @channel =~ /\.m3u$/ + system 'mpc', 'load', @channel + else + system 'mpc', 'add', @channel + end end super @channel end @@ -96,46 +102,51 @@ class RadioGarden < Radio require 'cgi' def initialize(_) - retrieved_channels = [] @base_url = 'https://radio.garden/api' - while retrieved_channels.empty? - print 'Enter radio search: ' - query = gets.chomp - begin - URI.parse("#{@base_url}/search?q=#{CGI.escape query}").open do |response| - retrieved_channels = JSON.parse(response.read)['hits']['hits'] - end - rescue OpenURI::HTTPError - retrieved_channels = [] - end - - channels = [] - retrieved_channels.each do |c| - channels << { name: "#{c['_source']['title']} (#{c['_source']['subtitle']})", - id: c['_source']['channelId'] } - end - end - + puts 'No radio found, please try again.' while (retrieved_channels = search_channels).empty? + channels = parse_channels(retrieved_channels) selected_channel = choose_from_list(channels, channels.map { |c| c[:name] }) - - begin - # Will redirect - @channel = URI.parse("#{@base_url}/ara/content/listen/#{selected_channel[:id]}/channel.mp3").open(redirect: false) - rescue OpenURI::HTTPRedirect => e - @channel = e.uri.to_s.gsub(/\?listening-from.*/, '') - rescue OpenURI::HTTPError - @channel = None - end + @channel = get_channel_link(selected_channel) super() + rescue Interrupt + @channel = nil end def play if @player == 'mpc' - system 'mpc', 'clear' + system 'mpc', 'clear', 1 => '/dev/null' system 'mpc', 'add', @channel end super @channel end + + private + + def search_channels + print 'Enter radio search: ' + query = gets.chomp + URI.parse("#{@base_url}/search?q=#{CGI.escape query}").open do |response| + JSON.parse(response.read)['hits']['hits'] + end + rescue OpenURI::HTTPError + [] + end + + def get_channel_link(selected_channel) + # Will redirect + @channel = URI.parse("#{@base_url}/ara/content/listen/#{selected_channel[:id]}/channel.mp3").open(redirect: false) + rescue OpenURI::HTTPRedirect => e + @channel = e.uri.to_s.gsub(/\?listening-from.*/, '') + rescue OpenURI::HTTPError + @channel = None + end + + def parse_channels(retrieved_channels) + retrieved_channels.inject([]) do |channels, c| + channels << { name: "#{c['_source']['title']} (#{c['_source']['subtitle']})", + id: c['_source']['channelId'] } + end + end end # Play music from a subreddit @@ -145,66 +156,81 @@ class Subreddit < Radio require 'shellwords' def initialize(_) - posts = [] - while posts.empty? - print 'Enter subreddit name: ' - sub = gets.chomp - url = "https://www.reddit.com/r/#{sub}/top.json?t=month&limit=100&show=all" - begin - URI.parse(url).open('User-Agent' => 'ruby/2.7', 'Accept' => 'application/json') do |response| - data = JSON.parse(response.read)['data'] - posts = data['children'] - end - rescue OpenURI::HTTPError - posts = [] - end - puts 'Subreddit has no music posts or does not exist.' if posts.empty? + puts 'Subreddit has no music posts or does not exist.' while (posts = retrieve_subreddit_posts).empty? + @links = extract_post_links(posts) + super() + rescue Interrupt + @channel = nil + end + + def play + puts "Number of tracks: #{@links.length}" + # TODO: support mpd + system("mpv --vid=no --volume=50 -- #{@links.map { |l| l[:url] }.shelljoin}") + end + + private + + def retrieve_subreddit_posts + print 'Enter subreddit name: ' + sub = gets.chomp + url = "https://www.reddit.com/r/#{sub}/top.json?t=month&limit=100&show=all" + URI.parse(url).open('User-Agent' => 'ruby/2.7', 'Accept' => 'application/json') do |response| + @channel = sub + JSON.parse(response.read)['data']['children'] end - @links = [] - posts.each do |post| + rescue OpenURI::HTTPError + [] + end + + def extract_post_links(posts) + posts.each_with_object([]) do |post, links| p = post['data'] if !p['is_self'] && p['post_hint'] != 'image' - @links.append(title: p['title'], url: p['url'], reddit: "https://reddit.com#{p['permalink']}") + links.append(title: p['title'], url: p['url'], reddit: "https://reddit.com#{p['permalink']}") end end + end +end - super() +def read_config_file(cfg) + channels = [] + ignore = /^\s*(\#|\s*$)/ + File.open(cfg, 'r').each do |line| + next if line.match?(ignore) + + parts = line.chomp.split(/(?<=")\s+(?=http)/) + channel = { name: parts.first.gsub('"', ''), link: parts.last } + channel[:radio] = (URI.parse(channel[:link]).host == SomaFM::HOSTNAME ? SomaFM : OtherRadio) + channels << channel end - def play - puts "Number of tracks: #{@links.length}" + channels +end - # TODO: support mpd - system("mpv --vid=no --volume=50 -- #{@links.map { |l| l[:url] }.shelljoin}") - rescue Interrupt - true +def load_channels_from_config + if File.exist? CONFIG_FILE + cfg = CONFIG_FILE + elsif File.exist? DEFAULT_CONFIG_FILE + cfg = DEFAULT_CONFIG_FILE + else + warn "Please set URLs in #{ENV['HOME']}/.config/radio/urls." + exit 1 end + read_config_file(cfg) end +channels = load_channels_from_config + [{ name: 'RadioGarden', radio: RadioGarden }, + { name: 'Subreddit', radio: Subreddit }] -# TODO: read some URLs from config file -channels = [ - { name: 'SOMA - Groove Salad (ambient/downtempo)', link: 'https://somafm.com/groovesalad256.pls', radio: SomaFM }, - { name: 'SOMA - Mission Control (ambient, space)', link: 'https://somafm.com/missioncontrol.pls', radio: SomaFM }, - { name: 'SOMA - The Trip (prog house/trance)', link: 'https://somafm.com/thetrip.pls', radio: SomaFM }, - { name: 'SOMA - Beat Blender (deep house, downtempo)', link: 'https://somafm.com/beatblender.pls', radio: SomaFM }, - { name: 'SOMA - Dub Step', link: 'https://somafm.com/dubstep256.pls', radio: SomaFM }, - { name: 'SOMA - Defcon', link: 'https://somafm.com/defcon256.pls', radio: SomaFM }, - { name: 'SOMA - Deep Space (deep ambient electro/experimental)', link: 'https://somafm.com/deepspaceone.pls', - radio: SomaFM }, - { name: 'SOMA - Thistle Radio (Celtic)', link: 'https://somafm.com/thistle.pls', radio: SomaFM }, - { name: 'SOMA - Fluid (instr. hip hop, liquid trap)', link: 'https://somafm.com/fluid.pls, radio: SomaFM }' }, - { name: 'Psystation: Classic Goa', link: 'http://hestia2.cdnstream.com/1458_128', radio: OtherRadio }, - { name: 'Psystation: Prog Psytrance', link: 'http://hestia2.cdnstream.com/1453_128', radio: OtherRadio }, - { name: 'Nightride FM', link: 'https://nightride.fm/stream/nightride.m4a', radio: OtherRadio }, - { name: 'Nightwave Plaza', link: 'https://plaza.one/mp3', radio: OtherRadio }, - { name: '6forty', link: 'http://54.173.171.80:8000/6forty', radio: OtherRadio }, - { name: 'Fnoob Techno', link: 'http://play.fnoobtechno.com:2199/tunein/fnoobtechno320.pls', radio: OtherRadio }, - { name: 'RadioGarden', radio: RadioGarden }, - { name: 'Subreddit', radio: Subreddit } -] - -selected_channel = choose_from_list(channels, channels.map { |c| c[:name] }) -exit 0 if selected_channel.nil? -radio = selected_channel[:radio].new(selected_channel) -radio.play +begin + radio = nil + loop do + selected_channel = choose_from_list(channels, channels.map { |c| c[:name] }) while selected_channel.nil? + radio = selected_channel[:radio].new(selected_channel) + break unless selected_channel.nil? || radio.channel.nil? + end + radio.play +rescue Interrupt + exit 0 +end