fcc 1.2.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed6aee05400375a29f67c500080cb21647f11ad018e3d9dc5091bb73e44eeed1
4
- data.tar.gz: 8c116d81d2c374d1e7488564453cfd1fa34305901ce8529d4f99b0ac4bf1d187
3
+ metadata.gz: 9e5293b201813db44d301865ab81106198363704a76624b6f191d55a311fbf42
4
+ data.tar.gz: ef103e7da9885445910a74ce925ee86a802d0cc16e4d4dac1a67c9dcb8951073
5
5
  SHA512:
6
- metadata.gz: ef246a70b27c0496fab4bf78ccf3d7df93c55867edd699cb39a62309055ecb510f5c550f4e0faa7bcef39b57c878d972316a634cc1e2ba1adc1bec77b317d66f
7
- data.tar.gz: a426466aa4a0de582fb198e16e2214690c4a4960bbfec971dc0e3e864dd29c4e45c50ed283c8ca532f6074a64c48d06d526bbee07fbd9309553b165c8601d60f
6
+ metadata.gz: 27737adf40e25217c5a38cb64cdb7d732410621a7d84f7a7e9abf233cae2b1625df7a83020d9e915a4ddf7c179eecf458003af39c320de8d8a0809f1d52069ae
7
+ data.tar.gz: c9c13a2d92bef1dfbd418610f5babb054ece95733920a55e6eed53b3a6bc0c2bbf78a753b16eebceb57ae1ce2ea8de3285640f1b8150cacbf9e0c0bf9f1226ba
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ fcc changelog
2
+
3
+ ## [1.3.0](https://github.com/jkeen/fcc/compare/v1.3.0...v1.2.1) (2022-07-10)
4
+
5
+ - Combine data from multiple sources to get a full picture of the station data (transition api, public api, LMS csv files)
6
+ - Now includes related transmitters, repeaters, low power stations, etc
data/Gemfile CHANGED
@@ -5,19 +5,7 @@ git_source(:github) { |repo_name| 'https://github.com/jkeen/fcc' }
5
5
  # Specify your gem's dependencies in popularity.gemspec
6
6
  gemspec
7
7
 
8
- #
9
- # source "http://rubygems.org"
10
- # # Add dependencies required to use your gem here.
11
- # # Example:
12
- # # gem "activesupport", ">= 2.3.5"
13
- #
14
- # # Add dependencies to develop your gem here.
15
- # # Include everything needed to run rake, tests, features, etc.
16
- #
17
- # gem "nokogiri"
18
- #
19
8
  # group :development do
20
- # gem "shoulda", ">= 0"
21
- # gem "bundler"
22
- # gem "jeweler"
9
+ # gem "awesome_print", require: true
10
+ # gem "byebug", require: true
23
11
  # end
data/Gemfile.lock CHANGED
@@ -1,34 +1,34 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fcc (1.2.1)
4
+ fcc (1.3.0)
5
5
  activesupport (>= 6.1)
6
6
  httparty (~> 0.18)
7
7
  lightly (~> 0.3.3)
8
+ logger
9
+ rubyzip
8
10
 
9
11
  GEM
10
12
  remote: https://rubygems.org/
11
13
  specs:
12
- activesupport (6.1.4.1)
14
+ activesupport (7.0.3)
13
15
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
16
  i18n (>= 1.6, < 2)
15
17
  minitest (>= 5.1)
16
18
  tzinfo (~> 2.0)
17
- zeitwerk (~> 2.3)
18
- awesome_print (1.8.0)
19
- byebug (11.1.3)
20
- concurrent-ruby (1.1.9)
21
- diff-lcs (1.4.4)
19
+ concurrent-ruby (1.1.10)
20
+ diff-lcs (1.5.0)
22
21
  httparty (0.20.0)
23
22
  mime-types (~> 3.0)
24
23
  multi_xml (>= 0.5.2)
25
- i18n (1.8.10)
24
+ i18n (1.10.0)
26
25
  concurrent-ruby (~> 1.0)
27
26
  lightly (0.3.3)
28
- mime-types (3.3.1)
27
+ logger (1.5.0)
28
+ mime-types (3.4.1)
29
29
  mime-types-data (~> 3.2015)
30
- mime-types-data (3.2021.0901)
31
- minitest (5.14.4)
30
+ mime-types-data (3.2022.0105)
31
+ minitest (5.16.2)
32
32
  multi_xml (0.6.0)
33
33
  rake (12.3.3)
34
34
  rspec (3.9.0)
@@ -44,17 +44,15 @@ GEM
44
44
  diff-lcs (>= 1.2.0, < 2.0)
45
45
  rspec-support (~> 3.9.0)
46
46
  rspec-support (3.9.4)
47
+ rubyzip (2.3.2)
47
48
  tzinfo (2.0.4)
48
49
  concurrent-ruby (~> 1.0)
49
- zeitwerk (2.5.1)
50
50
 
51
51
  PLATFORMS
52
52
  ruby
53
53
 
54
54
  DEPENDENCIES
55
- awesome_print
56
55
  bundler (~> 2.1)
57
- byebug
58
56
  fcc!
59
57
  rake (~> 12.3.3)
60
58
  rspec (~> 3.9.0)
data/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Query the FCC database for fm, am, and tv information.
4
4
 
5
+
6
+ This gem uses the new FCC apis and the old FCC apis and combines records to be the most complete version possible. The old (transition.fcc.gov) API provides way more data than the new one does.
7
+
5
8
  ## Install
6
9
 
7
10
  ```
@@ -12,8 +15,6 @@ gem install fcc
12
15
 
13
16
  * Ruby 2.0.0 or higher
14
17
 
15
- ## Examples
16
-
17
18
  #### Get station information by call sign
18
19
 
19
20
  ```ruby
@@ -41,9 +42,49 @@ if station.exists? && station.licensed?
41
42
  station.latitude #=> "30.266861111111112"
42
43
  station.longitude #=> "-97.67444444444445"
