fcc 1.1 → 1.3.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.
@@ -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