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:
| M | radio | | | 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