device_detector 0.9.1 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +49 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -9
- data/CHANGELOG.md +16 -3
- data/README.md +7 -9
- data/Rakefile +19 -13
- data/device_detector.gemspec +1 -0
- data/lib/device_detector.rb +32 -28
- data/lib/device_detector/bot.rb +2 -2
- data/lib/device_detector/client.rb +3 -2
- data/lib/device_detector/device.rb +44 -21
- data/lib/device_detector/memory_cache.rb +26 -19
- data/lib/device_detector/metadata_extractor.rb +7 -8
- data/lib/device_detector/model_extractor.rb +3 -3
- data/lib/device_detector/name_extractor.rb +2 -2
- data/lib/device_detector/os.rb +121 -111
- data/lib/device_detector/parser.rb +22 -9
- data/lib/device_detector/version.rb +3 -1
- data/lib/device_detector/version_extractor.rb +2 -3
- data/regexes/bots.yml +840 -20
- data/regexes/client/browser_engine.yml +11 -2
- data/regexes/client/browsers.yml +909 -108
- data/regexes/client/feed_readers.yml +38 -2
- data/regexes/client/libraries.yml +76 -2
- data/regexes/client/mediaplayers.yml +25 -5
- data/regexes/client/mobile_apps.yml +167 -2
- data/regexes/client/pim.yml +10 -1
- data/regexes/device/cameras.yml +1 -1
- data/regexes/device/car_browsers.yml +7 -3
- data/regexes/device/consoles.yml +3 -3
- data/regexes/device/mobiles.yml +10123 -465
- data/regexes/device/portable_media_player.yml +4 -6
- data/regexes/device/televisions.yml +18 -4
- data/regexes/oss.yml +115 -21
- data/regexes/vendorfragments.yml +6 -2
- data/spec/device_detector/concrete_user_agent_spec.rb +16 -17
- data/spec/device_detector/detector_fixtures_spec.rb +51 -11
- data/spec/device_detector/device_spec.rb +28 -48
- data/spec/device_detector/memory_cache_spec.rb +60 -28
- data/spec/device_detector/model_extractor_spec.rb +3 -3
- data/spec/device_detector/version_extractor_spec.rb +5 -6
- data/spec/device_detector_spec.rb +60 -69
- data/spec/fixtures/client/browser.yml +1785 -262
- data/spec/fixtures/client/feed_reader.yml +47 -35
- data/spec/fixtures/client/library.yml +112 -3
- data/spec/fixtures/client/mediaplayer.yml +32 -37
- data/spec/fixtures/client/mobile_app.yml +193 -6
- data/spec/fixtures/client/pim.yml +37 -18
- data/spec/fixtures/detector/bots.yml +1426 -118
- data/spec/fixtures/detector/camera.yml +36 -10
- data/spec/fixtures/detector/car_browser.yml +64 -3
- data/spec/fixtures/detector/console.yml +80 -26
- data/spec/fixtures/detector/desktop.yml +2222 -1589
- data/spec/fixtures/detector/feature_phone.yml +151 -42
- data/spec/fixtures/detector/feed_reader.yml +186 -121
- data/spec/fixtures/detector/mediaplayer.yml +113 -39
- data/spec/fixtures/detector/mobile_apps.yml +366 -21
- data/spec/fixtures/detector/phablet.yml +2597 -570
- data/spec/fixtures/detector/portable_media_player.yml +41 -16
- data/spec/fixtures/detector/smart_display.yml +8 -5
- data/spec/fixtures/detector/smart_speaker.yml +55 -0
- data/spec/fixtures/detector/smartphone-1.yml +5468 -5010
- data/spec/fixtures/detector/smartphone-10.yml +9977 -0
- data/spec/fixtures/detector/smartphone-11.yml +9891 -0
- data/spec/fixtures/detector/smartphone-12.yml +9906 -0
- data/spec/fixtures/detector/smartphone-13.yml +9920 -0
- data/spec/fixtures/detector/smartphone-14.yml +2662 -0
- data/spec/fixtures/detector/smartphone-2.yml +5213 -4635
- data/spec/fixtures/detector/smartphone-3.yml +5082 -4533
- data/spec/fixtures/detector/smartphone-4.yml +6806 -2625
- data/spec/fixtures/detector/smartphone-5.yml +9914 -0
- data/spec/fixtures/detector/smartphone-6.yml +9962 -0
- data/spec/fixtures/detector/smartphone-7.yml +9899 -0
- data/spec/fixtures/detector/smartphone-8.yml +9931 -0
- data/spec/fixtures/detector/smartphone-9.yml +9899 -0
- data/spec/fixtures/detector/smartphone.yml +5225 -4652
- data/spec/fixtures/detector/tablet-1.yml +4691 -4191
- data/spec/fixtures/detector/tablet-2.yml +9800 -71
- data/spec/fixtures/detector/tablet-3.yml +9959 -0
- data/spec/fixtures/detector/tablet-4.yml +4528 -0
- data/spec/fixtures/detector/tablet.yml +4664 -4177
- data/spec/fixtures/detector/tv.yml +3399 -1048
- data/spec/fixtures/detector/unknown.yml +1017 -977
- data/spec/fixtures/detector/wearable.yml +61 -0
- data/spec/fixtures/device/camera.yml +4 -3
- data/spec/fixtures/device/car_browser.yml +9 -2
- data/spec/fixtures/device/console.yml +15 -14
- data/spec/fixtures/parser/oss.yml +284 -2
- data/spec/fixtures/parser/vendorfragments.yml +8 -2
- metadata +50 -7
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class DeviceDetector
|
2
4
|
class MemoryCache
|
3
|
-
|
4
5
|
DEFAULT_MAX_KEYS = 5000
|
6
|
+
STORES_NIL_VALUE = :__is_nil__
|
5
7
|
|
6
8
|
attr_reader :data, :max_keys, :lock
|
7
9
|
private :lock
|
@@ -15,41 +17,46 @@ class DeviceDetector
|
|
15
17
|
def set(key, value)
|
16
18
|
lock.synchronize do
|
17
19
|
purge_cache
|
18
|
-
|
20
|
+
# convert nil values into symbol so we know a value is present
|
21
|
+
cache_value = value.nil? ? STORES_NIL_VALUE : value
|
22
|
+
data[String(key)] = cache_value
|
23
|
+
value
|
19
24
|
end
|
20
25
|
end
|
21
26
|
|
22
27
|
def get(key)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
def key?(string_key)
|
27
|
-
data.key?(string_key)
|
28
|
+
value, _hit = get_hit(key)
|
29
|
+
value
|
28
30
|
end
|
29
31
|
|
30
32
|
def get_or_set(key, value = nil)
|
31
33
|
string_key = String(key)
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
35
|
+
result, hit = get_hit(string_key)
|
36
|
+
return result if hit
|
37
|
+
|
38
|
+
value = yield if block_given?
|
39
|
+
set(string_key, value)
|
39
40
|
end
|
40
41
|
|
41
42
|
private
|
42
43
|
|
44
|
+
def get_hit(key)
|
45
|
+
value = data[String(key)]
|
46
|
+
is_hit = !value.nil? || value == STORES_NIL_VALUE
|
47
|
+
value = nil if value == STORES_NIL_VALUE
|
48
|
+
[value, is_hit]
|
49
|
+
end
|
50
|
+
|
43
51
|
def purge_cache
|
44
52
|
key_size = data.size
|
45
53
|
|
46
|
-
if key_size
|
47
|
-
# always remove about 1/3 of keys to reduce garbage collecting
|
48
|
-
amount_of_keys = key_size / 3
|
54
|
+
return if key_size < max_keys
|
49
55
|
|
50
|
-
|
51
|
-
|
52
|
-
end
|
56
|
+
# always remove about 1/3 of keys to reduce garbage collecting
|
57
|
+
amount_of_keys = key_size / 3
|
53
58
|
|
59
|
+
data.keys.first(amount_of_keys).each { |key| data.delete(key) }
|
60
|
+
end
|
54
61
|
end
|
55
62
|
end
|
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class DeviceDetector
|
2
4
|
class MetadataExtractor < Struct.new(:user_agent, :regex_meta)
|
3
|
-
|
4
5
|
def call
|
5
6
|
regex_meta.any? ? extract_metadata : nil
|
6
7
|
end
|
@@ -8,22 +9,20 @@ class DeviceDetector
|
|
8
9
|
private
|
9
10
|
|
10
11
|
def metadata_string
|
11
|
-
message = "#{
|
12
|
-
|
12
|
+
message = "#{name} (a child of MetadataExtractor) must implement the '#{__method__}' method."
|
13
|
+
raise NotImplementedError, message
|
13
14
|
end
|
14
15
|
|
15
16
|
def extract_metadata
|
16
17
|
user_agent.match(regex) do |match_data|
|
17
|
-
metadata_string.gsub(/\$(\d)/)
|
18
|
-
match_data[
|
19
|
-
|
18
|
+
metadata_string.gsub(/\$(\d)/) do
|
19
|
+
match_data[Regexp.last_match(1).to_i].to_s
|
20
|
+
end.strip
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
23
24
|
def regex
|
24
25
|
@regex ||= regex_meta[:regex]
|
25
26
|
end
|
26
|
-
|
27
27
|
end
|
28
28
|
end
|
29
|
-
|
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class DeviceDetector
|
2
4
|
class ModelExtractor < MetadataExtractor
|
3
|
-
|
4
5
|
def call
|
5
|
-
s = super.to_s.gsub('_',' ').strip
|
6
|
+
s = super.to_s.gsub('_', ' ').strip
|
6
7
|
s = s.gsub(/ TD$/i, '')
|
7
8
|
|
8
9
|
return nil if s == 'Build'
|
@@ -19,6 +20,5 @@ class DeviceDetector
|
|
19
20
|
def regex
|
20
21
|
@regex ||= regex_meta[:regex_model] || regex_meta[:regex]
|
21
22
|
end
|
22
|
-
|
23
23
|
end
|
24
24
|
end
|
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class DeviceDetector
|
2
4
|
class NameExtractor < MetadataExtractor
|
3
|
-
|
4
5
|
def call
|
5
6
|
if /\$[0-9]/ =~ metadata_string
|
6
7
|
extract_metadata
|
@@ -14,6 +15,5 @@ class DeviceDetector
|
|
14
15
|
def metadata_string
|
15
16
|
regex_meta[:name]
|
16
17
|
end
|
17
|
-
|
18
18
|
end
|
19
19
|
end
|
data/lib/device_detector/os.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'set'
|
2
4
|
|
3
5
|
class DeviceDetector
|
4
6
|
class OS < Parser
|
5
|
-
|
6
7
|
def name
|
7
8
|
os_info[:name]
|
8
9
|
end
|
@@ -38,125 +39,134 @@ class DeviceDetector
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
|
-
DESKTOP_OSS = Set.new(
|
42
|
+
DESKTOP_OSS = Set.new(
|
43
|
+
[
|
44
|
+
'AmigaOS', 'IBM', 'GNU/Linux', 'Mac', 'Unix', 'Windows', 'BeOS', 'Chrome OS'
|
45
|
+
]
|
46
|
+
)
|
42
47
|
|
43
48
|
# OS short codes mapped to long names
|
44
49
|
OPERATING_SYSTEMS = {
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
50
|
+
'AIX' => 'AIX',
|
51
|
+
'AND' => 'Android',
|
52
|
+
'AMG' => 'AmigaOS',
|
53
|
+
'ATV' => 'Apple TV',
|
54
|
+
'ARL' => 'Arch Linux',
|
55
|
+
'BTR' => 'BackTrack',
|
56
|
+
'SBA' => 'Bada',
|
57
|
+
'BEO' => 'BeOS',
|
58
|
+
'BLB' => 'BlackBerry OS',
|
59
|
+
'QNX' => 'BlackBerry Tablet OS',
|
60
|
+
'BMP' => 'Brew',
|
61
|
+
'CES' => 'CentOS',
|
62
|
+
'COS' => 'Chrome OS',
|
63
|
+
'CYN' => 'CyanogenMod',
|
64
|
+
'DEB' => 'Debian',
|
65
|
+
'DFB' => 'DragonFly',
|
66
|
+
'FED' => 'Fedora',
|
67
|
+
'FOS' => 'Firefox OS',
|
68
|
+
'FIR' => 'Fire OS',
|
69
|
+
'BSD' => 'FreeBSD',
|
70
|
+
'GNT' => 'Gentoo',
|
71
|
+
'GTV' => 'Google TV',
|
72
|
+
'HPX' => 'HP-UX',
|
73
|
+
'HAI' => 'Haiku OS',
|
74
|
+
'IRI' => 'IRIX',
|
75
|
+
'INF' => 'Inferno',
|
76
|
+
'KOS' => 'KaiOS',
|
77
|
+
'KNO' => 'Knoppix',
|
78
|
+
'KBT' => 'Kubuntu',
|
79
|
+
'LIN' => 'GNU/Linux',
|
80
|
+
'LBT' => 'Lubuntu',
|
81
|
+
'VLN' => 'VectorLinux',
|
82
|
+
'MAC' => 'Mac',
|
83
|
+
'MAE' => 'Maemo',
|
84
|
+
'MDR' => 'Mandriva',
|
85
|
+
'SMG' => 'MeeGo',
|
86
|
+
'MCD' => 'MocorDroid',
|
87
|
+
'MIN' => 'Mint',
|
88
|
+
'MLD' => 'MildWild',
|
89
|
+
'MOR' => 'MorphOS',
|
90
|
+
'NBS' => 'NetBSD',
|
91
|
+
'MTK' => 'MTK / Nucleus',
|
92
|
+
'WII' => 'Nintendo',
|
93
|
+
'NDS' => 'Nintendo Mobile',
|
94
|
+
'OS2' => 'OS/2',
|
95
|
+
'T64' => 'OSF1',
|
96
|
+
'OBS' => 'OpenBSD',
|
97
|
+
'ORD' => 'Ordissimo',
|
98
|
+
'PSP' => 'PlayStation Portable',
|
99
|
+
'PS3' => 'PlayStation',
|
100
|
+
'RHT' => 'Red Hat',
|
101
|
+
'ROS' => 'RISC OS',
|
102
|
+
'REM' => 'Remix OS',
|
103
|
+
'RZD' => 'RazoDroiD',
|
104
|
+
'SAB' => 'Sabayon',
|
105
|
+
'SSE' => 'SUSE',
|
106
|
+
'SAF' => 'Sailfish OS',
|
107
|
+
'SLW' => 'Slackware',
|
108
|
+
'SOS' => 'Solaris',
|
109
|
+
'SYL' => 'Syllable',
|
110
|
+
'SYM' => 'Symbian',
|
111
|
+
'SYS' => 'Symbian OS',
|
112
|
+
'S40' => 'Symbian OS Series 40',
|
113
|
+
'S60' => 'Symbian OS Series 60',
|
114
|
+
'SY3' => 'Symbian^3',
|
115
|
+
'TDX' => 'ThreadX',
|
116
|
+
'TIZ' => 'Tizen',
|
117
|
+
'TOS' => 'TmaxOS',
|
118
|
+
'UBT' => 'Ubuntu',
|
119
|
+
'WTV' => 'WebTV',
|
120
|
+
'WIN' => 'Windows',
|
121
|
+
'WCE' => 'Windows CE',
|
122
|
+
'WIO' => 'Windows IoT',
|
123
|
+
'WMO' => 'Windows Mobile',
|
124
|
+
'WPH' => 'Windows Phone',
|
125
|
+
'WRT' => 'Windows RT',
|
126
|
+
'XBX' => 'Xbox',
|
127
|
+
'XBT' => 'Xubuntu',
|
128
|
+
'YNS' => 'YunOs',
|
129
|
+
'IOS' => 'iOS',
|
130
|
+
'POS' => 'palmOS',
|
131
|
+
'WOS' => 'webOS'
|
132
|
+
}.freeze
|
133
|
+
|
134
|
+
DOWNCASED_OPERATING_SYSTEMS = OPERATING_SYSTEMS.each_with_object({}) do |(short, long), h|
|
135
|
+
h[long.downcase] = short
|
136
|
+
end
|
125
137
|
|
126
138
|
OS_FAMILIES = {
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
}
|
151
|
-
|
152
|
-
FAMILY_TO_OS = OS_FAMILIES.each_with_object({}) do |(family,oss),h|
|
153
|
-
oss.each{|os| h[os] = family}
|
139
|
+
'Android' => %w[AND CYN FIR REM RZD MLD MCD YNS],
|
140
|
+
'AmigaOS' => %w[AMG MOR],
|
141
|
+
'Apple TV' => ['ATV'],
|
142
|
+
'BlackBerry' => %w[BLB QNX],
|
143
|
+
'Brew' => ['BMP'],
|
144
|
+
'BeOS' => %w[BEO HAI],
|
145
|
+
'Chrome OS' => ['COS'],
|
146
|
+
'Firefox OS' => %w[FOS KOS],
|
147
|
+
'Gaming Console' => %w[WII PS3],
|
148
|
+
'Google TV' => ['GTV'],
|
149
|
+
'IBM' => ['OS2'],
|
150
|
+
'iOS' => ['IOS'],
|
151
|
+
'RISC OS' => ['ROS'],
|
152
|
+
'GNU/Linux' => %w[LIN ARL DEB KNO MIN UBT KBT XBT LBT FED RHT VLN MDR GNT SAB SLW SSE CES BTR SAF ORD TOS],
|
153
|
+
'Mac' => ['MAC'],
|
154
|
+
'Mobile Gaming Console' => %w[PSP NDS XBX],
|
155
|
+
'Real-time OS' => %w[MTK TDX],
|
156
|
+
'Other Mobile' => %w[WOS POS SBA TIZ SMG MAE],
|
157
|
+
'Symbian' => %w[SYM SYS SY3 S60 S40],
|
158
|
+
'Unix' => %w[SOS AIX HPX BSD NBS OBS DFB SYL IRI T64 INF],
|
159
|
+
'WebTV' => ['WTV'],
|
160
|
+
'Windows' => ['WIN'],
|
161
|
+
'Windows Mobile' => %w[WPH WMO WCE WRT WIO]
|
162
|
+
}.freeze
|
163
|
+
|
164
|
+
FAMILY_TO_OS = OS_FAMILIES.each_with_object({}) do |(family, oss), h|
|
165
|
+
oss.each { |os| h[os] = family }
|
154
166
|
end
|
155
167
|
|
156
168
|
def filenames
|
157
169
|
['oss.yml']
|
158
170
|
end
|
159
|
-
|
160
171
|
end
|
161
|
-
|
162
172
|
end
|
@@ -1,7 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class DeviceDetector
|
2
|
-
class Parser
|
4
|
+
class Parser
|
5
|
+
ROOT = File.expand_path('../..', __dir__)
|
6
|
+
|
7
|
+
REGEX_CACHE = ::DeviceDetector::MemoryCache.new({})
|
8
|
+
private_constant :REGEX_CACHE
|
9
|
+
|
10
|
+
def initialize(user_agent)
|
11
|
+
@user_agent = user_agent
|
12
|
+
end
|
3
13
|
|
4
|
-
|
14
|
+
attr_reader :user_agent
|
5
15
|
|
6
16
|
def name
|
7
17
|
from_cache(['name', self.class.name, user_agent]) do
|
@@ -32,17 +42,17 @@ class DeviceDetector
|
|
32
42
|
end
|
33
43
|
|
34
44
|
def filenames
|
35
|
-
|
45
|
+
raise NotImplementedError
|
36
46
|
end
|
37
47
|
|
38
48
|
def filepaths
|
39
49
|
filenames.map do |filename|
|
40
|
-
[
|
50
|
+
[filename.to_sym, File.join(ROOT, 'regexes', filename)]
|
41
51
|
end
|
42
52
|
end
|
43
53
|
|
44
54
|
def regexes_for(file_paths)
|
45
|
-
|
55
|
+
REGEX_CACHE.get_or_set(file_paths) do
|
46
56
|
load_regexes(file_paths).flat_map { |path, regex| parse_regexes(path, regex) }
|
47
57
|
end
|
48
58
|
end
|
@@ -54,16 +64,20 @@ class DeviceDetector
|
|
54
64
|
def symbolize_keys!(object)
|
55
65
|
case object
|
56
66
|
when Array
|
57
|
-
object.map!{ |v| symbolize_keys!(v) }
|
67
|
+
object.map! { |v| symbolize_keys!(v) }
|
58
68
|
when Hash
|
59
|
-
|
69
|
+
keys = object.keys
|
70
|
+
keys.each do |k|
|
71
|
+
object[k.to_sym] = symbolize_keys!(object.delete(k)) if k.is_a?(String)
|
72
|
+
end
|
60
73
|
end
|
61
74
|
object
|
62
75
|
end
|
63
76
|
|
64
77
|
def parse_regexes(path, raw_regexes)
|
65
78
|
raw_regexes.map do |meta|
|
66
|
-
|
79
|
+
raise "invalid device spec: #{meta.inspect}" unless meta[:regex].is_a? String
|
80
|
+
|
67
81
|
meta[:regex] = build_regex(meta[:regex])
|
68
82
|
meta[:path] = path
|
69
83
|
meta
|
@@ -77,6 +91,5 @@ class DeviceDetector
|
|
77
91
|
def from_cache(key)
|
78
92
|
DeviceDetector.cache.get_or_set(key) { yield }
|
79
93
|
end
|
80
|
-
|
81
94
|
end
|
82
95
|
end
|