fcc 1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -34,74 +38,8 @@ module FCC
34
38
  end
35
39
  end
36
40
 
37
- def self.extended_info_cache(service)
38
- @cache ||= {}
39
- @cache[service] ||= Station::Cache.new(service)
40
- @cache[service]
41
- end
42
-
43
- class Result
44
- EXTENDED_ATTRIBUTES = %i[signal_strength latitude longitude coordinates station_class file_number effective_radiated_power haat_horizontal haat_vertical] # 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 to_json
61
- {}.tap do |hash|
62
- [EXTENDED_ATTRIBUTES | BASIC_ATTRIBUTES | %i[contact owner community]].flatten.each do |attr|
63
- result = send(attr.to_sym)
64
- if result.respond_to?(:to_h)
65
- hash[attr] = result.to_h
66
- else
67
- hash[attr] = result.to_s
68
- end
69
- end
70
- end
71
- end
72
-
73
- def owner
74
- @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)
75
- end
76
-
77
- def community
78
- @community ||= Community.new(city: data.communityCity, state: data.communityState)
79
- end
80
-
81
- def contact
82
- contact = data.mainStudioContact
83
- @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'])
84
- end
85
-
86
- def coordinates
87
- [latitude.to_f, longitude.to_f].to_json
88
- end
89
-
90
- def coordinates_url
91
- "https://www.google.com/maps/search/#{coordinates[0]},#{coordinates[1]}"
92
- end
93
-
94
- def extended_data_url
95
- "https://transition.fcc.gov/fcc-bin/#{@service.to_s.downcase}q?list=4&facid=#{id}"
96
- end
97
-
98
- def extended_data
99
- @extended_data ||= ResultDelegate.new(ExtendedInfo.new(@service).find(@call_sign))
100
- end
101
-
102
- def data
103
- @data ||= ResultDelegate.new(Info.new(@service).find(@call_sign))
104
- end
41
+ def self.extended_info_cache
42
+ @cache ||= Station::Cache.new
105
43
  end
106
44
  end
107
45
  end
data/lib/fcc.rb CHANGED
@@ -3,8 +3,29 @@ 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'
9
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
10
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.1'
4
+ VERSION = '1.3.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.1'
4
+ version: 1.3.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: 2011-01-30 00:00:00.000000000 Z
11
+ date: 2022-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -39,75 +39,89 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.18'
41
41
  - !ruby/object:Gem::Dependency
42
- name: bundler
42
+ name: lightly
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.1'
48
- type: :development
47
+ version: 0.3.3
48
+ type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.1'
54
+ version: 0.3.3
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: rubyzip
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 12.3.3
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: 12.3.3
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rspec
70
+ name: logger
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 3.9.0
89
+ version: '2.1'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 3.9.0
96
+ version: '2.1'
83
97
  - !ruby/object:Gem::Dependency
84
- name: byebug
98
+ name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
- - - ">="
101
+ - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '0'
103
+ version: 12.3.3
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
- - - ">="
108
+ - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: '0'
110
+ version: 12.3.3
97
111
  - !ruby/object:Gem::Dependency
98
- name: awesome_print
112
+ name: rspec
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
- - - ">="
115
+ - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: '0'
117
+ version: 3.9.0
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
- - - ">="
122
+ - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: '0'
124
+ version: 3.9.0
111
125
  description: ''
112
126
  email: jeff@keen.me
113
127
  executables:
@@ -118,6 +132,7 @@ extra_rdoc_files: []
118
132
  files:
119
133
  - ".document"
120
134
  - ".gitignore"
135
+ - CHANGELOG.md
121
136
  - Gemfile
122
137
  - Gemfile.lock
123
138
  - LICENSE.txt
@@ -131,10 +146,14 @@ files:
131
146
  - lib/fcc/station.rb
132
147
  - lib/fcc/station/cache.rb
133
148
  - lib/fcc/station/extended_info.rb
134
- - lib/fcc/station/extended_info/parser.rb
135
149
  - lib/fcc/station/index.rb
136
150
  - lib/fcc/station/info.rb
137
- - 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
138
157
  - lib/version.rb
139
158
  - spec/fcc_spec.rb
140
159
  - spec/spec_helper.rb
@@ -157,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
176
  - !ruby/object:Gem::Version
158
177
  version: '0'
159
178
  requirements: []
160
- rubygems_version: 3.1.2
179
+ rubygems_version: 3.3.7
161
180
  signing_key:
162
181
  specification_version: 4
163
182
  summary: Searches the FCC's FM, AM, and TV databases