fcc 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/release.yml +27 -0
- data/.github/workflows/tests.yml +53 -0
- data/.gitignore +41 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +8 -13
- data/Gemfile.lock +52 -15
- data/README.md +106 -0
- data/Rakefile +6 -42
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fcc.gemspec +28 -0
- data/lib/fcc/station/cache.rb +35 -0
- data/lib/fcc/station/extended_info.rb +80 -0
- data/lib/fcc/station/index.rb +39 -0
- data/lib/fcc/station/info.rb +35 -0
- data/lib/fcc/station/lms_data.rb +139 -0
- data/lib/fcc/station/parsers/extended_info.rb +78 -0
- data/lib/fcc/station/parsers/lms_data.rb +13 -0
- data/lib/fcc/station/record_delegate.rb +121 -0
- data/lib/fcc/station/record_group.rb +49 -0
- data/lib/fcc/station/result.rb +180 -0
- data/lib/fcc/station.rb +45 -0
- data/lib/fcc.rb +31 -3
- data/lib/version.rb +5 -0
- data/package.json +92 -0
- data/spec/fcc_spec.rb +7 -0
- data/spec/spec_helper.rb +7 -0
- metadata +154 -120
- data/.rvmrc +0 -1
- data/README.rdoc +0 -28
- data/VERSION +0 -1
- data/lib/am.rb +0 -126
- data/lib/fm.rb +0 -144
- data/test/helper.rb +0 -18
- data/test/test_fcc.rb +0 -7
@@ -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,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,180 @@
|
|
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
|
+
rescue
|
80
|
+
[]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def related_stations
|
85
|
+
@related_stations ||= begin
|
86
|
+
records = lms_data.find_related_stations(call_sign: @call_sign)
|
87
|
+
records.keys.map do |call|
|
88
|
+
RecordDelegate.new(ExtendedInfo.new(@service).find(call))
|
89
|
+
end.select { |f| f.status.upcase == "LIC" }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def related
|
94
|
+
@related ||= begin
|
95
|
+
records = lms_data.find_all_related(call_sign: @call_sign)
|
96
|
+
records.keys.map do |call|
|
97
|
+
ExtendedInfo.new(@service).find(call).collect do |info|
|
98
|
+
RecordDelegate.new(info)
|
99
|
+
end
|
100
|
+
end.flatten.select { |f| f.status.upcase == "LIC" }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def print_broadcast_summary
|
105
|
+
FCC.log "[primary]"
|
106
|
+
transition_records.each do |record|
|
107
|
+
FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
|
108
|
+
end
|
109
|
+
|
110
|
+
FCC.log "[translators]"
|
111
|
+
related_translators.each do |record|
|
112
|
+
FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
|
113
|
+
end
|
114
|
+
|
115
|
+
FCC.log "[related stations]"
|
116
|
+
related_stations.each do |record|
|
117
|
+
FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
|
118
|
+
end
|
119
|
+
|
120
|
+
FCC.log "[all related]"
|
121
|
+
related.each do |record|
|
122
|
+
FCC.log "[#{record.band}] #{record.frequency} #{record.call_sign} — #{record.community.city} #{record.community.state}"
|
123
|
+
end
|
124
|
+
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
|
128
|
+
def lms_data
|
129
|
+
@lms_data ||= LmsData.new
|
130
|
+
end
|
131
|
+
|
132
|
+
def call_signs_match?(ours, theirs)
|
133
|
+
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]+$")
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def public_records
|
139
|
+
public_data_info.map { |r| RecordDelegate.new(r) }
|
140
|
+
end
|
141
|
+
|
142
|
+
def transition_records
|
143
|
+
transition_data_info.map { |r| RecordDelegate.new(r) }
|
144
|
+
end
|
145
|
+
|
146
|
+
def related_records
|
147
|
+
results = related.keys.collect do |call_sign|
|
148
|
+
RecordDelegate.new(ExtendedInfo.new(@service).find(call_sign))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def public_data_info
|
153
|
+
@public_data_info ||= [Info.new(@service).find(@call_sign)]
|
154
|
+
end
|
155
|
+
|
156
|
+
def transition_data_info
|
157
|
+
@transition_data_info ||= ExtendedInfo.new(@service).find(@call_sign)
|
158
|
+
end
|
159
|
+
|
160
|
+
def method_missing(m, *_args)
|
161
|
+
service = if @service == :fm
|
162
|
+
fm_record = grouped_records.find { |gr| FCC::FM_FULL_SERVICE == gr.band.upcase }
|
163
|
+
fm_low_power = grouped_records.find { |gr| FCC::FM_LOW_POWER == gr.band.upcase }
|
164
|
+
fm_booster = grouped_records.find { |gr| FCC::FM_BOOSTER == gr.band.upcase }
|
165
|
+
fm_translator = grouped_records.find { |gr| FCC::FM_TRANSLATOR == gr.band.upcase }
|
166
|
+
|
167
|
+
[fm_record, fm_low_power, fm_booster, fm_translator].compact.find { |r| r.send(m.to_sym) }
|
168
|
+
else
|
169
|
+
grouped_records.find { |r| r.send(m.to_sym) }
|
170
|
+
end
|
171
|
+
|
172
|
+
result = service.send(m.to_sym) if service
|
173
|
+
|
174
|
+
result = result.first if result.is_a?(Array) && result.size == 1
|
175
|
+
|
176
|
+
result
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/lib/fcc/station.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require_relative 'station/extended_info'
|
3
|
+
require_relative 'station/cache'
|
4
|
+
require_relative 'station/index'
|
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'
|
10
|
+
|
11
|
+
module FCC
|
12
|
+
module Station
|
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)
|
15
|
+
|
16
|
+
def self.find_each(service, &block)
|
17
|
+
results = index(service).results
|
18
|
+
|
19
|
+
results.each do |result|
|
20
|
+
yield find(service, result['callSign'])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find(service, call_sign, options = {})
|
25
|
+
Result.new(service, call_sign, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.index(service)
|
29
|
+
case service.to_s.downcase.to_sym
|
30
|
+
when :fm
|
31
|
+
@fm_index ||= Index.new(:fm)
|
32
|
+
when :am
|
33
|
+
@am_index ||= Index.new(:am)
|
34
|
+
when :tv
|
35
|
+
@tv_index ||= Index.new(:tv)
|
36
|
+
else
|
37
|
+
raise "unsupported service #{service}. :fm, :am, and :tv are valid"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.extended_info_cache
|
42
|
+
@cache ||= Station::Cache.new
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/fcc.rb
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './fcc/station'
|
3
|
+
require_relative './fcc/station/cache'
|
4
|
+
require_relative './fcc/station/info'
|
5
|
+
require_relative './fcc/station/extended_info'
|
6
|
+
require_relative './fcc/station/record_delegate'
|
7
|
+
|
8
|
+
module FCC
|
9
|
+
FM_FULL_SERVICE = 'FM'
|
10
|
+
FM_LOW_POWER = 'FL'
|
11
|
+
FM_BOOSTER = 'FB'
|
12
|
+
FM_TRANSLATOR = 'FX'
|
13
|
+
|
14
|
+
def self.cache
|
15
|
+
@cache ||= Station::Cache.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.cache=(cache_service)
|
19
|
+
@cache = cache_service
|
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
|
31
|
+
end
|