43
44
  station.file_number #=> BLED-19950103KA
45
+
46
+
47
+ station.to_json #=>
48
+ [{:band=>"FM",
49
+ :signal_strength=>"3.0 kW",
50
+ :latitude=>"30.266861111111112",
51
+ :longitude=>"-97.67444444444445",
52
+ :station_class=>"A",
53
+ :file_number=>"BLED-19950103KA",
54
+ :effective_radiated_power=>"3.0 kW",
55
+ :haat_horizontal=>"26.0",
56
+ :haat_vertical=>"26.0",
57
+ :antenna_type=>"ND",
58
+ :operating_hours=>nil,
59
+ :licensed_to=>"TEXAS EDUCATIONAL BROADCASTING CO-OPERATIVE, INC.",
60
+ :city=>"HORNSBY",
61
+ :state=>"TX",
62
+ :country=>"US",
63
+ :id=>"65320",
64
+ :call_sign=>"KOOP",
65
+ :status=>"LICENSED",
66
+ :rf_channel=>"219",
67
+ :license_expiration_date=>"08/01/2029",
68
+ :facility_type=>"ED",
69
+ :frequency=>"91.7",
70
+ :contact=>
71
+ {:name=>"Federico Pacheco",
72
+ :title=>"",
73
+ :address=>"3823 Airport Blvd. Suite B",
74
+ :address2=>"",
75
+ :city=>"Austin",
76
+ :state=>"TX",
77
+ :zip_code=>"78722",
78
+ :phone=>"(512)472-1369",
79
+ :fax=>"",
80
+ :email=>"federico.pacheco@koop.org",
81
+ :website=>"koop.org"},
82
+ :owner=>{:name=>"TEXAS EDUCATIONAL BROADCASTING CO-OPERATIVE, INC.", :city=>"AUSTIN", :state=>"TX", :phone=>"(512)472-1369"},
83
+ :community=>{:city=>"HORNSBY", :state=>"TX"}}]
44
84
  ```
45
85
  end
46
86
 
87
+
47
88
  ### Caching
48
89
  Extended attributes take several seconds to load from transition.fcc.gov. In order to work around this, we query the entire dataset and then cache the result locally for 3 days (using the lightly gem). To use your own cache, set `FCC.cache=` to your cache class (Rails.cache, maybe?) which should have a #fetch method that should take a key and a block like `cache.fetch(key) { // yield for expensive fetch }}`.
49
90
 
data/fcc.gemspec CHANGED
@@ -20,9 +20,9 @@ Gem::Specification.new do |spec|
20
20
  spec.add_dependency "activesupport", ">= 6.1"
21
21
  spec.add_dependency "httparty", "~> 0.18"
22
22
  spec.add_dependency "lightly", "~> 0.3.3"
23
+ spec.add_dependency "rubyzip"
24
+ spec.add_dependency "logger"
23
25
  spec.add_development_dependency "bundler", "~> 2.1"
24
26
  spec.add_development_dependency "rake", "~> 12.3.3"
25
27
  spec.add_development_dependency 'rspec', '~> 3.9.0'
26
- spec.add_development_dependency "byebug", ">= 0"
27
- spec.add_development_dependency "awesome_print", ">= 0"
28
28
  end
@@ -1,6 +1,7 @@
1
1
  require 'httparty'
2
- require_relative './extended_info/parser'
2
+ require_relative './parsers/extended_info'
3
3
  require 'lightly'
4
+ require 'logger'
4
5
 
5
6
  module FCC
6
7
  module Station
@@ -8,13 +9,25 @@ module FCC
8
9
  attr_reader :store
9
10
 
10
11
  def initialize
11
- @lightly = Lightly.new dir: "tmp/fcc_#{@service}_data", life: '3d', hash: true
12
+ @lightly = Lightly.new(life: '3d', hash: true).tap do |cache|
13
+ cache.prune
14
+ end
15
+ end
16
+
17
+ def flush
18
+ @lightly.flush
12
19
  end
13
20
 
14
- def fetch key
15
- @lightly.get key.to_s do
16
- puts "loading up cache with all results"
17
- yield
21
+ def fetch(key)
22
+ FCC.log("Retreiving #{key} from cache")
23
+ @lightly.get(key.to_s) do
24
+ FCC.log "Loading up the cache with results for key: #{key}. This might take a minute…"
25
+ value = yield
26
+ if value && value.present?
27
+ value
28
+ else
29
+ nil
30
+ end
18
31
  end
19
32
  end
20
33
  end
@@ -1,5 +1,5 @@
1
1
  require 'httparty'
2
- require_relative './extended_info/parser'
2
+ require_relative './parsers/extended_info'
3
3
 
4
4
  module FCC
5
5
  module Station
@@ -7,19 +7,20 @@ module FCC
7
7
  include HTTParty
8
8
  attr_accessor :results, :service
9
9
 
10
- base_uri 'https://transition.fcc.gov/fcc-bin/'
10
+ base_uri 'https://fcc-cache.b-cdn.net/fcc-bin/'
11
11
 
12
12
  def initialize(service)
13
13
  @service = service
14
14
  @options = {
15
15
  follow_redirects: true
16
16
  }
17
+
17
18
  @query = {
18
19
  # state: nil,
19
20
  # call: nil,
20
21
  # city: nil,
21
22
  # arn: nil,
22
- serv: service.to_s.downcase, # Only return primary main records, no backup transmitters, etc… for now
23
+ # serv: service.to_s.downcase, # Only return primary main records, no backup transmitters, etc… for now
23
24
  status: 3, # licensed records only
24
25
  # freq: @service.to_sym == :fm ? '87.1' : '530',
25
26
  # fre2: @service.to_sym == :fm ? '107.9' : '1700',
@@ -42,36 +43,38 @@ module FCC
42
43
  end
43
44
 
44
45
  def all_results
46
+
45
47
  begin
46
48
  cache_key = "#{self.class.instance_variable_get('@default_options')[:base_uri]}/#{@service.to_s.downcase}q"
47
49
  FCC.cache.fetch cache_key do
48
50
  response = self.class.get("/#{service.to_s.downcase}q", @options.merge(query: @query))
49
- puts response.request.uri.to_s.gsub('&list=4', '&list=0')
51
+ FCC.log(response.request.uri.to_s.gsub('&list=4', '&list=0'))
50
52
  response.parsed_response
51
53
  end
52
54
  rescue StandardError => e
53
- puts e.message
54
- puts e.backtrace
55
+ FCC.error(e.message)
56
+ FCC.error(e.backtrace)
55
57
  end
56
58
  end
57
59
 
58
60
  def find(id_or_call_sign)
59
61
  if id_or_call_sign =~ /^\d+$/
60
62
  id = id_or_call_sign
63
+ all_results.filter { |r| r[:fcc_id].to_s == id.to_s } || find_directly({ facid: id_or_call_sign })
61
64
  else
62
- id = FCC::Station.index(@service).call_sign_to_id(id_or_call_sign)
63
- end
64
-
65
- begin
66
- all_results.filter { |r| r[:fcc_id].to_s == id.to_s }
67
- rescue StandardError => e
68
- response = self.class.get("/#{service.to_s.downcase}q", @options.merge(query: @query.merge(facid: id)))
69
- puts response.request.uri.to_s.gsub('&list=4', '&list=0')
70
- response&.parsed_response
65
+ all_results.filter { |r|
66
+ r[:call_sign].to_s == id_or_call_sign.to_s || r[:call_sign].to_s.upcase =~ Regexp.new("^#{id_or_call_sign.upcase}[-—–][A-Z0-9]+$")
67
+ } || find_directly({ call: id_or_call_sign })
71
68
  end
72
69
  end
73
70
 
74
- parser FCC::Station::ExtendedInfoParser
71
+ def find_directly(options)
72
+ response = self.class.get("/#{service.to_s.downcase}q", @options.merge(query: @query.merge(options)))
73
+ FCC.log response.request.uri.to_s.gsub('&list=4', '&list=0')
74
+ response.parsed_response
75
+ end
76
+
77
+ parser FCC::Station::Parsers::ExtendedInfo
75
78
  end
76
79
  end
77
80
  end
@@ -22,7 +22,10 @@ module FCC
22
22
  response = self.class.get("/api/service/#{service.to_s.downcase}/facility/id/#{id}.json")
23
23
 
24
24
  begin
25
- response['results']['facility']
25
+ body = response['results']['facility']
26
+ body['band'] = service.to_s.upcase.to_s
27
+
28
+ body
26
29
  rescue StandardError => e
27
30
  return nil
28
31
  end
@@ -0,0 +1,139 @@
1
+ require 'httparty'
2
+ require 'open-uri'
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'date'
6
+ require_relative './parsers/lms_data'
7
+
8
+ module FCC
9
+ module Station
10
+ class LmsData
11
+ BASE_URI = 'https://enterpriseefiling.fcc.gov/dataentry/api/download/dbfile'
12
+ # include HTTParty
13
+
14
+ def find_all_related(call_sign: )
15
+ stations = find_related_stations(call_sign: call_sign)
16
+ translators = find_translators_for(call_sign: stations.keys)
17
+ stations.merge(translators)
18
+ end
19
+
20
+ def find_translators_for(call_sign: )
21
+ call_signs = [call_sign].flatten
22
+
23
+ records = common_station_data.entries.select do |entry|
24
+ call_signs.any? { |call_sign| call_signs_match?(call_sign, entry['callsign']) }
25
+ end
26
+
27
+ facility_ids = records.map { |r| r['facility_id'] }.uniq.compact
28
+
29
+ matched_facilities = facilities.entries.select do |facility|
30
+ facility_ids.include?(facility['primary_station']) #{}|| facility_ids.include?(facility['facility_id'])
31
+ end
32
+
33
+ {}.tap do |hash|
34
+ matched_facilities.each do |record|
35
+ hash[record['callsign']] = record['facility_id']
36
+ end
37
+ end
38
+ end
39
+
40
+ def find_related_stations(call_sign: )
41
+ call_signs = [call_sign].flatten
42
+
43
+ records = common_station_data.entries.select do |entry|
44
+ call_signs.any? { call_signs_match?(call_sign, entry['callsign']) }
45
+ end
46
+
47
+ correlated_app_ids = records.map { |m| m['eeo_application_id'] }
48
+ matches = common_station_data.entries.select do |entry|
49
+ correlated_app_ids.include?(entry['eeo_application_id'])
50
+ end
51
+
52
+ {}.tap do |hash|
53
+ matches.each do |record|
54
+ hash[record['callsign']] = record['facility_id']
55
+ end
56
+ end
57
+ end
58
+
59
+ def facilities
60
+ @facilities ||= read(:facility)
61
+ end
62
+
63
+ def common_station_data
64
+ @common_station_data ||= read(:common_station)
65
+ end
66
+
67
+ def find_facilities(facility_ids:, call_signs: [])
68
+ matched_facilities = facilities.entries.select do |facility|
69
+ facility_ids.include?(facility['primary_station']) || facility_ids.include?(facility['facility_id']) || call_signs.include?(facility['callsign'])
70
+ end
71
+
72
+ {}.tap do |hash|
73
+ matched_facilities.each do |record|
74
+ hash[record['callsign']] = record['facility_id']
75
+ end
76
+ end
77
+ end
78
+
79
+ def find_call_signs(call_signs:)
80
+ common_station_data.entries.select do |entry|
81
+ call_signs.any? do |call_sign|
82
+ call_signs_match?(call_sign, entry['callsign'])
83
+ end
84
+ end
85
+ end
86
+
87
+ def read(file_name)
88
+ key = "#{lms_date}-#{file_name}"
89
+ remote_url = URI("#{BASE_URI}/#{lms_date}/#{file_name}.zip")
90
+ FCC.log remote_url
91
+ contents = FCC.cache.fetch key do
92
+ begin
93
+ temp_file = http_download_uri(remote_url)
94
+ break if temp_file.empty?
95
+
96
+ contents = ""
97
+ Zip::File.open_buffer(temp_file) do |zf|
98
+ contents = zf.read(zf.entries.first)
99
+ break
100
+ end
101
+
102
+ value = contents
103
+ rescue Exception => e
104
+ FCC.error(e.message)
105
+ value = nil
106
+ ensure
107
+ value
108
+ end
109
+ end
110
+
111
+ if contents
112
+ CSV.parse(contents, col_sep: '|', headers: true)
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ def call_signs_match?(ours, theirs)
119
+ theirs.to_s.upcase.to_s == ours.to_s.upcase.to_s || theirs.to_s.upcase =~ Regexp.new("^#{ours.to_s.upcase}[-—–][A-Z0-9]+$")
120
+ end
121
+
122
+ def http_download_uri(uri)
123
+ FCC.log 'Downloading ' + uri.to_s
124
+ begin
125
+ Tempfile.create { HTTParty.get(uri)&.body }
126
+ rescue Exception => e
127
+ FCC.error "=> Exception: '#{e}'. Skipping download."
128
+
129
+ raise e
130
+ return false
131
+ end
132
+ end
133
+
134
+ def lms_date
135
+ Date.today.strftime('%m-%d-%Y')
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,78 @@
1
+ require 'httparty'
2
+
3
+ module FCC
4
+ module Station
5
+ module Parsers
6
+ class ExtendedInfo < HTTParty::Parser
7
+ def parse
8
+ results = []
9
+ body.each_line do |row|
10
+ attrs = {}
11
+ attrs[:raw] = row
12
+ fields = row.split('|').slice(1...-1).collect(&:strip).map { |v| v == '-' ? "" : v }
13
+
14
+ attrs[:call_sign] = fields[0]
15
+ attrs[:frequency] = parse_frequency(fields[1])
16
+ attrs[:band] = fields[2]
17
+ attrs[:channel] = fields[3]
18
+ attrs[:antenna_type] = fields[4] # Directional Antenna (DA) or NonDirectional (ND)
19
+ attrs[:operating_hours] = fields[5] if fields[5] && attrs[:band]&.upcase == "AM" # (Only used for AM)
20
+ attrs[:station_class] = fields[6]
21
+ attrs[:region_2_station_class] = fields[7] if fields[7] && attrs[:band]&.upcase == "AM" # (only used for AM)
22
+ attrs[:status] = fields[8]
23
+ attrs[:city] = fields[9]
24
+ attrs[:state] = fields[10]
25
+ attrs[:country] = fields[11]
26
+ attrs[:file_number] = fields[12] #File Number (Application, Construction Permit or License) or
27
+ attrs[:signal_strength] = parse_signal_strength(fields[13]) # Effective Radiated Power --
28
+ attrs[:effective_radiated_power] = parse_signal_strength(fields[14]) # Effective Radiated Power -- vertically polarized (maximum)
29
+ attrs[:haat_horizontal] = fields[15] # Antenna Height Above Average Terrain (HAAT) -- horizontal polarization
30
+ attrs[:haat_vertical] = fields[16] # Antenna Height Above Average Terrain (HAAT) -- vertical polarization
31
+ attrs[:fcc_id] = fields[17] # Facility ID Number (unique to each station)
32
+ attrs[:latitude] = parse_latitude(fields[18], fields[19], fields[20], fields[21])
33
+ attrs[:longitude] = parse_longitude(fields[22], fields[23], fields[24], fields[25])
34
+ attrs[:licensed_to] = fields[26] # Licensee or Permittee
35
+
36
+ results << attrs
37
+ end
38
+
39
+ results
40
+ end
41
+
42
+ def parse_longitude(direction, degrees, minutes, seconds)
43
+ decimal_degrees = degrees.to_i + (minutes.to_f / 60) + (seconds.to_f / 3600)
44
+
45
+ "#{direction =~ /W/ ? '-' : ''}#{decimal_degrees}"
46
+ end
47
+
48
+ def parse_latitude(direction, degrees, minutes, seconds)
49
+ decimal_degrees = degrees.to_i + (minutes.to_f / 60) + (seconds.to_f / 3600)
50
+
51
+ "#{direction =~ /S/ ? '-' : ''}#{decimal_degrees}"
52
+ end
53
+
54
+ def parse_signal_strength(power_string)
55
+ return unless power_string.present?
56
+
57
+ number, unit = power_string.strip.scan(/^([0-9.]+)\s+(\w+)$?/).flatten
58
+ multiplier = case unit&.downcase
59
+ when "w"
60
+ 1
61
+ when "kw"
62
+ 1000
63
+ when "mw"
64
+ 1000000
65
+ else
66
+ 1
67
+ end
68
+
69
+ number.to_f * multiplier
70
+ end
71
+
72
+ def parse_frequency(freq)
73
+ freq.to_f
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,13 @@
1
+ require 'httparty'
2
+
3
+ module FCC
4
+ module Station
5
+ module Parsers
6
+ class LmsData < HTTParty::Parser
7
+ def parse
8
+ body
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,121 @@
1
+ require 'active_support/inflector'
2
+
3
+ module FCC
4
+ module Station
5
+ class RecordDelegate
6
+ def initialize(result)
7
+ @result = result
8
+ end
9
+
10
+ def method_missing(m, *args, &block)
11
+ return find_result(@result, m) unless @result.is_a?(Array)
12
+ return find_result(@result.first, m) if @result.size == 1
13
+
14
+ filtered_results = @result.filter { |result|
15
+ result[:status] == 'LIC' # Licensed only, no construction permits
16
+ }
17
+
18
+ filtered_results = filtered_results.collect { |res|
19
+ find_result(res, m)
20
+ }.uniq
21
+
22
+ filtered_results.size == 1 ? filtered_results.first : filtered_results
23
+ end
24
+
25
+ def to_json
26
+ return {}.tap do |record|
27
+ [Station::Result::EXTENDED_ATTRIBUTES | Station::Result::BASIC_ATTRIBUTES].flatten.each do |attr|
28
+ record[attr.to_sym] = send(attr.to_sym)
29
+ end
30
+
31
+ %i[contact owner community].each do |attr|
32
+ result = send(attr.to_sym)
33
+ next unless result
34
+
35
+ record[attr] ||= if result.is_a?(Struct)
36
+ result.to_h.compact
37
+ elsif result.is_a?(Array) && result.compact.size > 0
38
+ result
39
+ elsif result.present?
40
+ result.to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def has_data?
47
+ @result.present?
48
+ end
49
+
50
+ def id
51
+ super || send(:fcc_id)
52
+ end
53
+
54
+ def frequency
55
+ super&.to_f
56
+ end
57
+
58
+ def rf_channel
59
+ super || send(:channel)
60
+ end
61
+
62
+ def operating_hours
63
+ super&.downcase
64
+ end
65
+
66
+ def owner
67
+ @owner ||= begin
68
+ contact = Contact.new(
69
+ name: party_name || licensed_to,
70
+ address: party_address_1,
71
+ address2: party_address_2,
72
+ city: (party_city || city),
73
+ state: (party_state || state),
74
+ zip_code: party_zip_1,
75
+ country: country,
76
+ phone: party_phone
77
+ )
78
+
79
+ contact if contact.to_h.compact.any?
80
+ end
81
+ end
82
+
83
+ def community
84
+ @community ||= begin
85
+ community = Community.new(city: community_city || city, state: community_state || state, country: country)
86
+ community if community.to_h.compact.any?
87
+ end
88
+ end
89
+
90
+ def contact
91
+ contact = main_studio_contact
92
+
93
+ return unless contact
94
+ @contact ||= begin
95
+ contact = Contact.new(name: contact['contactName'], title: contact['contactTitle'], address: contact['contactAddress1'], address2: contact['contactAddress2'], city: contact['contactCity'], state: contact['contactState'], zip_code: contact['contactZip'], phone: contact['contactPhone'], fax: contact['contactFax'], email: contact['contactEmail'], website: contact['contactWebsite'])
96
+ contact if contact.to_h.compact.any?
97
+ end
98
+ end
99
+
100
+ def inspect
101
+ "<RecordDelegate:[#{band}] #{frequency} #{call_sign} — #{community.city} #{community.state} />"
102
+ end
103
+
104
+ private
105
+
106
+ def find_key(result, name)
107
+ result&.keys&.detect { |d| name.to_s == d.to_s } || result&.keys&.detect { |d| name.to_s == d.to_s.underscore }
108
+ end
109
+
110
+ def find_result(result, name)
111
+ matched_key = find_key(result, name)
112
+
113
+ if matched_key
114
+ result[matched_key]
115
+ else
116
+ nil
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/inflector'
2
+
3
+ module FCC
4
+ module Station
5
+ class RecordGroup
6
+ def initialize(results = [])
7
+ @results = results.map do |result|
8
+ result.is_a?(RecordDelegate) ? result : RecordDelegate.new(result)
9
+ end
10
+ end
11
+
12
+ def to_json
13
+ return {}.tap do |record|
14
+ [Station::Result::EXTENDED_ATTRIBUTES | Station::Result::BASIC_ATTRIBUTES].flatten.each do |attr|
15
+ record[attr.to_sym] = result_attribute(attr.to_sym)
16
+ end
17
+
18
+ %i[contact owner community].each do |attr|
19
+ result = result_attribute(attr.to_sym)
20
+ next unless result
21
+
22
+ record[attr] ||= if result.is_a?(Struct)
23
+ result.to_h.compact
24
+ elsif result.is_a?(Array) && result.compact.size > 0
25
+ result
26
+ elsif result.present?
27
+ result.to_s
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def result_attribute(attr)
34
+ @results.collect { |r| r.send(attr.to_sym) }.compact.first
35
+ end
36
+
37
+ def method_missing(m, *args, &block)
38
+ result = result_attribute(m.to_sym)
39
+
40
+ if result.is_a?(Array) && result.size == 1
41
+ result = result.first
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,178 @@
1
+ require 'zip'
2
+
3
+ module FCC
4
+ module Station
5
+ class Result
6
+ EXTENDED_ATTRIBUTES = %i[band signal_strength latitude longitude station_class file_number effective_radiated_power haat_horizontal haat_vertical antenna_type operating_hours licensed_to city state country] # these take a long time to query
7
+ BASIC_ATTRIBUTES = %i[id call_sign status rf_channel license_expiration_date facility_type frequency band]
8
+
9
+ def initialize(service, call_sign, options = {})
10
+ @call_sign = call_sign.upcase
11
+ @service = service
12
+ @options = options
13
+
14
+ data
15
+
16
+ self
17
+ end
18
+
19
+ def details_available?
20
+ exists? && data.latitude.present?
21
+ end
22
+
23
+ def licensed?
24
+ exists? && data.status == 'LICENSED' && data.license_expiration_date && Time.parse(data.license_expiration_date) > Time.now
25
+ end
26
+
27
+ def exists?
28
+ grouped_records.any?
29
+ end
30
+
31
+ def to_json(*_args)
32
+ [].tap do |records|
33
+ grouped_records.each do |rg|
34
+ records << rg.to_json
35
+ end
36
+ end.flatten.compact.uniq
37
+ end
38
+
39
+ def coordinates_url
40
+ "https://www.google.com/maps/search/#{latitude},#{longitude}" if latitude.present? && longitude.present?
41
+ end
42
+
43
+ def extended_data_url
44
+ "https://transition.fcc.gov/fcc-bin/#{@service.to_s.downcase}q?list=4&facid=#{id}"
45
+ end
46
+
47
+ def enterprise_data_url
48
+ "https://enterpriseefiling.fcc.gov/dataentry/public/tv/publicFacilityDetails.html?facilityId=#{id}"
49
+ end
50
+
51
+ def data
52
+ @data ||= RecordDelegate.new(Info.new(@service).find(@call_sign))
53
+ end
54
+ alias public_data data
55
+
56
+ def grouped_records
57
+ grouped = all_records.group_by do |record|
58
+ [record.id, record.call_sign, record.band, record.frequency].compact.join('/')
59
+ end
60
+
61
+ [].tap do |res|
62
+ grouped.each do |_key, values|
63
+ res << RecordGroup.new(values)
64
+ end
65
+ end
66
+ end
67
+ alias records grouped_records
68
+
69
+ def all_records
70
+ [public_records, transition_records, related_translators].flatten.compact.filter { |f| f.has_data? }
71
+ end
72
+
73
+ def related_translators
74
+ @related_translators ||= begin
75
+ records = lms_data.find_translators_for(call_sign: @call_sign)
76
+ records.keys.map do |call|
77
+ RecordDelegate.new(ExtendedInfo.new(@service).find(call))
78
+ end.select { |f| f.status.upcase == "LIC" }
79
+ end
80
+ end
81
+
82
+ def related_stations
83
+ @related_stations ||= begin
84
+ records = lms_data.find_related_stations(call_sign: @call_sign)
85
+ records.keys.map do |call|
86
+ RecordDelegate.new(ExtendedInfo.new(@service).find(call))
87
+ end.select { |f| f.status.upcase == "LIC" }
88
+ end
89
+ end
90
+
91
+ def related
92
+ @related ||= begin
93
+ records = lms_data.find_all_related(call_sign: @call_sign)
94
+ records.keys.map do |call|
95
+ ExtendedInfo.new(@service).find(call).collect do |info|
96
+ RecordDelegate.new(info)
97
+ end
98
+ end.flatten.select { |f| f.status.upcase == "LIC" }
99
+ end
100
+ end
101
+
102
+ def print_broadcast_summary
103
+ FCC.log "[primary]"
104
+ transition_records.each do |record|
105
+ FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
106
+ end
107
+
108
+ FCC.log "[translators]"
109
+ related_translators.each do |record|
110
+ FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
111
+ end
112
+
113
+ FCC.log "[related stations]"
114
+ related_stations.each do |record|
115
+ FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
116
+ end
117
+
118
+ FCC.log "[all related]"
119
+ related.each do |record|
120
+ FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
121
+ end
122
+
123
+ nil
124
+ end
125
+
126
+ def lms_data
127
+ @lms_data ||= LmsData.new
128
+ end
129
+
130
+ def call_signs_match?(ours, theirs)
131
+ theirs.to_s.upcase.to_s == ours.to_s.upcase.to_s || theirs.to_s.upcase =~ Regexp.new("^#{ours.to_s.upcase}[-—–][A-Z0-9]+$")
132
+ end
133
+
134
+ private
135
+
136
+ def public_records
137
+ public_data_info.map { |r| RecordDelegate.new(r) }
138
+ end
139
+
140
+ def transition_records
141
+ transition_data_info.map { |r| RecordDelegate.new(r) }
142
+ end
143
+
144
+ def related_records
145
+ results = related.keys.collect do |call_sign|
146
+ RecordDelegate.new(ExtendedInfo.new(@service).find(call_sign))
147
+ end
148
+ end
149
+
150
+ def public_data_info
151
+ @public_data_info ||= [Info.new(@service).find(@call_sign)]
152
+ end
153
+
154
+ def transition_data_info
155
+ @transition_data_info ||= ExtendedInfo.new(@service).find(@call_sign)
156
+ end
157
+
158
+ def method_missing(m, *_args)
159
+ service = if @service == :fm
160
+ fm_record = grouped_records.find { |gr| FCC::FM_FULL_SERVICE == gr.band.upcase }
161
+ fm_low_power = grouped_records.find { |gr| FCC::FM_LOW_POWER == gr.band.upcase }
162
+ fm_booster = grouped_records.find { |gr| FCC::FM_BOOSTER == gr.band.upcase }
163
+ fm_translator = grouped_records.find { |gr| FCC::FM_TRANSLATOR == gr.band.upcase }
164
+
165
+ [fm_record, fm_low_power, fm_booster, fm_translator].compact.find { |r| r.send(m.to_sym) }
166
+ else
167
+ grouped_records.find { |r| r.send(m.to_sym) }
168
+ end
169
+
170
+ result = service.send(m.to_sym) if service
171
+
172
+ result = result.first if result.is_a?(Array) && result.size == 1
173
+
174
+ result
175
+ end
176
+ end
177
+ end
178
+ end
data/lib/fcc/station.rb CHANGED
@@ -1,13 +1,17 @@
1
1
  require 'active_support/core_ext/module/delegation'
2
- require_relative 'station/result_delegate'
3
2
  require_relative 'station/extended_info'
3
+ require_relative 'station/cache'
4
4
  require_relative 'station/index'
5
5
  require_relative 'station/info'
6
+ require_relative 'station/result'
7
+ require_relative 'station/lms_data'
8
+ require_relative 'station/record_group'
9
+ require_relative 'station/record_delegate'
6
10
 
7
11
  module FCC
8
12
  module Station
9
- Contact = Struct.new(:name, :title, :address, :address2, :city, :state, :zip_code, :phone, :fax, :email, :website, keyword_init: true)
10
- Community = Struct.new(:city, :state, keyword_init: true)
13
+ Contact = Struct.new(:name, :title, :address, :address2, :city, :state, :country, :zip_code, :phone, :fax, :email, :website, keyword_init: true)
14
+ Community = Struct.new(:city, :state, :country, keyword_init: true)
11
15
 
12
16
  def self.find_each(service, &block)
13
17
  results = index(service).results
@@ -18,9 +22,7 @@ module FCC
18
22
  end
19
23
 
20
24
  def self.find(service, call_sign, options = {})
21
- result = Result.new(service, call_sign, options)
22
-
23
- result if result&.exists?
25
+ Result.new(service, call_sign, options)
24
26
  end
25
27
 
26
28
  def self.index(service)
@@ -39,89 +41,5 @@ module FCC
39
41
  def self.extended_info_cache
40
42
  @cache ||= Station::Cache.new
41
43
  end
42
-
43
- class Result
44
- EXTENDED_ATTRIBUTES = %i[signal_strength latitude longitude station_class file_number effective_radiated_power haat_horizontal haat_vertical antenna_type] # these take a long time to query
45
- BASIC_ATTRIBUTES = %i[id call_sign status rf_channel license_expiration_date facility_type frequency]
46
-
47
- delegate *EXTENDED_ATTRIBUTES, to: :extended_data
48
- delegate *BASIC_ATTRIBUTES, to: :data
49
-
50
- alias_method :channel, :rf_channel
51
-
52
- def initialize(service, call_sign, options = {})
53
- @call_sign = call_sign.upcase
54
- @service = service
55
- @options = options
56
-
57
- data
58
- end
59
-
60
- def details_available?
61
- exists? && data.latitude.present?
62
- end
63
-
64
- def licensed?
65
- exists? && data.status == 'LICENSED' && data.license_expiration_date && Time.parse(data.license_expiration_date) > Time.now
66
- end
67
-
68
- def exists?
69
- data.instance_variable_get('@result')
70
- end
71
-
72
- def to_json
73
- {}.tap do |hash|
74
- [EXTENDED_ATTRIBUTES | BASIC_ATTRIBUTES | %i[contact owner community]].flatten.each do |attr|
75
- result = send(attr.to_sym)
76
- next unless result
77
-
78
- hash[attr] = if result.is_a?(Struct)
79
- result.to_h
80
- elsif result.is_a?(Array) && result.compact.size > 0
81
- result
82
- elsif result.present?
83
- result.to_s
84
- end
85
- end
86
- end
87
- end
88
-
89
- def owner
90
- @owner ||= Contact.new(name: data.partyName, address: data.partyAddress1, address2: data.partyAddress2, city: data.partyCity, state: data.partyState, zip_code: data.partyZip1, phone: data.partyPhone)
91
- end
92
-
93
- def community
94
- @community ||= Community.new(city: data.communityCity, state: data.communityState)
95
- end
96
-
97
- def operating_hours
98
- extended_data.am_operating_time if @service == :am
99
- end
100
-
101
- def contact
102
- contact = data.mainStudioContact
103
- @contact ||= Contact.new(name: contact['contactName'], title: contact['contactTitle'], address: contact['contactAddress1'], address2: contact['contactAddress2'], city: contact['contactCity'], state: contact['contactState'], zip_code: contact['contactZip'], phone: contact['contactPhone'], fax: contact['contactFax'], email: contact['contactEmail'], website: contact['contactWebsite'])
104
- end
105
-
106
- def coordinates_url
107
- "https://www.google.com/maps/search/#{latitude},#{longitude}" if latitude.present? && longitude.present?
108
- end
109
-
110
- def extended_data_url
111
- "https://transition.fcc.gov/fcc-bin/#{@service.to_s.downcase}q?list=4&facid=#{id}"
112
- end
113
-
114
- def enterprise_data_url
115
- "https://enterpriseefiling.fcc.gov/dataentry/public/tv/publicFacilityDetails.html?facilityId=#{id}"
116
- end
117
-
118
- def extended_data
119
- @extended_data ||= ResultDelegate.new(ExtendedInfo.new(@service).find(@call_sign))
120
- end
121
-
122
- def data
123
- @data ||= ResultDelegate.new(Info.new(@service).find(@call_sign))
124
- end
125
- end
126
44
  end
127
45
  end
data/lib/fcc.rb CHANGED
@@ -3,9 +3,14 @@ require_relative './fcc/station'
3
3
  require_relative './fcc/station/cache'
4
4
  require_relative './fcc/station/info'
5
5
  require_relative './fcc/station/extended_info'
6
- require_relative './fcc/station/result_delegate'
6
+ require_relative './fcc/station/record_delegate'
7
7
 
8
8
  module FCC
9
+ FM_FULL_SERVICE = 'FM'
10
+ FM_LOW_POWER = 'FL'
11
+ FM_BOOSTER = 'FB'
12
+ FM_TRANSLATOR = 'FX'
13
+
9
14
  def self.cache
10
15
  @cache ||= Station::Cache.new
11
16
  end
@@ -13,4 +18,14 @@ module FCC
13
18
  def self.cache=(cache_service)
14
19
  @cache = cache_service
15
20
  end
21
+
22
+ def self.log(message)
23
+ @logger ||= Logger.new($stdout)
24
+ @logger.info(message)
25
+ end
26
+
27
+ def self.error(message)
28
+ @error_logger ||= Logger.new($stderr)
29
+ @error_logger.error(message)
30
+ end
16
31
  end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FCC
4
- VERSION = '1.2.1'
4
+ VERSION = '1.4.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fcc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Keen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-25 00:00:00.000000000 Z
11
+ date: 2022-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -53,75 +53,75 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.3.3
55
55
  - !ruby/object:Gem::Dependency
56
- name: bundler
56
+ name: rubyzip
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '2.1'
62
- type: :development
61
+ version: '0'
62
+ type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '2.1'
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rake
70
+ name: logger
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: 12.3.3
76
- type: :development
75
+ version: '0'
76
+ type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "~>"
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: 12.3.3
82
+ version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec
84
+ name: bundler
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 3.9.0
89
+ version: '2.1'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 3.9.0
96
+ version: '2.1'
97
97
  - !ruby/object:Gem::Dependency
98
- name: byebug
98
+ name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: 12.3.3
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: 12.3.3
111
111
  - !ruby/object:Gem::Dependency
112
- name: awesome_print
112
+ name: rspec
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ">="
115
+ - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
117
+ version: 3.9.0
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ">="
122
+ - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0'
124
+ version: 3.9.0
125
125
  description: ''
126
126
  email: jeff@keen.me
127
127
  executables:
@@ -132,6 +132,7 @@ extra_rdoc_files: []
132
132
  files:
133
133
  - ".document"
134
134
  - ".gitignore"
135
+ - CHANGELOG.md
135
136
  - Gemfile
136
137
  - Gemfile.lock
137
138
  - LICENSE.txt
@@ -145,10 +146,14 @@ files:
145
146
  - lib/fcc/station.rb
146
147
  - lib/fcc/station/cache.rb
147
148
  - lib/fcc/station/extended_info.rb
148
- - lib/fcc/station/extended_info/parser.rb
149
149
  - lib/fcc/station/index.rb
150
150
  - lib/fcc/station/info.rb
151
- - lib/fcc/station/result_delegate.rb
151
+ - lib/fcc/station/lms_data.rb
152
+ - lib/fcc/station/parsers/extended_info.rb
153
+ - lib/fcc/station/parsers/lms_data.rb
154
+ - lib/fcc/station/record_delegate.rb
155
+ - lib/fcc/station/record_group.rb
156
+ - lib/fcc/station/result.rb
152
157
  - lib/version.rb
153
158
  - spec/fcc_spec.rb
154
159
  - spec/spec_helper.rb
@@ -171,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
176
  - !ruby/object:Gem::Version
172
177
  version: '0'
173
178
  requirements: []
174
- rubygems_version: 3.1.4
179
+ rubygems_version: 3.3.7
175
180
  signing_key:
176
181
  specification_version: 4
177
182
  summary: Searches the FCC's FM, AM, and TV databases
@@ -1,65 +0,0 @@
1
- require 'httparty'
2
-
3
- module FCC
4
- module Station
5
- class ExtendedInfoParser < HTTParty::Parser
6
- def parse
7
- results = []
8
- body.each_line do |row|
9
- attrs = {}
10
- attrs[:raw] = row
11
- fields = row.split('|').slice(1...-1).collect(&:strip).map { |v| v == '-' ? "" : v }
12
-
13
- attrs[:call_sign] = fields[0]
14
- attrs[:frequency] = parse_frequency(fields[1])
15
- attrs[:band] = fields[2]
16
- attrs[:channel] = fields[3]
17
- attrs[:antenna_type] = fields[4] # Directional Antenna (DA) or NonDirectional (ND)
18
- attrs[:am_operating_time] = fields[5] if fields[5] && attrs[:band]&.upcase == "AM" # (Only used for AM)
19
- attrs[:station_class] = fields[6]
20
- attrs[:region_2_station_class] = fields[7] if fields[7] && attrs[:band]&.upcase == "AM" # (only used for AM)
21
- attrs[:status] = fields[8]
22
- attrs[:city] = fields[9]
23
- attrs[:state] = fields[10]
24
- attrs[:country] = fields[11]
25
- attrs[:file_number] = fields[12] #File Number (Application, Construction Permit or License) or
26
- attrs[:signal_strength] = parse_signal_strength(fields[13]) # Effective Radiated Power --
27
- attrs[:effective_radiated_power] = parse_signal_strength(fields[14]) # Effective Radiated Power -- vertically polarized (maximum)
28
- attrs[:haat_horizontal] = fields[15] # Antenna Height Above Average Terrain (HAAT) -- horizontal polarization
29
- attrs[:haat_vertical] = fields[16] # Antenna Height Above Average Terrain (HAAT) -- vertical polarization
30
- attrs[:fcc_id] = fields[17] # Facility ID Number (unique to each station)
31
- attrs[:latitude] = parse_latitude(fields[18], fields[19], fields[20], fields[21])
32
- attrs[:longitude] = parse_longitude(fields[22], fields[23], fields[24], fields[25])
33
- attrs[:licensed_to] = fields[26] # Licensee or Permittee
34
-
35
- results << attrs
36
- end
37
-
38
- results
39
- end
40
-
41
- def parse_longitude(direction, degrees, minutes, seconds)
42
- decimal_degrees = degrees.to_i + (minutes.to_f / 60) + (seconds.to_f / 3600)
43
-
44
- "#{direction =~ /W/ ? '-' : ''}#{decimal_degrees}"
45
- end
46
-
47
- def parse_latitude(direction, degrees, minutes, seconds)
48
- decimal_degrees = degrees.to_i + (minutes.to_f / 60) + (seconds.to_f / 3600)
49
-
50
- "#{direction =~ /S/ ? '-' : ''}#{decimal_degrees}"
51
- end
52
-
53
- def parse_signal_strength(raw_signal)
54
- if raw_signal
55
- parsed_signal = raw_signal[/[0-9]+\.?[0-9]?/]
56
- "#{parsed_signal.to_f} #{raw_signal.scan(/\w+$/).first}"
57
- end
58
- end
59
-
60
- def parse_frequency(freq)
61
- freq.to_f
62
- end
63
- end
64
- end
65
- end
@@ -1,42 +0,0 @@
1
- require 'active_support/inflector'
2
-
3
- module FCC
4
- module Station
5
- class ResultDelegate
6
- def initialize(result)
7
- @result = result
8
- end
9
-
10
- def method_missing(m, *args, &block)
11
- return find_result(@result, m) unless @result.is_a?(Array)
12
- return find_result(@result.first, m) if @result.size == 1
13
-
14
- filtered_results = @result.filter { |result|
15
- result[:status] == 'LIC' # Licensed only, no construction permits
16
- }
17
-
18
- filtered_results = filtered_results.collect { |res|
19
- find_result(res, m)
20
- }.uniq
21
-
22
- filtered_results.size == 1 ? filtered_results.first : filtered_results
23
- end
24
-
25
- private
26
-
27
- def find_key(result, name)
28
- result.keys.detect { |d| name.to_s == d.to_s } || result.keys.detect { |d| name.to_s == d.to_s.underscore }
29
- end
30
-
31
- def find_result(result, name)
32
- matched_key = find_key(result, name)
33
-
34
- if matched_key
35
- result[matched_key]
36
- else
37
- nil
38
- end
39
- end
40
- end
41
- end
42
- end