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 +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -14
- data/Gemfile.lock +12 -14
- data/README.md +43 -2
- data/fcc.gemspec +2 -2
- data/lib/fcc/station/cache.rb +19 -6
- data/lib/fcc/station/extended_info.rb +19 -16
- data/lib/fcc/station/info.rb +4 -1
- 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 +178 -0
- data/lib/fcc/station.rb +8 -90
- data/lib/fcc.rb +16 -1
- data/lib/version.rb +1 -1
- metadata +35 -30
- data/lib/fcc/station/extended_info/parser.rb +0 -65
- data/lib/fcc/station/result_delegate.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e5293b201813db44d301865ab81106198363704a76624b6f191d55a311fbf42
|
4
|
+
data.tar.gz: ef103e7da9885445910a74ce925ee86a802d0cc16e4d4dac1a67c9dcb8951073
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 "
|
21
|
-
# gem "
|
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.
|
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 (
|
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
|
-
|
18
|
-
|
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.
|
24
|
+
i18n (1.10.0)
|
26
25
|
concurrent-ruby (~> 1.0)
|
27
26
|
lightly (0.3.3)
|
28
|
-
|
27
|
+
logger (1.5.0)
|
28
|
+
mime-types (3.4.1)
|
29
29
|
mime-types-data (~> 3.2015)
|
30
|
-
mime-types-data (3.
|
31
|
-
minitest (5.
|
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
|
data/lib/fcc/station/cache.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'httparty'
|
2
|
-
require_relative './extended_info
|
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
|
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
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
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://
|
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
|
-
|
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
|
-
|
54
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
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
|
data/lib/fcc/station/info.rb
CHANGED
@@ -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,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
|
-
|
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/
|
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
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.
|
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:
|
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:
|
56
|
+
name: rubyzip
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
62
|
-
type: :
|
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: '
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: logger
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
76
|
-
type: :
|
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:
|
82
|
+
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: bundler
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
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:
|
96
|
+
version: '2.1'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: rake
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- - "
|
101
|
+
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
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:
|
110
|
+
version: 12.3.3
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: rspec
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
|
-
- - "
|
115
|
+
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version:
|
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:
|
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/
|
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.
|
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
|