diff --git a/app_common/__init__.py b/app_common/__init__.py index 9ea74431..7f49029b 100644 --- a/app_common/__init__.py +++ b/app_common/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from . import models from . import controllers +from . import models from .hooks import pre_init_hook from .hooks import post_init_hook -from .hooks import uninstall_hook \ No newline at end of file +from .hooks import uninstall_hook diff --git a/app_common/__manifest__.py b/app_common/__manifest__.py index d6678341..76051baa 100644 --- a/app_common/__manifest__.py +++ b/app_common/__manifest__.py @@ -33,7 +33,7 @@ { 'name': "Sunpop Odooapp Common Func", - 'version': '14.21.01.27', + 'version': '14.21.10.03', 'author': 'Sunpop.cn', 'category': 'Base', 'website': 'https://www.sunpop.cn', @@ -42,10 +42,6 @@ 'price': 0.00, 'currency': 'EUR', 'images': ['static/description/banner.png'], - 'depends': [ - 'base', - 'web', - ], 'summary': ''' Core for common use sunpop apps. 基础核心,必须没有要被依赖字段及视图等,实现auto_install @@ -64,11 +60,15 @@ 4. 多公司支持 5. Odoo 13, 12, 企业版,社区版,多版本支持 ''', + 'depends': [ + 'base', + 'web', + ], 'data': [ # 'security/*.xml', # 'security/ir.model.access.csv', # 'data/.xml', - # 'views/ir_module_module_views.xml', + 'views/ir_cron_views.xml', # 'report/.xml', ], 'qweb': [ diff --git a/app_common/controllers/__init__.py b/app_common/controllers/__init__.py index 221005dc..6920e202 100644 --- a/app_common/controllers/__init__.py +++ b/app_common/controllers/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- -# from . import main \ No newline at end of file + +from . import main \ No newline at end of file diff --git a/app_common/controllers/main.py b/app_common/controllers/main.py new file mode 100644 index 00000000..fe720a53 --- /dev/null +++ b/app_common/controllers/main.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +import base64 +from io import BytesIO +import requests +from math import radians, cos, sin, asin, sqrt + +from ..lib.user_agents import parse + +from odoo import api, http, SUPERUSER_ID, _ +from odoo import http, exceptions +from odoo.http import request + +import logging +_logger = logging.getLogger(__name__) + +class AppController(http.Controller): + + def get_image_from_url(self, url): + if not url: + return None + try: + response = requests.get(url) # 将这个图片保存在内存 + except Exception as e: + return None + # 返回这个图片的base64编码 + return base64.b64encode(BytesIO(response.content).read()) + + @http.route('/web/ua/show', auth='public', methods=['GET']) + def app_ua_show(self): + # https://github.com/selwin/python-user-agents + ua_string = request.httprequest.headers.get('User-Agent') + user_agent = parse(ua_string) + ua_type = self.get_ua_type() + ustr = "Request UA:
%s
Parse UA:
%s
UA Type:
%s
" % (ua_string, str(user_agent), ua_type) + return request.make_response(ustr, [('Content-Type', 'text/html')]) + + def get_ua_type(self): + ua = request.httprequest.headers.get('User-Agent') + # 临时用 agent 处理,后续要前端中正确处理或者都从后台来 + # 微信浏览器 + # MicroMessenger: Mozilla/5.0 (Linux; Android 10; ELE-AL00 Build/HUAWEIELE-AL00; wv) + # AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.120 + # MQQBrowser/6.2 TBS/045525 Mobile Safari/537.36 MMWEBID/3135 MicroMessenger/8.0.2.1860(0x2800023B) Process/tools WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 + # 微信浏览器,开发工具,网页 iphone + # ,Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1 + # wechatdevtools/1.03.2011120 MicroMessenger/7.0.4 Language/zh_CN webview/16178807094901773 + # webdebugger port/27772 token/b91f4a234b918f4e2a5d1a835a09c31e + + # 微信小程序 + # MicroMessenger: Mozilla/5.0 (Linux; Android 10; ELE-AL00 Build/HUAWEIELE-AL00; wv) + # AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.62 XWEB/2767 MMWEBSDK/20210302 Mobile Safari/537.36 MMWEBID/6689 MicroMessenger/8.0.2.1860(0x2800023B) Process/appbrand2 WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 + # MiniProgramEnv/android + # 微信浏览器,开发工具,小程序,iphone + # Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1 + # wechatdevtools/1.03.2011120 MicroMessenger/7.0.4 Language/zh_CN webview/ + # 微信内,iphone web + # Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 + # MicroMessenger/8.0.3(0x1800032a) NetType/WIFI Language/zh_CN + # 安卓app,h5 + # ELE-AL00(Android/10) (cn.erpapp.o20sticks.App/13.20.12.09) Weex/0.26.0 1080x2265 + + utype = 'web' + # todo: 引入现成 py lib,处理企业微信 + if 'MicroMessenger' in ua and 'webdebugger' not in ua and ('MiniProgramEnv' in ua or 'wechatdevtools' in ua): + # 微信小程序及开发者工具 + utype = 'wxapp' + elif 'MicroMessenger' in ua: + # 微信浏览器 + utype = 'wxweb' + elif 'cn.erpapp.o20sticks.App' in ua: + # 安卓app + utype = 'native_android' + # _logger.warning('=========get ua %s,%s' % (utype, ua)) + return utype + + +def haversine(lon1, lat1, lon2, lat2): + # 计算地图上两点的距离 + # in:经度1,纬度1,经度2,纬度2 (十进制度数) + # out: 距离(米) + """ + Calculate the great circle distance between two points + on the earth (specified in decimal degrees) + """ + # 将十进制度数转化为弧度 + lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) + + # haversine公式 + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2 + c = 2 * asin(sqrt(a)) + r = 6371 # 地球平均半径,单位为公里 + return c * r * 1000 diff --git a/app_common/i18n/zh_CN.po b/app_common/i18n/zh_CN.po index f1e36e3e..9552e39e 100644 --- a/app_common/i18n/zh_CN.po +++ b/app_common/i18n/zh_CN.po @@ -12,3 +12,18 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" + +#. module: app_common +#: model:ir.model.fields,field_description:app_common.field_ir_cron__trigger_user_id +msgid "Last Trigger User" +msgstr "手动运行用户" + +#. module: app_common +#: model:ir.model,name:app_common.model_ir_cron +msgid "Scheduled Actions" +msgstr "安排的动作" + +#. module: app_common +#: model:ir.model,name:app_common.model_ir_ui_view +msgid "View" +msgstr "查看" diff --git a/app_common/lib/ua_parser/__init__.py b/app_common/lib/ua_parser/__init__.py new file mode 100644 index 00000000..ca04bbe6 --- /dev/null +++ b/app_common/lib/ua_parser/__init__.py @@ -0,0 +1 @@ +VERSION = (0, 10, 0) diff --git a/app_common/lib/ua_parser/_regexes.py b/app_common/lib/ua_parser/_regexes.py new file mode 100644 index 00000000..2e626e16 --- /dev/null +++ b/app_common/lib/ua_parser/_regexes.py @@ -0,0 +1,7406 @@ +# -*- coding: utf-8 -*- +############################################ +# NOTICE: This file is autogenerated from # +# regexes.yaml. Do not edit by hand, # +# instead, re-run `setup.py build_regexes` # +############################################ + +from __future__ import absolute_import, unicode_literals +from .user_agent_parser import ( + UserAgentParser, DeviceParser, OSParser, +) + +__all__ = ( + 'USER_AGENT_PARSERS', 'DEVICE_PARSERS', 'OS_PARSERS', +) + +USER_AGENT_PARSERS = [ + UserAgentParser( + '^(Luminary)[Stage]+/(\\d+) CFNetwork', + None, + None, + None, + ), + UserAgentParser( + '(ESPN)[%20| ]+Radio/(\\d+)\\.(\\d+)\\.(\\d+) CFNetwork', + None, + None, + None, + ), + UserAgentParser( + '(Antenna)/(\\d+) CFNetwork', + 'AntennaPod', + None, + None, + ), + UserAgentParser( + '(TopPodcasts)Pro/(\\d+) CFNetwork', + None, + None, + None, + ), + UserAgentParser( + '(MusicDownloader)Lite/(\\d+)\\.(\\d+)\\.(\\d+) CFNetwork', + None, + None, + None, + ), + UserAgentParser( + '^(.*)-iPad\\/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)(?:\\.(\\d+)|) CFNetwork', + None, + None, + None, + ), + UserAgentParser( + '^(.*)-iPhone/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)(?:\\.(\\d+)|) CFNetwork', + None, + None, + None, + ), + UserAgentParser( + '^(.*)/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)(?:\\.(\\d+)|) CFNetwork', + None, + None, + None, + ), + UserAgentParser( + '^(Luminary)/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(espn\\.go)', + 'ESPN', + None, + None, + ), + UserAgentParser( + '(espnradio\\.com)', + 'ESPN', + None, + None, + ), + UserAgentParser( + 'ESPN APP$', + 'ESPN', + None, + None, + ), + UserAgentParser( + '(audioboom\\.com)', + 'AudioBoom', + None, + None, + ), + UserAgentParser( + ' (Rivo) RHYTHM', + None, + None, + None, + ), + UserAgentParser( + '(CFNetwork)(?:/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)|)', + 'CFNetwork', + None, + None, + ), + UserAgentParser( + '(Pingdom\\.com_bot_version_)(\\d+)\\.(\\d+)', + 'PingdomBot', + None, + None, + ), + UserAgentParser( + '(PingdomTMS)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'PingdomBot', + None, + None, + ), + UserAgentParser( + ' (PTST)/(\\d+)(?:\\.(\\d+)|)$', + 'WebPageTest.org bot', + None, + None, + ), + UserAgentParser( + 'X11; (Datanyze); Linux', + None, + None, + None, + ), + UserAgentParser( + '(NewRelicPinger)/(\\d+)\\.(\\d+)', + 'NewRelicPingerBot', + None, + None, + ), + UserAgentParser( + '(Tableau)/(\\d+)\\.(\\d+)', + 'Tableau', + None, + None, + ), + UserAgentParser( + 'AppleWebKit/\\d+\\.\\d+.* Safari.* (CreativeCloud)/(\\d+)\\.(\\d+).(\\d+)', + 'Adobe CreativeCloud', + None, + None, + ), + UserAgentParser( + '(Salesforce)(?:.)\\/(\\d+)\\.(\\d?)', + None, + None, + None, + ), + UserAgentParser( + '(\\(StatusCake\\))', + 'StatusCakeBot', + None, + None, + ), + UserAgentParser( + '(facebookexternalhit)/(\\d+)\\.(\\d+)', + 'FacebookBot', + None, + None, + ), + UserAgentParser( + 'Google.*/\\+/web/snippet', + 'GooglePlusBot', + None, + None, + ), + UserAgentParser( + 'via ggpht\\.com GoogleImageProxy', + 'GmailImageProxy', + None, + None, + ), + UserAgentParser( + 'YahooMailProxy; https://help\\.yahoo\\.com/kb/yahoo-mail-proxy-SLN28749\\.html', + 'YahooMailProxy', + None, + None, + ), + UserAgentParser( + '(Twitterbot)/(\\d+)\\.(\\d+)', + 'Twitterbot', + None, + None, + ), + UserAgentParser( + '/((?:Ant-|)Nutch|[A-z]+[Bb]ot|[A-z]+[Ss]pider|Axtaris|fetchurl|Isara|ShopSalad|Tailsweep)[ \\-](\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '\\b(008|Altresium|Argus|BaiduMobaider|BoardReader|DNSGroup|DataparkSearch|EDI|Goodzer|Grub|INGRID|Infohelfer|LinkedInBot|LOOQ|Nutch|OgScrper|PathDefender|Peew|PostPost|Steeler|Twitterbot|VSE|WebCrunch|WebZIP|Y!J-BR[A-Z]|YahooSeeker|envolk|sproose|wminer)/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(MSIE) (\\d+)\\.(\\d+)([a-z]\\d|[a-z]|);.* MSIECrawler', + 'MSIECrawler', + None, + None, + ), + UserAgentParser( + '(DAVdroid)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(Google-HTTP-Java-Client|Apache-HttpClient|Go-http-client|scalaj-http|http%20client|Python-urllib|HttpMonitor|TLSProber|WinHTTP|JNLP|okhttp|aihttp|reqwest|axios|unirest-(?:java|python|ruby|nodejs|php|net))(?:[ /](\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)|)', + None, + None, + None, + ), + UserAgentParser( + '(Pinterest(?:bot|))/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)[;\\s(]+\\+https://www.pinterest.com/bot.html', + 'Pinterestbot', + None, + None, + ), + UserAgentParser( + '(CSimpleSpider|Cityreview Robot|CrawlDaddy|CrawlFire|Finderbots|Index crawler|Job Roboter|KiwiStatus Spider|Lijit Crawler|QuerySeekerSpider|ScollSpider|Trends Crawler|USyd-NLP-Spider|SiteCat Webbot|BotName\\/\\$BotVersion|123metaspider-Bot|1470\\.net crawler|50\\.nu|8bo Crawler Bot|Aboundex|Accoona-[A-z]{1,30}-Agent|AdsBot-Google(?:-[a-z]{1,30}|)|altavista|AppEngine-Google|archive.{0,30}\\.org_bot|archiver|Ask Jeeves|[Bb]ai[Dd]u[Ss]pider(?:-[A-Za-z]{1,30})(?:-[A-Za-z]{1,30}|)|bingbot|BingPreview|blitzbot|BlogBridge|Bloglovin|BoardReader Blog Indexer|BoardReader Favicon Fetcher|boitho.com-dc|BotSeer|BUbiNG|\\b\\w{0,30}favicon\\w{0,30}\\b|\\bYeti(?:-[a-z]{1,30}|)|Catchpoint(?: bot|)|[Cc]harlotte|Checklinks|clumboot|Comodo HTTP\\(S\\) Crawler|Comodo-Webinspector-Crawler|ConveraCrawler|CRAWL-E|CrawlConvera|Daumoa(?:-feedfetcher|)|Feed Seeker Bot|Feedbin|findlinks|Flamingo_SearchEngine|FollowSite Bot|furlbot|Genieo|gigabot|GomezAgent|gonzo1|(?:[a-zA-Z]{1,30}-|)Googlebot(?:-[a-zA-Z]{1,30}|)|Google SketchUp|grub-client|gsa-crawler|heritrix|HiddenMarket|holmes|HooWWWer|htdig|ia_archiver|ICC-Crawler|Icarus6j|ichiro(?:/mobile|)|IconSurf|IlTrovatore(?:-Setaccio|)|InfuzApp|Innovazion Crawler|InternetArchive|IP2[a-z]{1,30}Bot|jbot\\b|KaloogaBot|Kraken|Kurzor|larbin|LEIA|LesnikBot|Linguee Bot|LinkAider|LinkedInBot|Lite Bot|Llaut|lycos|Mail\\.RU_Bot|masscan|masidani_bot|Mediapartners-Google|Microsoft .{0,30} Bot|mogimogi|mozDex|MJ12bot|msnbot(?:-media {0,2}|)|msrbot|Mtps Feed Aggregation System|netresearch|Netvibes|NewsGator[^/]{0,30}|^NING|Nutch[^/]{0,30}|Nymesis|ObjectsSearch|OgScrper|Orbiter|OOZBOT|PagePeeker|PagesInventory|PaxleFramework|Peeplo Screenshot Bot|PlantyNet_WebRobot|Pompos|Qwantify|Read%20Later|Reaper|RedCarpet|Retreiver|Riddler|Rival IQ|scooter|Scrapy|Scrubby|searchsight|seekbot|semanticdiscovery|SemrushBot|Simpy|SimplePie|SEOstats|SimpleRSS|SiteCon|Slackbot-LinkExpanding|Slack-ImgProxy|Slurp|snappy|Speedy Spider|Squrl Java|Stringer|TheUsefulbot|ThumbShotsBot|Thumbshots\\.ru|Tiny Tiny RSS|Twitterbot|WhatsApp|URL2PNG|Vagabondo|VoilaBot|^vortex|Votay bot|^voyager|WASALive.Bot|Web-sniffer|WebThumb|WeSEE:[A-z]{1,30}|WhatWeb|WIRE|WordPress|Wotbox|www\\.almaden\\.ibm\\.com|Xenu(?:.s|) Link Sleuth|Xerka [A-z]{1,30}Bot|yacy(?:bot|)|YahooSeeker|Yahoo! Slurp|Yandex\\w{1,30}|YodaoBot(?:-[A-z]{1,30}|)|YottaaMonitor|Yowedo|^Zao|^Zao-Crawler|ZeBot_www\\.ze\\.bz|ZooShot|ZyBorg)(?:[ /]v?(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)|)|)|)', + None, + None, + None, + ), + UserAgentParser( + '\\b(Boto3?|JetS3t|aws-(?:cli|sdk-(?:cpp|go|java|nodejs|ruby2?|dotnet-(?:\\d{1,2}|core)))|s3fs)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '\\[(FBAN/MessengerForiOS|FB_IAB/MESSENGER);FBAV/(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)|)|)', + 'Facebook Messenger', + None, + None, + ), + UserAgentParser( + '\\[FB.*;(FBAV)/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + 'Facebook', + None, + None, + ), + UserAgentParser( + '\\[FB.*;', + 'Facebook', + None, + None, + ), + UserAgentParser( + '(?:\\/[A-Za-z0-9\\.]+|) {0,5}([A-Za-z0-9 \\-_\\!\\[\\]:]{0,50}(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]{0,50}))[/ ](\\d+)(?:\\.(\\d+)(?:\\.(\\d+)|)|)', + None, + None, + None, + ), + UserAgentParser( + '((?:[A-Za-z][A-Za-z0-9 -]{0,50}|)[^C][^Uu][Bb]ot)\\b(?:(?:[ /]| v)(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)|)', + None, + None, + None, + ), + UserAgentParser( + '((?:[A-z0-9]{1,50}|[A-z\\-]{1,50} ?|)(?: the |)(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]{0,50})(?:(?:[ /]| v)(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)|)', + None, + None, + None, + ), + UserAgentParser( + '(HbbTV)/(\\d+)\\.(\\d+)\\.(\\d+) \\(', + None, + None, + None, + ), + UserAgentParser( + '(Chimera|SeaMonkey|Camino|Waterfox)/(\\d+)\\.(\\d+)\\.?([ab]?\\d+[a-z]*|)', + None, + None, + None, + ), + UserAgentParser( + '(SailfishBrowser)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Sailfish Browser', + None, + None, + ), + UserAgentParser( + '\\[(Pinterest)/[^\\]]+\\]', + None, + None, + None, + ), + UserAgentParser( + '(Pinterest)(?: for Android(?: Tablet|)|)/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + 'Mozilla.*Mobile.*(Instagram).(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + 'Mozilla.*Mobile.*(Flipboard).(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + 'Mozilla.*Mobile.*(Flipboard-Briefing).(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + 'Mozilla.*Mobile.*(Onefootball)\\/Android.(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Snapchat)\\/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Twitter for (?:iPhone|iPad)|TwitterAndroid)(?:\\/(\\d+)\\.(\\d+)|)', + 'Twitter', + None, + None, + ), + UserAgentParser( + '(Firefox)/(\\d+)\\.(\\d+) Basilisk/(\\d+)', + 'Basilisk', + None, + None, + ), + UserAgentParser( + '(PaleMoon)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Pale Moon', + None, + None, + ), + UserAgentParser( + '(Fennec)/(\\d+)\\.(\\d+)\\.?([ab]?\\d+[a-z]*)', + 'Firefox Mobile', + None, + None, + ), + UserAgentParser( + '(Fennec)/(\\d+)\\.(\\d+)(pre)', + 'Firefox Mobile', + None, + None, + ), + UserAgentParser( + '(Fennec)/(\\d+)\\.(\\d+)', + 'Firefox Mobile', + None, + None, + ), + UserAgentParser( + '(?:Mobile|Tablet);.*(Firefox)/(\\d+)\\.(\\d+)', + 'Firefox Mobile', + None, + None, + ), + UserAgentParser( + '(Namoroka|Shiretoko|Minefield)/(\\d+)\\.(\\d+)\\.(\\d+(?:pre|))', + 'Firefox ($1)', + None, + None, + ), + UserAgentParser( + '(Firefox)/(\\d+)\\.(\\d+)(a\\d+[a-z]*)', + 'Firefox Alpha', + None, + None, + ), + UserAgentParser( + '(Firefox)/(\\d+)\\.(\\d+)(b\\d+[a-z]*)', + 'Firefox Beta', + None, + None, + ), + UserAgentParser( + '(Firefox)-(?:\\d+\\.\\d+|)/(\\d+)\\.(\\d+)(a\\d+[a-z]*)', + 'Firefox Alpha', + None, + None, + ), + UserAgentParser( + '(Firefox)-(?:\\d+\\.\\d+|)/(\\d+)\\.(\\d+)(b\\d+[a-z]*)', + 'Firefox Beta', + None, + None, + ), + UserAgentParser( + '(Namoroka|Shiretoko|Minefield)/(\\d+)\\.(\\d+)([ab]\\d+[a-z]*|)', + 'Firefox ($1)', + None, + None, + ), + UserAgentParser( + '(Firefox).*Tablet browser (\\d+)\\.(\\d+)\\.(\\d+)', + 'MicroB', + None, + None, + ), + UserAgentParser( + '(MozillaDeveloperPreview)/(\\d+)\\.(\\d+)([ab]\\d+[a-z]*|)', + None, + None, + None, + ), + UserAgentParser( + '(FxiOS)/(\\d+)\\.(\\d+)(\\.(\\d+)|)(\\.(\\d+)|)', + 'Firefox iOS', + None, + None, + ), + UserAgentParser( + '(Flock)/(\\d+)\\.(\\d+)(b\\d+?)', + None, + None, + None, + ), + UserAgentParser( + '(RockMelt)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Navigator)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Netscape', + None, + None, + ), + UserAgentParser( + '(Navigator)/(\\d+)\\.(\\d+)([ab]\\d+)', + 'Netscape', + None, + None, + ), + UserAgentParser( + '(Netscape6)/(\\d+)\\.(\\d+)\\.?([ab]?\\d+|)', + 'Netscape', + None, + None, + ), + UserAgentParser( + '(MyIBrow)/(\\d+)\\.(\\d+)', + 'My Internet Browser', + None, + None, + ), + UserAgentParser( + '(UC? ?Browser|UCWEB|U3)[ /]?(\\d+)\\.(\\d+)\\.(\\d+)', + 'UC Browser', + None, + None, + ), + UserAgentParser( + '(Opera Tablet).*Version/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(Opera Mini)(?:/att|)/?(\\d+|)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(Opera)/.+Opera Mobi.+Version/(\\d+)\\.(\\d+)', + 'Opera Mobile', + None, + None, + ), + UserAgentParser( + '(Opera)/(\\d+)\\.(\\d+).+Opera Mobi', + 'Opera Mobile', + None, + None, + ), + UserAgentParser( + 'Opera Mobi.+(Opera)(?:/|\\s+)(\\d+)\\.(\\d+)', + 'Opera Mobile', + None, + None, + ), + UserAgentParser( + 'Opera Mobi', + 'Opera Mobile', + None, + None, + ), + UserAgentParser( + '(Opera)/9.80.*Version/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(?:Mobile Safari).*(OPR)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Opera Mobile', + None, + None, + ), + UserAgentParser( + '(?:Chrome).*(OPR)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Opera', + None, + None, + ), + UserAgentParser( + '(Coast)/(\\d+).(\\d+).(\\d+)', + 'Opera Coast', + None, + None, + ), + UserAgentParser( + '(OPiOS)/(\\d+).(\\d+).(\\d+)', + 'Opera Mini', + None, + None, + ), + UserAgentParser( + 'Chrome/.+( MMS)/(\\d+).(\\d+).(\\d+)', + 'Opera Neon', + None, + None, + ), + UserAgentParser( + '(hpw|web)OS/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'webOS Browser', + None, + None, + ), + UserAgentParser( + '(luakit)', + 'LuaKit', + None, + None, + ), + UserAgentParser( + '(Snowshoe)/(\\d+)\\.(\\d+).(\\d+)', + None, + None, + None, + ), + UserAgentParser( + 'Gecko/\\d+ (Lightning)/(\\d+)\\.(\\d+)\\.?((?:[ab]?\\d+[a-z]*)|(?:\\d*))', + None, + None, + None, + ), + UserAgentParser( + '(Firefox)/(\\d+)\\.(\\d+)\\.(\\d+(?:pre|)) \\(Swiftfox\\)', + 'Swiftfox', + None, + None, + ), + UserAgentParser( + '(Firefox)/(\\d+)\\.(\\d+)([ab]\\d+[a-z]*|) \\(Swiftfox\\)', + 'Swiftfox', + None, + None, + ), + UserAgentParser( + '(rekonq)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|) Safari', + 'Rekonq', + None, + None, + ), + UserAgentParser( + 'rekonq', + 'Rekonq', + None, + None, + ), + UserAgentParser( + '(conkeror|Conkeror)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Conkeror', + None, + None, + ), + UserAgentParser( + '(konqueror)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Konqueror', + None, + None, + ), + UserAgentParser( + '(WeTab)-Browser', + None, + None, + None, + ), + UserAgentParser( + '(Comodo_Dragon)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Comodo Dragon', + None, + None, + ), + UserAgentParser( + '(Symphony) (\\d+).(\\d+)', + None, + None, + None, + ), + UserAgentParser( + 'PLAYSTATION 3.+WebKit', + 'NetFront NX', + None, + None, + ), + UserAgentParser( + 'PLAYSTATION 3', + 'NetFront', + None, + None, + ), + UserAgentParser( + '(PlayStation Portable)', + 'NetFront', + None, + None, + ), + UserAgentParser( + '(PlayStation Vita)', + 'NetFront NX', + None, + None, + ), + UserAgentParser( + 'AppleWebKit.+ (NX)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'NetFront NX', + None, + None, + ), + UserAgentParser( + '(Nintendo 3DS)', + 'NetFront NX', + None, + None, + ), + UserAgentParser( + '(Silk)/(\\d+)\\.(\\d+)(?:\\.([0-9\\-]+)|)', + 'Amazon Silk', + None, + None, + ), + UserAgentParser( + '(Puffin)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + 'Windows Phone .*(Edge)/(\\d+)\\.(\\d+)', + 'Edge Mobile', + None, + None, + ), + UserAgentParser( + '(EdgA)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Edge Mobile', + None, + None, + ), + UserAgentParser( + '(EdgiOS)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Edge Mobile', + None, + None, + ), + UserAgentParser( + '(SamsungBrowser)/(\\d+)\\.(\\d+)', + 'Samsung Internet', + None, + None, + ), + UserAgentParser( + '(SznProhlizec)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Seznam prohlížeč', + None, + None, + ), + UserAgentParser( + '(coc_coc_browser)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Coc Coc', + None, + None, + ), + UserAgentParser( + '(baidubrowser)[/\\s](\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + 'Baidu Browser', + None, + None, + ), + UserAgentParser( + '(FlyFlow)/(\\d+)\\.(\\d+)', + 'Baidu Explorer', + None, + None, + ), + UserAgentParser( + '(MxBrowser)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Maxthon', + None, + None, + ), + UserAgentParser( + '(Crosswalk)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Line)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'LINE', + None, + None, + ), + UserAgentParser( + '(MiuiBrowser)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'MiuiBrowser', + None, + None, + ), + UserAgentParser( + '(Mint Browser)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Mint Browser', + None, + None, + ), + UserAgentParser( + '(TopBuzz)/(\\d+).(\\d+).(\\d+)', + 'TopBuzz', + None, + None, + ), + UserAgentParser( + 'Mozilla.+Android.+(GSA)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Google', + None, + None, + ), + UserAgentParser( + '(MQQBrowser/Mini)(?:(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)|)', + 'QQ Browser Mini', + None, + None, + ), + UserAgentParser( + '(MQQBrowser)(?:/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)|)', + 'QQ Browser Mobile', + None, + None, + ), + UserAgentParser( + '(QQBrowser)(?:/(\\d+)(?:\\.(\\d+)\\.(\\d+)(?:\\.(\\d+)|)|)|)', + 'QQ Browser', + None, + None, + ), + UserAgentParser( + 'Version/.+(Chrome)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Chrome Mobile WebView', + None, + None, + ), + UserAgentParser( + '; wv\\).+(Chrome)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Chrome Mobile WebView', + None, + None, + ), + UserAgentParser( + '(CrMo)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Chrome Mobile', + None, + None, + ), + UserAgentParser( + '(CriOS)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Chrome Mobile iOS', + None, + None, + ), + UserAgentParser( + '(Chrome)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+) Mobile(?:[ /]|$)', + 'Chrome Mobile', + None, + None, + ), + UserAgentParser( + ' Mobile .*(Chrome)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Chrome Mobile', + None, + None, + ), + UserAgentParser( + '(chromeframe)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Chrome Frame', + None, + None, + ), + UserAgentParser( + '(SLP Browser)/(\\d+)\\.(\\d+)', + 'Tizen Browser', + None, + None, + ), + UserAgentParser( + '(SE 2\\.X) MetaSr (\\d+)\\.(\\d+)', + 'Sogou Explorer', + None, + None, + ), + UserAgentParser( + '(Rackspace Monitoring)/(\\d+)\\.(\\d+)', + 'RackspaceBot', + None, + None, + ), + UserAgentParser( + '(PyAMF)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(YaBrowser)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Yandex Browser', + None, + None, + ), + UserAgentParser( + '(Chrome)/(\\d+)\\.(\\d+)\\.(\\d+).* MRCHROME', + 'Mail.ru Chromium Browser', + None, + None, + ), + UserAgentParser( + '(AOL) (\\d+)\\.(\\d+); AOLBuild (\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(PodCruncher|Downcast)[ /]?(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + ' (BoxNotes)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Whale)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+) Mobile(?:[ /]|$)', + 'Whale', + None, + None, + ), + UserAgentParser( + '(Whale)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Whale', + None, + None, + ), + UserAgentParser( + '(1Password)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Ghost)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Slack_SSB)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Slack Desktop Client', + None, + None, + ), + UserAgentParser( + '(HipChat)/?(\\d+|)', + 'HipChat Desktop Client', + None, + None, + ), + UserAgentParser( + '\\b(MobileIron|FireWeb|Jasmine|ANTGalio|Midori|Fresco|Lobo|PaleMoon|Maxthon|Lynx|OmniWeb|Dillo|Camino|Demeter|Fluid|Fennec|Epiphany|Shiira|Sunrise|Spotify|Flock|Netscape|Lunascape|WebPilot|NetFront|Netfront|Konqueror|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|Opera Mini|iCab|NetNewsWire|ThunderBrowse|Iris|UP\\.Browser|Bunjalloo|Google Earth|Raven for Mac|Openwave|MacOutlook|Electron|OktaMobile)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + 'Microsoft Office Outlook 12\\.\\d+\\.\\d+|MSOffice 12', + 'Outlook', + '2007', + None, + ), + UserAgentParser( + 'Microsoft Outlook 14\\.\\d+\\.\\d+|MSOffice 14', + 'Outlook', + '2010', + None, + ), + UserAgentParser( + 'Microsoft Outlook 15\\.\\d+\\.\\d+', + 'Outlook', + '2013', + None, + ), + UserAgentParser( + 'Microsoft Outlook (?:Mail )?16\\.\\d+\\.\\d+|MSOffice 16', + 'Outlook', + '2016', + None, + ), + UserAgentParser( + 'Microsoft Office (Word) 2014', + None, + None, + None, + ), + UserAgentParser( + 'Outlook-Express\\/7\\.0.*', + 'Windows Live Mail', + None, + None, + ), + UserAgentParser( + '(Airmail) (\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(Thunderbird)/(\\d+)\\.(\\d+)(?:\\.(\\d+(?:pre|))|)', + 'Thunderbird', + None, + None, + ), + UserAgentParser( + '(Postbox)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Postbox', + None, + None, + ), + UserAgentParser( + '(Barca(?:Pro)?)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Barca', + None, + None, + ), + UserAgentParser( + '(Lotus-Notes)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Lotus Notes', + None, + None, + ), + UserAgentParser( + 'Superhuman', + 'Superhuman', + None, + None, + ), + UserAgentParser( + '(Vivaldi)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Edge?)/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + 'Edge', + None, + None, + ), + UserAgentParser( + '(brave)/(\\d+)\\.(\\d+)\\.(\\d+) Chrome', + 'Brave', + None, + None, + ), + UserAgentParser( + '(Chrome)/(\\d+)\\.(\\d+)\\.(\\d+)[\\d.]* Iron[^/]', + 'Iron', + None, + None, + ), + UserAgentParser( + '\\b(Dolphin)(?: |HDCN/|/INT\\-)(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(HeadlessChrome)(?:/(\\d+)\\.(\\d+)\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(Evolution)/(\\d+)\\.(\\d+)\\.(\\d+\\.\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(RCM CardDAV plugin)/(\\d+)\\.(\\d+)\\.(\\d+(?:-dev|))', + None, + None, + None, + ), + UserAgentParser( + '(bingbot|Bolt|AdobeAIR|Jasmine|IceCat|Skyfire|Midori|Maxthon|Lynx|Arora|IBrowse|Dillo|Camino|Shiira|Fennec|Phoenix|Flock|Netscape|Lunascape|Epiphany|WebPilot|Opera Mini|Opera|NetFront|Netfront|Konqueror|Googlebot|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|iCab|iTunes|MacAppStore|NetNewsWire|Space Bison|Stainless|Orca|Dolfin|BOLT|Minimo|Tizen Browser|Polaris|Abrowser|Planetweb|ICE Browser|mDolphin|qutebrowser|Otter|QupZilla|MailBar|kmail2|YahooMobileMail|ExchangeWebServices|ExchangeServicesClient|Dragon|Outlook-iOS-Android)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(Chromium|Chrome)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(IEMobile)[ /](\\d+)\\.(\\d+)', + 'IE Mobile', + None, + None, + ), + UserAgentParser( + '(BacaBerita App)\\/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '^(bPod|Pocket Casts|Player FM)$', + None, + None, + None, + ), + UserAgentParser( + '^(AlexaMediaPlayer|VLC)/(\\d+)\\.(\\d+)\\.([^.\\s]+)', + None, + None, + None, + ), + UserAgentParser( + '^(AntennaPod|WMPlayer|Zune|Podkicker|Radio|ExoPlayerDemo|Overcast|PocketTunes|NSPlayer|okhttp|DoggCatcher|QuickNews|QuickTime|Peapod|Podcasts|GoldenPod|VLC|Spotify|Miro|MediaGo|Juice|iPodder|gPodder|Banshee)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '^(Peapod|Liferea)/([^.\\s]+)\\.([^.\\s]+|)\\.?([^.\\s]+|)', + None, + None, + None, + ), + UserAgentParser( + '^(bPod|Player FM) BMID/(\\S+)', + None, + None, + None, + ), + UserAgentParser( + '^(Podcast ?Addict)/v(\\d+) ', + None, + None, + None, + ), + UserAgentParser( + '^(Podcast ?Addict) ', + 'PodcastAddict', + None, + None, + ), + UserAgentParser( + '(Replay) AV', + None, + None, + None, + ), + UserAgentParser( + '(VOX) Music Player', + None, + None, + None, + ), + UserAgentParser( + '(CITA) RSS Aggregator/(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Pocket Casts)$', + None, + None, + None, + ), + UserAgentParser( + '(Player FM)$', + None, + None, + None, + ), + UserAgentParser( + '(LG Player|Doppler|FancyMusic|MediaMonkey|Clementine) (\\d+)\\.(\\d+)\\.?([^.\\s]+|)\\.?([^.\\s]+|)', + None, + None, + None, + ), + UserAgentParser( + '(philpodder)/(\\d+)\\.(\\d+)\\.?([^.\\s]+|)\\.?([^.\\s]+|)', + None, + None, + None, + ), + UserAgentParser( + '(Player FM|Pocket Casts|DoggCatcher|Spotify|MediaMonkey|MediaGo|BashPodder)', + None, + None, + None, + ), + UserAgentParser( + '(QuickTime)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Kinoma)(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Fancy) Cloud Music (\\d+)\\.(\\d+)', + 'FancyMusic', + None, + None, + ), + UserAgentParser( + 'EspnDownloadManager', + 'ESPN', + None, + None, + ), + UserAgentParser( + '(ESPN) Radio (\\d+)\\.(\\d+)(?:\\.(\\d+)|) ?(?:rv:(\\d+)|) ', + None, + None, + None, + ), + UserAgentParser( + '(podracer|jPodder) v ?(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(ZDM)/(\\d+)\\.(\\d+)[; ]?', + None, + None, + None, + ), + UserAgentParser( + '(Zune|BeyondPod) (\\d+)(?:\\.(\\d+)|)[\\);]', + None, + None, + None, + ), + UserAgentParser( + '(WMPlayer)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '^(Lavf)', + 'WMPlayer', + None, + None, + ), + UserAgentParser( + '^(RSSRadio)[ /]?(\\d+|)', + None, + None, + None, + ), + UserAgentParser( + '(RSS_Radio) (\\d+)\\.(\\d+)', + 'RSSRadio', + None, + None, + ), + UserAgentParser( + '(Podkicker) \\S+/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Podkicker', + None, + None, + ), + UserAgentParser( + '^(HTC) Streaming Player \\S+ / \\S+ / \\S+ / (\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '^(Stitcher)/iOS', + None, + None, + None, + ), + UserAgentParser( + '^(Stitcher)/Android', + None, + None, + None, + ), + UserAgentParser( + '^(VLC) .*version (\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + ' (VLC) for', + None, + None, + None, + ), + UserAgentParser( + '(vlc)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'VLC', + None, + None, + ), + UserAgentParser( + '^(foobar)\\S+/([^.\\s]+)\\.([^.\\s]+|)\\.?([^.\\s]+|)', + None, + None, + None, + ), + UserAgentParser( + '^(Clementine)\\S+ ([^.\\s]+)\\.([^.\\s]+|)\\.?([^.\\s]+|)', + None, + None, + None, + ), + UserAgentParser( + '(amarok)/([^.\\s]+)\\.([^.\\s]+|)\\.?([^.\\s]+|)', + 'Amarok', + None, + None, + ), + UserAgentParser( + '(Custom)-Feed Reader', + None, + None, + None, + ), + UserAgentParser( + '(iRider|Crazy Browser|SkipStone|iCab|Lunascape|Sleipnir|Maemo Browser) (\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(iCab|Lunascape|Opera|Android|Jasmine|Polaris|Microsoft SkyDriveSync|The Bat!) (\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(Kindle)/(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Android) Donut', + None, + '1', + '2', + ), + UserAgentParser( + '(Android) Eclair', + None, + '2', + '1', + ), + UserAgentParser( + '(Android) Froyo', + None, + '2', + '2', + ), + UserAgentParser( + '(Android) Gingerbread', + None, + '2', + '3', + ), + UserAgentParser( + '(Android) Honeycomb', + None, + '3', + None, + ), + UserAgentParser( + '(MSIE) (\\d+)\\.(\\d+).*XBLWP7', + 'IE Large Screen', + None, + None, + ), + UserAgentParser( + '(Nextcloud)', + None, + None, + None, + ), + UserAgentParser( + '(mirall)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(ownCloud-android)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Owncloud', + None, + None, + ), + UserAgentParser( + '(OC)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+) \\(Skype for Business\\)', + 'Skype', + None, + None, + ), + UserAgentParser( + '(Obigo)InternetBrowser', + None, + None, + None, + ), + UserAgentParser( + '(Obigo)\\-Browser', + None, + None, + None, + ), + UserAgentParser( + '(Obigo|OBIGO)[^\\d]*(\\d+)(?:.(\\d+)|)', + 'Obigo', + None, + None, + ), + UserAgentParser( + '(MAXTHON|Maxthon) (\\d+)\\.(\\d+)', + 'Maxthon', + None, + None, + ), + UserAgentParser( + '(Maxthon|MyIE2|Uzbl|Shiira)', + None, + '0', + None, + ), + UserAgentParser( + '(BrowseX) \\((\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(NCSA_Mosaic)/(\\d+)\\.(\\d+)', + 'NCSA Mosaic', + None, + None, + ), + UserAgentParser( + '(POLARIS)/(\\d+)\\.(\\d+)', + 'Polaris', + None, + None, + ), + UserAgentParser( + '(Embider)/(\\d+)\\.(\\d+)', + 'Polaris', + None, + None, + ), + UserAgentParser( + '(BonEcho)/(\\d+)\\.(\\d+)\\.?([ab]?\\d+|)', + 'Bon Echo', + None, + None, + ), + UserAgentParser( + '(TopBuzz) com.alex.NewsMaster/(\\d+).(\\d+).(\\d+)', + 'TopBuzz', + None, + None, + ), + UserAgentParser( + '(TopBuzz) com.mobilesrepublic.newsrepublic/(\\d+).(\\d+).(\\d+)', + 'TopBuzz', + None, + None, + ), + UserAgentParser( + '(TopBuzz) com.topbuzz.videoen/(\\d+).(\\d+).(\\d+)', + 'TopBuzz', + None, + None, + ), + UserAgentParser( + '(iPod|iPhone|iPad).+GSA/(\\d+)\\.(\\d+)\\.(\\d+)(?:\\.(\\d+)|) Mobile', + 'Google', + None, + None, + ), + UserAgentParser( + '(iPod|iPhone|iPad).+Version/(\\d+)\\.(\\d+)(?:\\.(\\d+)|).*[ +]Safari', + 'Mobile Safari', + None, + None, + ), + UserAgentParser( + '(iPod|iPod touch|iPhone|iPad);.*CPU.*OS[ +](\\d+)_(\\d+)(?:_(\\d+)|).* AppleNews\\/\\d+\\.\\d+\\.\\d+?', + 'Mobile Safari UI/WKWebView', + None, + None, + ), + UserAgentParser( + '(iPod|iPhone|iPad).+Version/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Mobile Safari UI/WKWebView', + None, + None, + ), + UserAgentParser( + '(iPod|iPod touch|iPhone|iPad).* Safari', + 'Mobile Safari', + None, + None, + ), + UserAgentParser( + '(iPod|iPod touch|iPhone|iPad)', + 'Mobile Safari UI/WKWebView', + None, + None, + ), + UserAgentParser( + '(Watch)(\\d+),(\\d+)', + 'Apple $1 App', + None, + None, + ), + UserAgentParser( + '(Outlook-iOS)/\\d+\\.\\d+\\.prod\\.iphone \\((\\d+)\\.(\\d+)\\.(\\d+)\\)', + None, + None, + None, + ), + UserAgentParser( + '(AvantGo) (\\d+).(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(OneBrowser)/(\\d+).(\\d+)', + 'ONE Browser', + None, + None, + ), + UserAgentParser( + '(Avant)', + None, + '1', + None, + ), + UserAgentParser( + '(QtCarBrowser)', + None, + '1', + None, + ), + UserAgentParser( + '^(iBrowser/Mini)(\\d+).(\\d+)', + 'iBrowser Mini', + None, + None, + ), + UserAgentParser( + '^(iBrowser|iRAPP)/(\\d+).(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '^(Nokia)', + 'Nokia Services (WAP) Browser', + None, + None, + ), + UserAgentParser( + '(NokiaBrowser)/(\\d+)\\.(\\d+).(\\d+)\\.(\\d+)', + 'Nokia Browser', + None, + None, + ), + UserAgentParser( + '(NokiaBrowser)/(\\d+)\\.(\\d+).(\\d+)', + 'Nokia Browser', + None, + None, + ), + UserAgentParser( + '(NokiaBrowser)/(\\d+)\\.(\\d+)', + 'Nokia Browser', + None, + None, + ), + UserAgentParser( + '(BrowserNG)/(\\d+)\\.(\\d+).(\\d+)', + 'Nokia Browser', + None, + None, + ), + UserAgentParser( + '(Series60)/5\\.0', + 'Nokia Browser', + '7', + '0', + ), + UserAgentParser( + '(Series60)/(\\d+)\\.(\\d+)', + 'Nokia OSS Browser', + None, + None, + ), + UserAgentParser( + '(S40OviBrowser)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)', + 'Ovi Browser', + None, + None, + ), + UserAgentParser( + '(Nokia)[EN]?(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(PlayBook).+RIM Tablet OS (\\d+)\\.(\\d+)\\.(\\d+)', + 'BlackBerry WebKit', + None, + None, + ), + UserAgentParser( + '(Black[bB]erry|BB10).+Version/(\\d+)\\.(\\d+)\\.(\\d+)', + 'BlackBerry WebKit', + None, + None, + ), + UserAgentParser( + '(Black[bB]erry)\\s?(\\d+)', + 'BlackBerry', + None, + None, + ), + UserAgentParser( + '(OmniWeb)/v(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Blazer)/(\\d+)\\.(\\d+)', + 'Palm Blazer', + None, + None, + ), + UserAgentParser( + '(Pre)/(\\d+)\\.(\\d+)', + 'Palm Pre', + None, + None, + ), + UserAgentParser( + '(ELinks)/(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(ELinks) \\((\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Links) \\((\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(QtWeb) Internet Browser/(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(PhantomJS)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(AppleWebKit)/(\\d+)(?:\\.(\\d+)|)\\+ .* Safari', + 'WebKit Nightly', + None, + None, + ), + UserAgentParser( + '(Version)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|).*Safari/', + 'Safari', + None, + None, + ), + UserAgentParser( + '(Safari)/\\d+', + None, + None, + None, + ), + UserAgentParser( + '(OLPC)/Update(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(OLPC)/Update()\\.(\\d+)', + None, + '0', + None, + ), + UserAgentParser( + '(SEMC\\-Browser)/(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Teleca)', + 'Teleca Browser', + None, + None, + ), + UserAgentParser( + '(Phantom)/V(\\d+)\\.(\\d+)', + 'Phantom Browser', + None, + None, + ), + UserAgentParser( + '(Trident)/(7|8)\\.(0)', + 'IE', + '11', + None, + ), + UserAgentParser( + '(Trident)/(6)\\.(0)', + 'IE', + '10', + None, + ), + UserAgentParser( + '(Trident)/(5)\\.(0)', + 'IE', + '9', + None, + ), + UserAgentParser( + '(Trident)/(4)\\.(0)', + 'IE', + '8', + None, + ), + UserAgentParser( + '(Espial)/(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '(AppleWebKit)/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Apple Mail', + None, + None, + ), + UserAgentParser( + '(Firefox)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Firefox)/(\\d+)\\.(\\d+)(pre|[ab]\\d+[a-z]*|)', + None, + None, + None, + ), + UserAgentParser( + '([MS]?IE) (\\d+)\\.(\\d+)', + 'IE', + None, + None, + ), + UserAgentParser( + '(python-requests)/(\\d+)\\.(\\d+)', + 'Python Requests', + None, + None, + ), + UserAgentParser( + '\\b(Windows-Update-Agent|Microsoft-CryptoAPI|SophosUpdateManager|SophosAgent|Debian APT-HTTP|Ubuntu APT-HTTP|libcurl-agent|libwww-perl|urlgrabber|curl|PycURL|Wget|aria2|Axel|OpenBSD ftp|lftp|jupdate|insomnia|fetch libfetch|akka-http|got)(?:[ /](\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)|)', + None, + None, + None, + ), + UserAgentParser( + '(Python/3\\.\\d{1,3} aiohttp)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Python/3\\.\\d{1,3} aiohttp)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Java)[/ ]?\\d+\\.(\\d+)\\.(\\d+)[_-]*([a-zA-Z0-9]+|)', + None, + None, + None, + ), + UserAgentParser( + '^(Cyberduck)/(\\d+)\\.(\\d+)\\.(\\d+)(?:\\.\\d+|)', + None, + None, + None, + ), + UserAgentParser( + '^(S3 Browser) (\\d+)-(\\d+)-(\\d+)(?:\\s*http://s3browser\\.com|)', + None, + None, + None, + ), + UserAgentParser( + '(S3Gof3r)', + None, + None, + None, + ), + UserAgentParser( + '\\b(ibm-cos-sdk-(?:core|java|js|python))/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + None, + None, + None, + ), + UserAgentParser( + '^(rusoto)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '^(rclone)/v(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '^(Roku)/DVP-(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '(Kurio)\\/(\\d+)\\.(\\d+)\\.(\\d+)', + 'Kurio App', + None, + None, + ), + UserAgentParser( + '^(Box(?: Sync)?)/(\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + ), + UserAgentParser( + '^(ViaFree|Viafree)-(?:tvOS-)?[A-Z]{2}/(\\d+)\\.(\\d+)\\.(\\d+)', + 'ViaFree', + None, + None, + ), +] + +DEVICE_PARSERS = [ + DeviceParser( + '(?:(?:iPhone|Windows CE|Windows Phone|Android).*(?:(?:Bot|Yeti)-Mobile|YRSpider|BingPreview|bots?/\\d|(?:bot|spider)\\.html)|AdsBot-Google-Mobile.*iPhone)', + 'i', + 'Spider', + 'Spider', + 'Smartphone', + ), + DeviceParser( + '(?:DoCoMo|\\bMOT\\b|\\bLG\\b|Nokia|Samsung|SonyEricsson).*(?:(?:Bot|Yeti)-Mobile|bots?/\\d|(?:bot|crawler)\\.html|(?:jump|google|Wukong)bot|ichiro/mobile|/spider|YahooSeeker)', + 'i', + 'Spider', + 'Spider', + 'Feature Phone', + ), + DeviceParser( + ' PTST/\\d+(?:\\.)?\\d+$', + None, + 'Spider', + 'Spider', + None, + ), + DeviceParser( + 'X11; Datanyze; Linux', + None, + 'Spider', + 'Spider', + None, + ), + DeviceParser( + '\\bSmartWatch {0,2}\\( {0,2}([^;]+) {0,2}; {0,2}([^;]+) {0,2};', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + 'Android Application[^\\-]+ - (Sony) ?(Ericsson|) (.+) \\w+ - ', + None, + '$1 $2', + '$1$2', + '$3', + ), + DeviceParser( + 'Android Application[^\\-]+ - (?:HTC|HUAWEI|LGE|LENOVO|MEDION|TCT) (HTC|HUAWEI|LG|LENOVO|MEDION|ALCATEL)[ _\\-](.+) \\w+ - ', + 'i', + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + 'Android Application[^\\-]+ - ([^ ]+) (.+) \\w+ - ', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *([BLRQ]C\\d{4}[A-Z]+?)(?: Build|\\) AppleWebKit)', + None, + '3Q $1', + '3Q', + '$1', + ), + DeviceParser( + '; *(?:3Q_)([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '3Q $1', + '3Q', + '$1', + ), + DeviceParser( + 'Android [34].*; *(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700(?: Lite| 3G|)|A701|B1-A71|A1-\\d{3}|B1-\\d{3}|V360|V370|W500|W500P|W501|W501P|W510|W511|W700|Slider SL101|DA22[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Acer', + '$1', + ), + DeviceParser( + '; *Acer Iconia Tab ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Acer', + '$1', + ), + DeviceParser( + '; *(Z1[1235]0|E320[^/]*|S500|S510|Liquid[^;/]*|Iconia A\\d+)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Acer', + '$1', + ), + DeviceParser( + '; *(Acer |ACER )([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Acer', + '$2', + ), + DeviceParser( + '; *(Advent |)(Vega(?:Bean|Comb|)).*?(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Advent', + '$2', + ), + DeviceParser( + '; *(Ainol |)((?:NOVO|[Nn]ovo)[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Ainol', + '$2', + ), + DeviceParser( + '; *AIRIS[ _\\-]?([^/;\\)]+) *(?:;|\\)|Build)', + 'i', + '$1', + 'Airis', + '$1', + ), + DeviceParser( + '; *(OnePAD[^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'Airis', + '$1', + ), + DeviceParser( + '; *Airpad[ \\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Airpad $1', + 'Airpad', + '$1', + ), + DeviceParser( + '; *(one ?touch) (EVO7|T10|T20)(?: Build|\\) AppleWebKit)', + None, + 'Alcatel One Touch $2', + 'Alcatel', + 'One Touch $2', + ), + DeviceParser( + '; *(?:alcatel[ _]|)(?:(?:one[ _]?touch[ _])|ot[ \\-])([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Alcatel One Touch $1', + 'Alcatel', + 'One Touch $1', + ), + DeviceParser( + '; *(TCL)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(Vodafone Smart II|Optimus_Madrid)(?: Build|\\) AppleWebKit)', + None, + 'Alcatel $1', + 'Alcatel', + '$1', + ), + DeviceParser( + '; *BASE_Lutea_3(?: Build|\\) AppleWebKit)', + None, + 'Alcatel One Touch 998', + 'Alcatel', + 'One Touch 998', + ), + DeviceParser( + '; *BASE_Varia(?: Build|\\) AppleWebKit)', + None, + 'Alcatel One Touch 918D', + 'Alcatel', + 'One Touch 918D', + ), + DeviceParser( + '; *((?:FINE|Fine)\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Allfine', + '$1', + ), + DeviceParser( + '; *(ALLVIEW[ _]?|Allview[ _]?)((?:Speed|SPEED).*?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Allview', + '$2', + ), + DeviceParser( + '; *(ALLVIEW[ _]?|Allview[ _]?|)(AX1_Shine|AX2_Frenzy)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Allview', + '$2', + ), + DeviceParser( + '; *(ALLVIEW[ _]?|Allview[ _]?)([^;/]*?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Allview', + '$2', + ), + DeviceParser( + '; *(A13-MID)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Allwinner', + '$1', + ), + DeviceParser( + '; *(Allwinner)[ _\\-]?([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Allwinner', + '$1', + ), + DeviceParser( + '; *(A651|A701B?|A702|A703|A705|A706|A707|A711|A712|A713|A717|A722|A785|A801|A802|A803|A901|A902|A1002|A1003|A1006|A1007|A9701|A9703|Q710|Q80)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Amaway', + '$1', + ), + DeviceParser( + '; *(?:AMOI|Amoi)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Amoi $1', + 'Amoi', + '$1', + ), + DeviceParser( + '^(?:AMOI|Amoi)[ _]([^;/]+?) Linux', + None, + 'Amoi $1', + 'Amoi', + '$1', + ), + DeviceParser( + '; *(MW(?:0[789]|10)[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Aoc', + '$1', + ), + DeviceParser( + '; *(G7|M1013|M1015G|M11[CG]?|M-?12[B]?|M15|M19[G]?|M30[ACQ]?|M31[GQ]|M32|M33[GQ]|M36|M37|M38|M701T|M710|M712B|M713|M715G|M716G|M71(?:G|GS|T|)|M72[T]?|M73[T]?|M75[GT]?|M77G|M79T|M7L|M7LN|M81|M810|M81T|M82|M92|M92KS|M92S|M717G|M721|M722G|M723|M725G|M739|M785|M791|M92SK|M93D)(?: Build|\\) AppleWebKit)', + None, + 'Aoson $1', + 'Aoson', + '$1', + ), + DeviceParser( + '; *Aoson ([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Aoson $1', + 'Aoson', + '$1', + ), + DeviceParser( + '; *[Aa]panda[ _\\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Apanda $1', + 'Apanda', + '$1', + ), + DeviceParser( + '; *(?:ARCHOS|Archos) ?(GAMEPAD.*?)(?: Build|\\) AppleWebKit)', + None, + 'Archos $1', + 'Archos', + '$1', + ), + DeviceParser( + 'ARCHOS; GOGI; ([^;]+);', + None, + 'Archos $1', + 'Archos', + '$1', + ), + DeviceParser( + '(?:ARCHOS|Archos)[ _]?(.*?)(?: Build|[;/\\(\\)\\-]|$)', + None, + 'Archos $1', + 'Archos', + '$1', + ), + DeviceParser( + '; *(AN(?:7|8|9|10|13)[A-Z0-9]{1,4})(?: Build|\\) AppleWebKit)', + None, + 'Archos $1', + 'Archos', + '$1', + ), + DeviceParser( + '; *(A28|A32|A43|A70(?:BHT|CHT|HB|S|X)|A101(?:B|C|IT)|A7EB|A7EB-WK|101G9|80G9)(?: Build|\\) AppleWebKit)', + None, + 'Archos $1', + 'Archos', + '$1', + ), + DeviceParser( + '; *(PAD-FMD[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Arival', + '$1', + ), + DeviceParser( + '; *(BioniQ) ?([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Arival', + '$1 $2', + ), + DeviceParser( + '; *(AN\\d[^;/]+|ARCHM\\d+)(?: Build|\\) AppleWebKit)', + None, + 'Arnova $1', + 'Arnova', + '$1', + ), + DeviceParser( + '; *(?:ARNOVA|Arnova) ?([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Arnova $1', + 'Arnova', + '$1', + ), + DeviceParser( + '; *(?:ASSISTANT |)(AP)-?([1789]\\d{2}[A-Z]{0,2}|80104)(?: Build|\\) AppleWebKit)', + None, + 'Assistant $1-$2', + 'Assistant', + '$1-$2', + ), + DeviceParser( + '; *(ME17\\d[^;/]*|ME3\\d{2}[^;/]+|K00[A-Z]|Nexus 10|Nexus 7(?: 2013|)|PadFone[^;/]*|Transformer[^;/]*|TF\\d{3}[^;/]*|eeepc)(?: Build|\\) AppleWebKit)', + None, + 'Asus $1', + 'Asus', + '$1', + ), + DeviceParser( + '; *ASUS[ _]*([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Asus $1', + 'Asus', + '$1', + ), + DeviceParser( + '; *Garmin-Asus ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Garmin-Asus $1', + 'Garmin-Asus', + '$1', + ), + DeviceParser( + '; *(Garminfone)(?: Build|\\) AppleWebKit)', + None, + 'Garmin $1', + 'Garmin-Asus', + '$1', + ), + DeviceParser( + '; (@TAB-[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Attab', + '$1', + ), + DeviceParser( + '; *(T-(?:07|[^0]\\d)[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Audiosonic', + '$1', + ), + DeviceParser( + '; *(?:Axioo[ _\\-]([^;/]+?)|(picopad)[ _\\-]([^;/]+?))(?: Build|\\) AppleWebKit)', + 'i', + 'Axioo $1$2 $3', + 'Axioo', + '$1$2 $3', + ), + DeviceParser( + '; *(V(?:100|700|800)[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Azend', + '$1', + ), + DeviceParser( + '; *(IBAK\\-[^;/]*)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'Bak', + '$1', + ), + DeviceParser( + '; *(HY5001|HY6501|X12|X21|I5)(?: Build|\\) AppleWebKit)', + None, + 'Bedove $1', + 'Bedove', + '$1', + ), + DeviceParser( + '; *(JC-[^;/]*)(?: Build|\\) AppleWebKit)', + None, + 'Benss $1', + 'Benss', + '$1', + ), + DeviceParser( + '; *(BB) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Blackberry', + '$2', + ), + DeviceParser( + '; *(BlackBird)[ _](I8.*?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(BlackBird)[ _](.*?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *([0-9]+BP[EM][^;/]*|Endeavour[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Blaupunkt $1', + 'Blaupunkt', + '$1', + ), + DeviceParser( + '; *((?:BLU|Blu)[ _\\-])([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Blu', + '$2', + ), + DeviceParser( + '; *(?:BMOBILE )?(Blu|BLU|DASH [^;/]+|VIVO 4\\.3|TANK 4\\.5)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Blu', + '$1', + ), + DeviceParser( + '; *(TOUCH\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Blusens', + '$1', + ), + DeviceParser( + '; *(AX5\\d+)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Bmobile', + '$1', + ), + DeviceParser( + '; *([Bb]q) ([^;/]+?);?(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'bq', + '$2', + ), + DeviceParser( + '; *(Maxwell [^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'bq', + '$1', + ), + DeviceParser( + '; *((?:B-Tab|B-TAB) ?\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Braun', + '$1', + ), + DeviceParser( + '; *(Broncho) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *CAPTIVA ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Captiva $1', + 'Captiva', + '$1', + ), + DeviceParser( + '; *(C771|CAL21|IS11CA)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Casio', + '$1', + ), + DeviceParser( + '; *(?:Cat|CAT) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Cat $1', + 'Cat', + '$1', + ), + DeviceParser( + '; *(?:Cat)(Nova.*?)(?: Build|\\) AppleWebKit)', + None, + 'Cat $1', + 'Cat', + '$1', + ), + DeviceParser( + '; *(INM8002KP|ADM8000KP_[AB])(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Cat', + 'Tablet PHOENIX 8.1J0', + ), + DeviceParser( + '; *(?:[Cc]elkon[ _\\*]|CELKON[ _\\*])([^;/\\)]+) ?(?:Build|;|\\))', + None, + '$1', + 'Celkon', + '$1', + ), + DeviceParser( + 'Build/(?:[Cc]elkon)+_?([^;/_\\)]+)', + None, + '$1', + 'Celkon', + '$1', + ), + DeviceParser( + '; *(CT)-?(\\d+)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Celkon', + '$1$2', + ), + DeviceParser( + '; *(A19|A19Q|A105|A107[^;/\\)]*) ?(?:Build|;|\\))', + None, + '$1', + 'Celkon', + '$1', + ), + DeviceParser( + '; *(TPC[0-9]{4,5})(?: Build|\\) AppleWebKit)', + None, + '$1', + 'ChangJia', + '$1', + ), + DeviceParser( + '; *(Cloudfone)[ _](Excite)([^ ][^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2 $3', + 'Cloudfone', + '$1 $2 $3', + ), + DeviceParser( + '; *(Excite|ICE)[ _](\\d+[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Cloudfone $1 $2', + 'Cloudfone', + 'Cloudfone $1 $2', + ), + DeviceParser( + '; *(Cloudfone|CloudPad)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Cloudfone', + '$1 $2', + ), + DeviceParser( + '; *((?:Aquila|Clanga|Rapax)[^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'Cmx', + '$1', + ), + DeviceParser( + '; *(?:CFW-|Kyros )?(MID[0-9]{4}(?:[ABC]|SR|TV)?)(\\(3G\\)-4G| GB 8K| 3G| 8K| GB)? *(?:Build|[;\\)])', + None, + 'CobyKyros $1$2', + 'CobyKyros', + '$1$2', + ), + DeviceParser( + '; *([^;/]*)Coolpad[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Coolpad', + '$1$2', + ), + DeviceParser( + '; *(CUBE[ _])?([KU][0-9]+ ?GT.*?|A5300)(?: Build|\\) AppleWebKit)', + 'i', + '$1$2', + 'Cube', + '$2', + ), + DeviceParser( + '; *CUBOT ([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'Cubot', + '$1', + ), + DeviceParser( + '; *(BOBBY)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'Cubot', + '$1', + ), + DeviceParser( + '; *(Dslide [^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Danew', + '$1', + ), + DeviceParser( + '; *(XCD)[ _]?(28|35)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1$2', + 'Dell', + '$1$2', + ), + DeviceParser( + '; *(001DL)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + 'Streak', + ), + DeviceParser( + '; *(?:Dell|DELL) (Streak)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + 'Streak', + ), + DeviceParser( + '; *(101DL|GS01|Streak Pro[^;/]*)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + 'Streak Pro', + ), + DeviceParser( + '; *([Ss]treak ?7)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + 'Streak 7', + ), + DeviceParser( + '; *(Mini-3iX)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + '$1', + ), + DeviceParser( + '; *(?:Dell|DELL)[ _](Aero|Venue|Thunder|Mini.*?|Streak[ _]Pro)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + '$1', + ), + DeviceParser( + '; *Dell[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + '$1', + ), + DeviceParser( + '; *Dell ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Dell $1', + 'Dell', + '$1', + ), + DeviceParser( + '; *(TA[CD]-\\d+[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Denver', + '$1', + ), + DeviceParser( + '; *(iP[789]\\d{2}(?:-3G)?|IP10\\d{2}(?:-8GB)?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Dex', + '$1', + ), + DeviceParser( + '; *(AirTab)[ _\\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'DNS', + '$1 $2', + ), + DeviceParser( + '; *(F\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Fujitsu', + '$1', + ), + DeviceParser( + '; *(HT-03A)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'HTC', + 'Magic', + ), + DeviceParser( + '; *(HT\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(L\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'LG', + '$1', + ), + DeviceParser( + '; *(N\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Nec', + '$1', + ), + DeviceParser( + '; *(P\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Panasonic', + '$1', + ), + DeviceParser( + '; *(SC\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Samsung', + '$1', + ), + DeviceParser( + '; *(SH\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Sharp', + '$1', + ), + DeviceParser( + '; *(SO\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'SonyEricsson', + '$1', + ), + DeviceParser( + '; *(T\\-0[12][^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Toshiba', + '$1', + ), + DeviceParser( + '; *(DOOV)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'DOOV', + '$2', + ), + DeviceParser( + '; *(Enot|ENOT)[ -]?([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Enot', + '$2', + ), + DeviceParser( + '; *[^;/]+ Build/(?:CROSS|Cross)+[ _\\-]([^\\)]+)', + None, + 'CROSS $1', + 'Evercoss', + 'Cross $1', + ), + DeviceParser( + '; *(CROSS|Cross)[ _\\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Evercoss', + 'Cross $2', + ), + DeviceParser( + '; *Explay[_ ](.+?)(?:[\\)]| Build)', + None, + '$1', + 'Explay', + '$1', + ), + DeviceParser( + '; *(IQ.*?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Fly', + '$1', + ), + DeviceParser( + '; *(Fly|FLY)[ _](IQ[^;]+?|F[34]\\d+[^;]*?);?(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Fly', + '$2', + ), + DeviceParser( + '; *(M532|Q572|FJL21)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Fujitsu', + '$1', + ), + DeviceParser( + '; *(G1)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Galapad', + '$1', + ), + DeviceParser( + '; *(Geeksphone) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(G[^F]?FIVE) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Gfive', + '$2', + ), + DeviceParser( + '; *(Gionee)[ _\\-]([^;/]+?)(?:/[^;/]+|)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'Gionee', + '$2', + ), + DeviceParser( + '; *(GN\\d+[A-Z]?|INFINITY_PASSION|Ctrl_V1)(?: Build|\\) AppleWebKit)', + None, + 'Gionee $1', + 'Gionee', + '$1', + ), + DeviceParser( + '; *(E3) Build/JOP40D', + None, + 'Gionee $1', + 'Gionee', + '$1', + ), + DeviceParser( + '\\sGIONEE[-\\s_](\\w*)', + 'i', + 'Gionee $1', + 'Gionee', + '$1', + ), + DeviceParser( + '; *((?:FONE|QUANTUM|INSIGNIA) \\d+[^;/]*|PLAYTAB)(?: Build|\\) AppleWebKit)', + None, + 'GoClever $1', + 'GoClever', + '$1', + ), + DeviceParser( + '; *GOCLEVER ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'GoClever $1', + 'GoClever', + '$1', + ), + DeviceParser( + '; *(Glass \\d+)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Google', + '$1', + ), + DeviceParser( + '; *(Pixel.*?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Google', + '$1', + ), + DeviceParser( + '; *(GSmart)[ -]([^/]+)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Gigabyte', + '$1 $2', + ), + DeviceParser( + '; *(imx5[13]_[^/]+)(?: Build|\\) AppleWebKit)', + None, + 'Freescale $1', + 'Freescale', + '$1', + ), + DeviceParser( + '; *Haier[ _\\-]([^/]+)(?: Build|\\) AppleWebKit)', + None, + 'Haier $1', + 'Haier', + '$1', + ), + DeviceParser( + '; *(PAD1016)(?: Build|\\) AppleWebKit)', + None, + 'Haipad $1', + 'Haipad', + '$1', + ), + DeviceParser( + '; *(M701|M7|M8|M9)(?: Build|\\) AppleWebKit)', + None, + 'Haipad $1', + 'Haipad', + '$1', + ), + DeviceParser( + '; *(SN\\d+T[^;\\)/]*)(?: Build|[;\\)])', + None, + 'Hannspree $1', + 'Hannspree', + '$1', + ), + DeviceParser( + 'Build/HCL ME Tablet ([^;\\)]+)[\\);]', + None, + 'HCLme $1', + 'HCLme', + '$1', + ), + DeviceParser( + '; *([^;\\/]+) Build/HCL', + None, + 'HCLme $1', + 'HCLme', + '$1', + ), + DeviceParser( + '; *(MID-?\\d{4}C[EM])(?: Build|\\) AppleWebKit)', + None, + 'Hena $1', + 'Hena', + '$1', + ), + DeviceParser( + '; *(EG\\d{2,}|HS-[^;/]+|MIRA[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Hisense $1', + 'Hisense', + '$1', + ), + DeviceParser( + '; *(andromax[^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Hisense $1', + 'Hisense', + '$1', + ), + DeviceParser( + '; *(?:AMAZE[ _](S\\d+)|(S\\d+)[ _]AMAZE)(?: Build|\\) AppleWebKit)', + None, + 'AMAZE $1$2', + 'hitech', + 'AMAZE $1$2', + ), + DeviceParser( + '; *(PlayBook)(?: Build|\\) AppleWebKit)', + None, + 'HP $1', + 'HP', + '$1', + ), + DeviceParser( + '; *HP ([^/]+)(?: Build|\\) AppleWebKit)', + None, + 'HP $1', + 'HP', + '$1', + ), + DeviceParser( + '; *([^/]+_tenderloin)(?: Build|\\) AppleWebKit)', + None, + 'HP TouchPad', + 'HP', + 'TouchPad', + ), + DeviceParser( + '; *(HUAWEI |Huawei-|)([UY][^;/]+) Build/(?:Huawei|HUAWEI)([UY][^\\);]+)\\)', + None, + '$1$2', + 'Huawei', + '$2', + ), + DeviceParser( + '; *([^;/]+) Build[/ ]Huawei(MT1-U06|[A-Z]+\\d+[^\\);]+)\\)', + None, + '$1', + 'Huawei', + '$2', + ), + DeviceParser( + '; *(S7|M860) Build', + None, + '$1', + 'Huawei', + '$1', + ), + DeviceParser( + '; *((?:HUAWEI|Huawei)[ \\-]?)(MediaPad) Build', + None, + '$1$2', + 'Huawei', + '$2', + ), + DeviceParser( + '; *((?:HUAWEI[ _]?|Huawei[ _]|)Ascend[ _])([^;/]+) Build', + None, + '$1$2', + 'Huawei', + '$2', + ), + DeviceParser( + '; *((?:HUAWEI|Huawei)[ _\\-]?)((?:G700-|MT-)[^;/]+) Build', + None, + '$1$2', + 'Huawei', + '$2', + ), + DeviceParser( + '; *((?:HUAWEI|Huawei)[ _\\-]?)([^;/]+) Build', + None, + '$1$2', + 'Huawei', + '$2', + ), + DeviceParser( + '; *(MediaPad[^;]+|SpringBoard) Build/Huawei', + None, + '$1', + 'Huawei', + '$1', + ), + DeviceParser( + '; *([^;]+) Build/(?:Huawei|HUAWEI)', + None, + '$1', + 'Huawei', + '$1', + ), + DeviceParser( + '; *([Uu])([89]\\d{3}) Build', + None, + '$1$2', + 'Huawei', + 'U$2', + ), + DeviceParser( + '; *(?:Ideos |IDEOS )(S7) Build', + None, + 'Huawei Ideos$1', + 'Huawei', + 'Ideos$1', + ), + DeviceParser( + '; *(?:Ideos |IDEOS )([^;/]+\\s*|\\s*)Build', + None, + 'Huawei Ideos$1', + 'Huawei', + 'Ideos$1', + ), + DeviceParser( + '; *(Orange Daytona|Pulse|Pulse Mini|Vodafone 858|C8500|C8600|C8650|C8660|Nexus 6P|ATH-.+?) Build[/ ]', + None, + 'Huawei $1', + 'Huawei', + '$1', + ), + DeviceParser( + '; *((?:[A-Z]{3})\\-L[A-Za0-9]{2})[\\)]', + None, + 'Huawei $1', + 'Huawei', + '$1', + ), + DeviceParser( + '; *HTC[ _]([^;]+); Windows Phone', + None, + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(?:HTC[ _/])+([^ _/]+)(?:[/\\\\]1\\.0 | V|/| +)\\d+\\.\\d[\\d\\.]*(?: *Build|\\))', + None, + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(?:HTC[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)|)(?:[/\\\\]1\\.0 | V|/| +)\\d+\\.\\d[\\d\\.]*(?: *Build|\\))', + None, + 'HTC $1 $2', + 'HTC', + '$1 $2', + ), + DeviceParser( + '; *(?:HTC[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+)|)|)(?:[/\\\\]1\\.0 | V|/| +)\\d+\\.\\d[\\d\\.]*(?: *Build|\\))', + None, + 'HTC $1 $2 $3', + 'HTC', + '$1 $2 $3', + ), + DeviceParser( + '; *(?:HTC[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+)|)|)|)(?:[/\\\\]1\\.0 | V|/| +)\\d+\\.\\d[\\d\\.]*(?: *Build|\\))', + None, + 'HTC $1 $2 $3 $4', + 'HTC', + '$1 $2 $3 $4', + ), + DeviceParser( + '; *(?:(?:HTC|htc)(?:_blocked|)[ _/])+([^ _/;]+)(?: *Build|[;\\)]| - )', + None, + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(?:(?:HTC|htc)(?:_blocked|)[ _/])+([^ _/]+)(?:[ _/]([^ _/;\\)]+)|)(?: *Build|[;\\)]| - )', + None, + 'HTC $1 $2', + 'HTC', + '$1 $2', + ), + DeviceParser( + '; *(?:(?:HTC|htc)(?:_blocked|)[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/;\\)]+)|)|)(?: *Build|[;\\)]| - )', + None, + 'HTC $1 $2 $3', + 'HTC', + '$1 $2 $3', + ), + DeviceParser( + '; *(?:(?:HTC|htc)(?:_blocked|)[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ /;]+)|)|)|)(?: *Build|[;\\)]| - )', + None, + 'HTC $1 $2 $3 $4', + 'HTC', + '$1 $2 $3 $4', + ), + DeviceParser( + 'HTC Streaming Player [^\\/]*/[^\\/]*/ htc_([^/]+) /', + None, + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + '(?:[;,] *|^)(?:htccn_chs-|)HTC[ _-]?([^;]+?)(?: *Build|clay|Android|-?Mozilla| Opera| Profile| UNTRUSTED|[;/\\(\\)]|$)', + 'i', + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(A6277|ADR6200|ADR6300|ADR6350|ADR6400[A-Z]*|ADR6425[A-Z]*|APX515CKT|ARIA|Desire[^_ ]*|Dream|EndeavorU|Eris|Evo|Flyer|HD2|Hero|HERO200|Hero CDMA|HTL21|Incredible|Inspire[A-Z0-9]*|Legend|Liberty|Nexus ?(?:One|HD2)|One|One S C2|One[ _]?(?:S|V|X\\+?)\\w*|PC36100|PG06100|PG86100|S31HT|Sensation|Wildfire)(?: Build|[/;\\(\\)])', + 'i', + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(ADR6200|ADR6400L|ADR6425LVW|Amaze|DesireS?|EndeavorU|Eris|EVO|Evo\\d[A-Z]+|HD2|IncredibleS?|Inspire[A-Z0-9]*|Inspire[A-Z0-9]*|Sensation[A-Z0-9]*|Wildfire)[ _-](.+?)(?:[/;\\)]|Build|MIUI|1\\.0)', + 'i', + 'HTC $1 $2', + 'HTC', + '$1 $2', + ), + DeviceParser( + '; *HYUNDAI (T\\d[^/]*)(?: Build|\\) AppleWebKit)', + None, + 'Hyundai $1', + 'Hyundai', + '$1', + ), + DeviceParser( + '; *HYUNDAI ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Hyundai $1', + 'Hyundai', + '$1', + ), + DeviceParser( + '; *(X700|Hold X|MB-6900)(?: Build|\\) AppleWebKit)', + None, + 'Hyundai $1', + 'Hyundai', + '$1', + ), + DeviceParser( + '; *(?:iBall[ _\\-]|)(Andi)[ _]?(\\d[^;/]*)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'iBall', + '$1 $2', + ), + DeviceParser( + '; *(IBall)(?:[ _]([^;/]+?)|)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'iBall', + '$2', + ), + DeviceParser( + '; *(NT-\\d+[^ ;/]*|Net[Tt]AB [^;/]+|Mercury [A-Z]+|iconBIT)(?: S/N:[^;/]+|)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'IconBIT', + '$1', + ), + DeviceParser( + '; *(IMO)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'IMO', + '$2', + ), + DeviceParser( + '; *i-?mobile[ _]([^/]+)(?: Build|\\) AppleWebKit)', + 'i', + 'i-mobile $1', + 'imobile', + '$1', + ), + DeviceParser( + '; *(i-(?:style|note)[^/]*)(?: Build|\\) AppleWebKit)', + 'i', + 'i-mobile $1', + 'imobile', + '$1', + ), + DeviceParser( + '; *(ImPAD) ?(\\d+(?:.)*?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Impression', + '$1 $2', + ), + DeviceParser( + '; *(Infinix)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Infinix', + '$2', + ), + DeviceParser( + '; *(Informer)[ \\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Informer', + '$2', + ), + DeviceParser( + '; *(TAB) ?([78][12]4)(?: Build|\\) AppleWebKit)', + None, + 'Intenso $1', + 'Intenso', + '$1 $2', + ), + DeviceParser( + '; *(?:Intex[ _]|)(AQUA|Aqua)([ _\\.\\-])([^;/]+?) *(?:Build|;)', + None, + '$1$2$3', + 'Intex', + '$1 $3', + ), + DeviceParser( + '; *(?:INTEX|Intex)(?:[_ ]([^\\ _;/]+))(?:[_ ]([^\\ _;/]+)|) *(?:Build|;)', + None, + '$1 $2', + 'Intex', + '$1 $2', + ), + DeviceParser( + '; *([iI]Buddy)[ _]?(Connect)(?:_|\\?_| |)([^;/]*) *(?:Build|;)', + None, + '$1 $2 $3', + 'Intex', + 'iBuddy $2 $3', + ), + DeviceParser( + '; *(I-Buddy)[ _]([^;/]+?) *(?:Build|;)', + None, + '$1 $2', + 'Intex', + 'iBuddy $2', + ), + DeviceParser( + '; *(iOCEAN) ([^/]+)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'iOCEAN', + '$2', + ), + DeviceParser( + '; *(TP\\d+(?:\\.\\d+|)\\-\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'ionik $1', + 'ionik', + '$1', + ), + DeviceParser( + '; *(M702pro)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Iru', + '$1', + ), + DeviceParser( + '; *(DE88Plus|MD70)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Ivio', + '$1', + ), + DeviceParser( + '; *IVIO[_\\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Ivio', + '$1', + ), + DeviceParser( + '; *(TPC-\\d+|JAY-TECH)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Jaytech', + '$1', + ), + DeviceParser( + '; *(JY-[^;/]+|G[234]S?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Jiayu', + '$1', + ), + DeviceParser( + '; *(JXD)[ _\\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'JXD', + '$2', + ), + DeviceParser( + '; *Karbonn[ _]?([^;/]+) *(?:Build|;)', + 'i', + '$1', + 'Karbonn', + '$1', + ), + DeviceParser( + '; *([^;]+) Build/Karbonn', + None, + '$1', + 'Karbonn', + '$1', + ), + DeviceParser( + '; *(A11|A39|A37|A34|ST8|ST10|ST7|Smart Tab3|Smart Tab2|Titanium S\\d) +Build', + None, + '$1', + 'Karbonn', + '$1', + ), + DeviceParser( + '; *(IS01|IS03|IS05|IS\\d{2}SH)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Sharp', + '$1', + ), + DeviceParser( + '; *(IS04)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Regza', + '$1', + ), + DeviceParser( + '; *(IS06|IS\\d{2}PT)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Pantech', + '$1', + ), + DeviceParser( + '; *(IS11S)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'SonyEricsson', + 'Xperia Acro', + ), + DeviceParser( + '; *(IS11CA)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Casio', + 'GzOne $1', + ), + DeviceParser( + '; *(IS11LG)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'LG', + 'Optimus X', + ), + DeviceParser( + '; *(IS11N)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Medias', + '$1', + ), + DeviceParser( + '; *(IS11PT)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Pantech', + 'MIRACH', + ), + DeviceParser( + '; *(IS12F)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Fujitsu', + 'Arrows ES', + ), + DeviceParser( + '; *(IS12M)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Motorola', + 'XT909', + ), + DeviceParser( + '; *(IS12S)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'SonyEricsson', + 'Xperia Acro HD', + ), + DeviceParser( + '; *(ISW11F)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Fujitsu', + 'Arrowz Z', + ), + DeviceParser( + '; *(ISW11HT)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'HTC', + 'EVO', + ), + DeviceParser( + '; *(ISW11K)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Kyocera', + 'DIGNO', + ), + DeviceParser( + '; *(ISW11M)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Motorola', + 'Photon', + ), + DeviceParser( + '; *(ISW11SC)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Samsung', + 'GALAXY S II WiMAX', + ), + DeviceParser( + '; *(ISW12HT)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'HTC', + 'EVO 3D', + ), + DeviceParser( + '; *(ISW13HT)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'HTC', + 'J', + ), + DeviceParser( + '; *(ISW?[0-9]{2}[A-Z]{0,2})(?: Build|\\) AppleWebKit)', + None, + '$1', + 'KDDI', + '$1', + ), + DeviceParser( + '; *(INFOBAR [^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'KDDI', + '$1', + ), + DeviceParser( + '; *(JOYPAD|Joypad)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Kingcom', + '$1 $2', + ), + DeviceParser( + '; *(Vox|VOX|Arc|K080)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'Kobo', + '$1', + ), + DeviceParser( + '\\b(Kobo Touch)\\b', + None, + '$1', + 'Kobo', + '$1', + ), + DeviceParser( + '; *(K-Touch)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'Ktouch', + '$2', + ), + DeviceParser( + '; *((?:EV|KM)-S\\d+[A-Z]?)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'KTtech', + '$1', + ), + DeviceParser( + '; *(Zio|Hydro|Torque|Event|EVENT|Echo|Milano|Rise|URBANO PROGRESSO|WX04K|WX06K|WX10K|KYL21|101K|C5[12]\\d{2})(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Kyocera', + '$1', + ), + DeviceParser( + '; *(?:LAVA[ _]|)IRIS[ _\\-]?([^/;\\)]+) *(?:;|\\)|Build)', + 'i', + 'Iris $1', + 'Lava', + 'Iris $1', + ), + DeviceParser( + '; *LAVA[ _]([^;/]+) Build', + None, + '$1', + 'Lava', + '$1', + ), + DeviceParser( + '; *(?:(Aspire A1)|(?:LEMON|Lemon)[ _]([^;/]+))_?(?: Build|\\) AppleWebKit)', + None, + 'Lemon $1$2', + 'Lemon', + '$1$2', + ), + DeviceParser( + '; *(TAB-1012)(?: Build|\\) AppleWebKit)', + None, + 'Lenco $1', + 'Lenco', + '$1', + ), + DeviceParser( + '; Lenco ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Lenco $1', + 'Lenco', + '$1', + ), + DeviceParser( + '; *(A1_07|A2107A-H|S2005A-H|S1-37AH0) Build', + None, + '$1', + 'Lenovo', + '$1', + ), + DeviceParser( + '; *(Idea[Tp]ab)[ _]([^;/]+);? Build', + None, + 'Lenovo $1 $2', + 'Lenovo', + '$1 $2', + ), + DeviceParser( + '; *(Idea(?:Tab|pad)) ?([^;/]+) Build', + None, + 'Lenovo $1 $2', + 'Lenovo', + '$1 $2', + ), + DeviceParser( + '; *(ThinkPad) ?(Tablet) Build/', + None, + 'Lenovo $1 $2', + 'Lenovo', + '$1 $2', + ), + DeviceParser( + '; *(?:LNV-|)(?:=?[Ll]enovo[ _\\-]?|LENOVO[ _])(.+?)(?:Build|[;/\\)])', + None, + 'Lenovo $1', + 'Lenovo', + '$1', + ), + DeviceParser( + '[;,] (?:Vodafone |)(SmartTab) ?(II) ?(\\d+) Build/', + None, + 'Lenovo $1 $2 $3', + 'Lenovo', + '$1 $2 $3', + ), + DeviceParser( + '; *(?:Ideapad |)K1 Build/', + None, + 'Lenovo Ideapad K1', + 'Lenovo', + 'Ideapad K1', + ), + DeviceParser( + '; *(3GC101|3GW10[01]|A390) Build/', + None, + '$1', + 'Lenovo', + '$1', + ), + DeviceParser( + '\\b(?:Lenovo|LENOVO)+[ _\\-]?([^,;:/ ]+)', + None, + 'Lenovo $1', + 'Lenovo', + '$1', + ), + DeviceParser( + '; *(MFC\\d+)[A-Z]{2}([^;,/]*),?(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Lexibook', + '$1$2', + ), + DeviceParser( + '; *(E[34][0-9]{2}|LS[6-8][0-9]{2}|VS[6-9][0-9]+[^;/]+|Nexus 4|Nexus 5X?|GT540f?|Optimus (?:2X|G|4X HD)|OptimusX4HD) *(?:Build|;)', + None, + '$1', + 'LG', + '$1', + ), + DeviceParser( + '[;:] *(L-\\d+[A-Z]|LGL\\d+[A-Z]?)(?:/V\\d+|) *(?:Build|[;\\)])', + None, + '$1', + 'LG', + '$1', + ), + DeviceParser( + '; *(LG-)([A-Z]{1,2}\\d{2,}[^,;/\\)\\(]*?)(?:Build| V\\d+|[,;/\\)\\(]|$)', + None, + '$1$2', + 'LG', + '$2', + ), + DeviceParser( + '; *(LG[ \\-]|LG)([^;/]+)[;/]? Build', + None, + '$1$2', + 'LG', + '$2', + ), + DeviceParser( + '^(LG)-([^;/]+)/ Mozilla/.*; Android', + None, + '$1 $2', + 'LG', + '$2', + ), + DeviceParser( + '(Web0S); Linux/(SmartTV)', + None, + 'LG $1 $2', + 'LG', + '$1 $2', + ), + DeviceParser( + '; *((?:SMB|smb)[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Malata', + '$1', + ), + DeviceParser( + '; *(?:Malata|MALATA) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Malata', + '$1', + ), + DeviceParser( + '; *(MS[45][0-9]{3}|MID0[568][NS]?|MID[1-9]|MID[78]0[1-9]|MID970[1-9]|MID100[1-9])(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Manta', + '$1', + ), + DeviceParser( + '; *(M1052|M806|M9000|M9100|M9701|MID100|MID120|MID125|MID130|MID135|MID140|MID701|MID710|MID713|MID727|MID728|MID731|MID732|MID733|MID735|MID736|MID737|MID760|MID800|MID810|MID820|MID830|MID833|MID835|MID860|MID900|MID930|MID933|MID960|MID980)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Match', + '$1', + ), + DeviceParser( + '; *(GenxDroid7|MSD7.*?|AX\\d.*?|Tab 701|Tab 722)(?: Build|\\) AppleWebKit)', + None, + 'Maxx $1', + 'Maxx', + '$1', + ), + DeviceParser( + '; *(M-PP[^;/]+|PhonePad ?\\d{2,}[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Mediacom $1', + 'Mediacom', + '$1', + ), + DeviceParser( + '; *(M-MP[^;/]+|SmartPad ?\\d{2,}[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Mediacom $1', + 'Mediacom', + '$1', + ), + DeviceParser( + '; *(?:MD_|)LIFETAB[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Medion Lifetab $1', + 'Medion', + 'Lifetab $1', + ), + DeviceParser( + '; *MEDION ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Medion $1', + 'Medion', + '$1', + ), + DeviceParser( + '; *(M030|M031|M035|M040|M065|m9)(?: Build|\\) AppleWebKit)', + None, + 'Meizu $1', + 'Meizu', + '$1', + ), + DeviceParser( + '; *(?:meizu_|MEIZU )(.+?) *(?:Build|[;\\)])', + None, + 'Meizu $1', + 'Meizu', + '$1', + ), + DeviceParser( + '; *(?:Micromax[ _](A111|A240)|(A111|A240)) Build', + 'i', + 'Micromax $1$2', + 'Micromax', + '$1$2', + ), + DeviceParser( + '; *Micromax[ _](A\\d{2,3}[^;/]*) Build', + 'i', + 'Micromax $1', + 'Micromax', + '$1', + ), + DeviceParser( + '; *(A\\d{2}|A[12]\\d{2}|A90S|A110Q) Build', + 'i', + 'Micromax $1', + 'Micromax', + '$1', + ), + DeviceParser( + '; *Micromax[ _](P\\d{3}[^;/]*) Build', + 'i', + 'Micromax $1', + 'Micromax', + '$1', + ), + DeviceParser( + '; *(P\\d{3}|P\\d{3}\\(Funbook\\)) Build', + 'i', + 'Micromax $1', + 'Micromax', + '$1', + ), + DeviceParser( + '; *(MITO)[ _\\-]?([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'Mito', + '$2', + ), + DeviceParser( + '; *(Cynus)[ _](F5|T\\d|.+?) *(?:Build|[;/\\)])', + 'i', + '$1 $2', + 'Mobistel', + '$1 $2', + ), + DeviceParser( + '; *(MODECOM |)(FreeTab) ?([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1$2 $3', + 'Modecom', + '$2 $3', + ), + DeviceParser( + '; *(MODECOM )([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'Modecom', + '$2', + ), + DeviceParser( + '; *(MZ\\d{3}\\+?|MZ\\d{3} 4G|Xoom|XOOM[^;/]*) Build', + None, + 'Motorola $1', + 'Motorola', + '$1', + ), + DeviceParser( + '; *(Milestone )(XT[^;/]*) Build', + None, + 'Motorola $1$2', + 'Motorola', + '$2', + ), + DeviceParser( + '; *(Motoroi ?x|Droid X|DROIDX) Build', + 'i', + 'Motorola $1', + 'Motorola', + 'DROID X', + ), + DeviceParser( + '; *(Droid[^;/]*|DROID[^;/]*|Milestone[^;/]*|Photon|Triumph|Devour|Titanium) Build', + None, + 'Motorola $1', + 'Motorola', + '$1', + ), + DeviceParser( + '; *(A555|A85[34][^;/]*|A95[356]|ME[58]\\d{2}\\+?|ME600|ME632|ME722|MB\\d{3}\\+?|MT680|MT710|MT870|MT887|MT917|WX435|WX453|WX44[25]|XT\\d{3,4}[A-Z\\+]*|CL[iI]Q|CL[iI]Q XT) Build', + None, + '$1', + 'Motorola', + '$1', + ), + DeviceParser( + '; *(Motorola MOT-|Motorola[ _\\-]|MOT\\-?)([^;/]+) Build', + None, + '$1$2', + 'Motorola', + '$2', + ), + DeviceParser( + '; *(Moto[_ ]?|MOT\\-)([^;/]+) Build', + None, + '$1$2', + 'Motorola', + '$2', + ), + DeviceParser( + '; *((?:MP[DQ]C|MPG\\d{1,4}|MP\\d{3,4}|MID(?:(?:10[234]|114|43|7[247]|8[24]|7)C|8[01]1))[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Mpman', + '$1', + ), + DeviceParser( + '; *(?:MSI[ _]|)(Primo\\d+|Enjoy[ _\\-][^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'Msi', + '$1', + ), + DeviceParser( + '; *Multilaser[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Multilaser', + '$1', + ), + DeviceParser( + '; *(My)[_]?(Pad)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2 $3', + 'MyPhone', + '$1$2 $3', + ), + DeviceParser( + '; *(My)\\|?(Phone)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2 $3', + 'MyPhone', + '$3', + ), + DeviceParser( + '; *(A\\d+)[ _](Duo|)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'MyPhone', + '$1 $2', + ), + DeviceParser( + '; *(myTab[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Mytab', + '$1', + ), + DeviceParser( + '; *(NABI2?-)([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Nabi', + '$2', + ), + DeviceParser( + '; *(N-\\d+[CDE])(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Nec', + '$1', + ), + DeviceParser( + '; ?(NEC-)(.*?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Nec', + '$2', + ), + DeviceParser( + '; *(LT-NA7)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Nec', + 'Lifetouch Note', + ), + DeviceParser( + '; *(NXM\\d+[A-Za-z0-9_]*|Next\\d[A-Za-z0-9_ \\-]*|NEXT\\d[A-Za-z0-9_ \\-]*|Nextbook [A-Za-z0-9_ ]*|DATAM803HC|M805)(?: Build|[\\);])', + None, + '$1', + 'Nextbook', + '$1', + ), + DeviceParser( + '; *(Nokia)([ _\\-]*)([^;/]*) Build', + 'i', + '$1$2$3', + 'Nokia', + '$3', + ), + DeviceParser( + '; *(Nook ?|Barnes & Noble Nook |BN )([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Nook', + '$2', + ), + DeviceParser( + '; *(NOOK |)(BNRV200|BNRV200A|BNTV250|BNTV250A|BNTV400|BNTV600|LogicPD Zoom2)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Nook', + '$2', + ), + DeviceParser( + '; Build/(Nook)', + None, + '$1', + 'Nook', + 'Tablet', + ), + DeviceParser( + '; *(OP110|OliPad[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Olivetti $1', + 'Olivetti', + '$1', + ), + DeviceParser( + '; *OMEGA[ _\\-](MID[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Omega $1', + 'Omega', + '$1', + ), + DeviceParser( + '^(MID7500|MID\\d+) Mozilla/5\\.0 \\(iPad;', + None, + 'Omega $1', + 'Omega', + '$1', + ), + DeviceParser( + '; *((?:CIUS|cius)[^;/]*)(?: Build|\\) AppleWebKit)', + None, + 'Openpeak $1', + 'Openpeak', + '$1', + ), + DeviceParser( + '; *(Find ?(?:5|7a)|R8[012]\\d{1,2}|T703\\d?|U70\\d{1,2}T?|X90\\d{1,2})(?: Build|\\) AppleWebKit)', + None, + 'Oppo $1', + 'Oppo', + '$1', + ), + DeviceParser( + '; *OPPO ?([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Oppo $1', + 'Oppo', + '$1', + ), + DeviceParser( + '; *(?:Odys\\-|ODYS\\-|ODYS )([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Odys $1', + 'Odys', + '$1', + ), + DeviceParser( + '; *(SELECT) ?(7)(?: Build|\\) AppleWebKit)', + None, + 'Odys $1 $2', + 'Odys', + '$1 $2', + ), + DeviceParser( + '; *(PEDI)_(PLUS)_(W)(?: Build|\\) AppleWebKit)', + None, + 'Odys $1 $2 $3', + 'Odys', + '$1 $2 $3', + ), + DeviceParser( + '; *(AEON|BRAVIO|FUSION|FUSION2IN1|Genio|EOS10|IEOS[^;/]*|IRON|Loox|LOOX|LOOX Plus|Motion|NOON|NOON_PRO|NEXT|OPOS|PEDI[^;/]*|PRIME[^;/]*|STUDYTAB|TABLO|Tablet-PC-4|UNO_X8|XELIO[^;/]*|Xelio ?\\d+ ?[Pp]ro|XENO10|XPRESS PRO)(?: Build|\\) AppleWebKit)', + None, + 'Odys $1', + 'Odys', + '$1', + ), + DeviceParser( + '; (ONE [a-zA-Z]\\d+)(?: Build|\\) AppleWebKit)', + None, + 'OnePlus $1', + 'OnePlus', + '$1', + ), + DeviceParser( + '; (ONEPLUS [a-zA-Z]\\d+)(?: Build|\\) AppleWebKit)', + None, + 'OnePlus $1', + 'OnePlus', + '$1', + ), + DeviceParser( + '; *(TP-\\d+)(?: Build|\\) AppleWebKit)', + None, + 'Orion $1', + 'Orion', + '$1', + ), + DeviceParser( + '; *(G100W?)(?: Build|\\) AppleWebKit)', + None, + 'PackardBell $1', + 'PackardBell', + '$1', + ), + DeviceParser( + '; *(Panasonic)[_ ]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(FZ-A1B|JT-B1)(?: Build|\\) AppleWebKit)', + None, + 'Panasonic $1', + 'Panasonic', + '$1', + ), + DeviceParser( + '; *(dL1|DL1)(?: Build|\\) AppleWebKit)', + None, + 'Panasonic $1', + 'Panasonic', + '$1', + ), + DeviceParser( + '; *(SKY[ _]|)(IM\\-[AT]\\d{3}[^;/]+).* Build/', + None, + 'Pantech $1$2', + 'Pantech', + '$1$2', + ), + DeviceParser( + '; *((?:ADR8995|ADR910L|ADR930L|ADR930VW|PTL21|P8000)(?: 4G|)) Build/', + None, + '$1', + 'Pantech', + '$1', + ), + DeviceParser( + '; *Pantech([^;/]+).* Build/', + None, + 'Pantech $1', + 'Pantech', + '$1', + ), + DeviceParser( + '; *(papyre)[ _\\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1 $2', + 'Papyre', + '$2', + ), + DeviceParser( + '; *(?:Touchlet )?(X10\\.[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Pearl $1', + 'Pearl', + '$1', + ), + DeviceParser( + '; PHICOMM (i800)(?: Build|\\) AppleWebKit)', + None, + 'Phicomm $1', + 'Phicomm', + '$1', + ), + DeviceParser( + '; PHICOMM ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Phicomm $1', + 'Phicomm', + '$1', + ), + DeviceParser( + '; *(FWS\\d{3}[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Phicomm $1', + 'Phicomm', + '$1', + ), + DeviceParser( + '; *(D633|D822|D833|T539|T939|V726|W335|W336|W337|W3568|W536|W5510|W626|W632|W6350|W6360|W6500|W732|W736|W737|W7376|W820|W832|W8355|W8500|W8510|W930)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Philips', + '$1', + ), + DeviceParser( + '; *(?:Philips|PHILIPS)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Philips $1', + 'Philips', + '$1', + ), + DeviceParser( + 'Android 4\\..*; *(M[12356789]|U[12368]|S[123])\\ ?(pro)?(?: Build|\\) AppleWebKit)', + None, + 'Pipo $1$2', + 'Pipo', + '$1$2', + ), + DeviceParser( + '; *(MOMO[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Ployer', + '$1', + ), + DeviceParser( + '; *(?:Polaroid[ _]|)((?:MIDC\\d{3,}|PMID\\d{2,}|PTAB\\d{3,})[^;/]*?)(\\/[^;/]*|)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Polaroid', + '$1', + ), + DeviceParser( + '; *(?:Polaroid )(Tablet)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Polaroid', + '$1', + ), + DeviceParser( + '; *(POMP)[ _\\-](.+?) *(?:Build|[;/\\)])', + None, + '$1 $2', + 'Pomp', + '$2', + ), + DeviceParser( + '; *(TB07STA|TB10STA|TB07FTA|TB10FTA)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Positivo', + '$1', + ), + DeviceParser( + '; *(?:Positivo |)((?:YPY|Ypy)[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Positivo', + '$1', + ), + DeviceParser( + '; *(MOB-[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'POV', + '$1', + ), + DeviceParser( + '; *POV[ _\\-]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'POV $1', + 'POV', + '$1', + ), + DeviceParser( + '; *((?:TAB-PLAYTAB|TAB-PROTAB|PROTAB|PlayTabPro|Mobii[ _\\-]|TAB-P)[^;/]*)(?: Build|\\) AppleWebKit)', + None, + 'POV $1', + 'POV', + '$1', + ), + DeviceParser( + '; *(?:Prestigio |)((?:PAP|PMP)\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Prestigio $1', + 'Prestigio', + '$1', + ), + DeviceParser( + '; *(PLT[0-9]{4}.*?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Proscan', + '$1', + ), + DeviceParser( + '; *(A2|A5|A8|A900)_?(Classic|)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Qmobile', + '$1 $2', + ), + DeviceParser( + '; *(Q[Mm]obile)_([^_]+)_([^_]+?)(?: Build|\\) AppleWebKit)', + None, + 'Qmobile $2 $3', + 'Qmobile', + '$2 $3', + ), + DeviceParser( + '; *(Q\\-?[Mm]obile)[_ ](A[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Qmobile $2', + 'Qmobile', + '$2', + ), + DeviceParser( + '; *(Q\\-Smart)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Qmobilevn', + '$2', + ), + DeviceParser( + '; *(Q\\-?[Mm]obile)[ _\\-](S[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Qmobilevn', + '$2', + ), + DeviceParser( + '; *(TA1013)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Quanta', + '$1', + ), + DeviceParser( + '; (RCT\\w+)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'RCA', + '$1', + ), + DeviceParser( + '; RCA (\\w+)(?: Build|\\) AppleWebKit)', + None, + 'RCA $1', + 'RCA', + '$1', + ), + DeviceParser( + '; *(RK\\d+),?(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Rockchip', + '$1', + ), + DeviceParser( + ' Build/(RK\\d+)', + None, + '$1', + 'Rockchip', + '$1', + ), + DeviceParser( + '; *(SAMSUNG |Samsung |)((?:Galaxy (?:Note II|S\\d)|GT-I9082|GT-I9205|GT-N7\\d{3}|SM-N9005)[^;/]*)\\/?[^;/]* Build/', + None, + 'Samsung $1$2', + 'Samsung', + '$2', + ), + DeviceParser( + '; *(Google |)(Nexus [Ss](?: 4G|)) Build/', + None, + 'Samsung $1$2', + 'Samsung', + '$2', + ), + DeviceParser( + '; *(SAMSUNG |Samsung )([^\\/]*)\\/[^ ]* Build/', + None, + 'Samsung $2', + 'Samsung', + '$2', + ), + DeviceParser( + '; *(Galaxy(?: Ace| Nexus| S ?II+|Nexus S| with MCR 1.2| Mini Plus 4G|)) Build/', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '; *(SAMSUNG[ _\\-]|)(?:SAMSUNG[ _\\-])([^;/]+) Build', + None, + 'Samsung $2', + 'Samsung', + '$2', + ), + DeviceParser( + '; *(SAMSUNG-|)(GT\\-[BINPS]\\d{4}[^\\/]*)(\\/[^ ]*) Build', + None, + 'Samsung $1$2$3', + 'Samsung', + '$2', + ), + DeviceParser( + '(?:; *|^)((?:GT\\-[BIiNPS]\\d{4}|I9\\d{2}0[A-Za-z\\+]?\\b)[^;/\\)]*?)(?:Build|Linux|MIUI|[;/\\)])', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '; (SAMSUNG-)([A-Za-z0-9\\-]+).* Build/', + None, + 'Samsung $1$2', + 'Samsung', + '$2', + ), + DeviceParser( + '; *((?:SCH|SGH|SHV|SHW|SPH|SC|SM)\\-[A-Za-z0-9 ]+)(/?[^ ]*|) Build', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '; *((?:SC)\\-[A-Za-z0-9 ]+)(/?[^ ]*|)\\)', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + ' ((?:SCH)\\-[A-Za-z0-9 ]+)(/?[^ ]*|) Build', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '; *(Behold ?(?:2|II)|YP\\-G[^;/]+|EK-GC100|SCL21|I9300) Build', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '; *((?:SCH|SGH|SHV|SHW|SPH|SC|SM)\\-[A-Za-z0-9]{5,6})[\\)]', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '; *(SH\\-?\\d\\d[^;/]+|SBM\\d[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Sharp', + '$1', + ), + DeviceParser( + '; *(SHARP[ -])([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Sharp', + '$2', + ), + DeviceParser( + '; *(SPX[_\\-]\\d[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Simvalley', + '$1', + ), + DeviceParser( + '; *(SX7\\-PEARL\\.GmbH)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Simvalley', + '$1', + ), + DeviceParser( + '; *(SP[T]?\\-\\d{2}[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Simvalley', + '$1', + ), + DeviceParser( + '; *(SK\\-.*?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'SKtelesys', + '$1', + ), + DeviceParser( + '; *(?:SKYTEX|SX)-([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Skytex', + '$1', + ), + DeviceParser( + '; *(IMAGINE [^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Skytex', + '$1', + ), + DeviceParser( + '; *(SmartQ) ?([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(WF7C|WF10C|SBT[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Smartbitt', + '$1', + ), + DeviceParser( + '; *(SBM(?:003SH|005SH|006SH|007SH|102SH)) Build', + None, + '$1', + 'Sharp', + '$1', + ), + DeviceParser( + '; *(003P|101P|101P11C|102P) Build', + None, + '$1', + 'Panasonic', + '$1', + ), + DeviceParser( + '; *(00\\dZ) Build/', + None, + '$1', + 'ZTE', + '$1', + ), + DeviceParser( + '; HTC(X06HT) Build', + None, + '$1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(001HT|X06HT) Build', + None, + '$1', + 'HTC', + '$1', + ), + DeviceParser( + '; *(201M) Build', + None, + '$1', + 'Motorola', + 'XT902', + ), + DeviceParser( + '; *(ST\\d{4}.*)Build/ST', + None, + 'Trekstor $1', + 'Trekstor', + '$1', + ), + DeviceParser( + '; *(ST\\d{4}.*?)(?: Build|\\) AppleWebKit)', + None, + 'Trekstor $1', + 'Trekstor', + '$1', + ), + DeviceParser( + '; *(Sony ?Ericsson ?)([^;/]+) Build', + None, + '$1$2', + 'SonyEricsson', + '$2', + ), + DeviceParser( + '; *((?:SK|ST|E|X|LT|MK|MT|WT)\\d{2}[a-z0-9]*(?:-o|)|R800i|U20i) Build', + None, + '$1', + 'SonyEricsson', + '$1', + ), + DeviceParser( + '; *(Xperia (?:A8|Arc|Acro|Active|Live with Walkman|Mini|Neo|Play|Pro|Ray|X\\d+)[^;/]*) Build', + 'i', + '$1', + 'SonyEricsson', + '$1', + ), + DeviceParser( + '; Sony (Tablet[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Sony $1', + 'Sony', + '$1', + ), + DeviceParser( + '; Sony ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Sony $1', + 'Sony', + '$1', + ), + DeviceParser( + '; *(Sony)([A-Za-z0-9\\-]+)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(Xperia [^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Sony', + '$1', + ), + DeviceParser( + '; *(C(?:1[0-9]|2[0-9]|53|55|6[0-9])[0-9]{2}|D[25]\\d{3}|D6[56]\\d{2})(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Sony', + '$1', + ), + DeviceParser( + '; *(SGP\\d{3}|SGPT\\d{2})(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Sony', + '$1', + ), + DeviceParser( + '; *(NW-Z1000Series)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Sony', + '$1', + ), + DeviceParser( + 'PLAYSTATION 3', + None, + 'PlayStation 3', + 'Sony', + 'PlayStation 3', + ), + DeviceParser( + '(PlayStation (?:Portable|Vita|\\d+))', + None, + '$1', + 'Sony', + '$1', + ), + DeviceParser( + '; *((?:CSL_Spice|Spice|SPICE|CSL)[ _\\-]?|)([Mm][Ii])([ _\\-]|)(\\d{3}[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1$2$3$4', + 'Spice', + 'Mi$4', + ), + DeviceParser( + '; *(Sprint )(.+?) *(?:Build|[;/])', + None, + '$1$2', + 'Sprint', + '$2', + ), + DeviceParser( + '\\b(Sprint)[: ]([^;,/ ]+)', + None, + '$1$2', + 'Sprint', + '$2', + ), + DeviceParser( + '; *(TAGI[ ]?)(MID) ?([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2$3', + 'Tagi', + '$2$3', + ), + DeviceParser( + '; *(Oyster500|Opal 800)(?: Build|\\) AppleWebKit)', + None, + 'Tecmobile $1', + 'Tecmobile', + '$1', + ), + DeviceParser( + '; *(TECNO[ _])([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Tecno', + '$2', + ), + DeviceParser( + '; *Android for (Telechips|Techvision) ([^ ]+) ', + 'i', + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(T-Hub2)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Telstra', + '$1', + ), + DeviceParser( + '; *(PAD) ?(100[12])(?: Build|\\) AppleWebKit)', + None, + 'Terra $1$2', + 'Terra', + '$1$2', + ), + DeviceParser( + '; *(T[BM]-\\d{3}[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Texet', + '$1', + ), + DeviceParser( + '; *(tolino [^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Thalia', + '$1', + ), + DeviceParser( + '; *Build/.* (TOLINO_BROWSER)', + None, + '$1', + 'Thalia', + 'Tolino Shine', + ), + DeviceParser( + '; *(?:CJ[ -])?(ThL|THL)[ -]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Thl', + '$2', + ), + DeviceParser( + '; *(T100|T200|T5|W100|W200|W8s)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Thl', + '$1', + ), + DeviceParser( + '; *(T-Mobile[ _]G2[ _]Touch) Build', + None, + '$1', + 'HTC', + 'Hero', + ), + DeviceParser( + '; *(T-Mobile[ _]G2) Build', + None, + '$1', + 'HTC', + 'Desire Z', + ), + DeviceParser( + '; *(T-Mobile myTouch Q) Build', + None, + '$1', + 'Huawei', + 'U8730', + ), + DeviceParser( + '; *(T-Mobile myTouch) Build', + None, + '$1', + 'Huawei', + 'U8680', + ), + DeviceParser( + '; *(T-Mobile_Espresso) Build', + None, + '$1', + 'HTC', + 'Espresso', + ), + DeviceParser( + '; *(T-Mobile G1) Build', + None, + '$1', + 'HTC', + 'Dream', + ), + DeviceParser( + '\\b(T-Mobile ?|)(myTouch)[ _]?([34]G)[ _]?([^\\/]*) (?:Mozilla|Build)', + None, + '$1$2 $3 $4', + 'HTC', + '$2 $3 $4', + ), + DeviceParser( + '\\b(T-Mobile)_([^_]+)_(.*) Build', + None, + '$1 $2 $3', + 'Tmobile', + '$2 $3', + ), + DeviceParser( + '\\b(T-Mobile)[_ ]?(.*?)Build', + None, + '$1 $2', + 'Tmobile', + '$2', + ), + DeviceParser( + ' (ATP[0-9]{4})(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Tomtec', + '$1', + ), + DeviceParser( + ' *(TOOKY)[ _\\-]([^;/]+?) ?(?:Build|;)', + 'i', + '$1 $2', + 'Tooky', + '$2', + ), + DeviceParser( + '\\b(TOSHIBA_AC_AND_AZ|TOSHIBA_FOLIO_AND_A|FOLIO_AND_A)', + None, + '$1', + 'Toshiba', + 'Folio 100', + ), + DeviceParser( + '; *([Ff]olio ?100)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Toshiba', + 'Folio 100', + ), + DeviceParser( + '; *(AT[0-9]{2,3}(?:\\-A|LE\\-A|PE\\-A|SE|a|)|AT7-A|AT1S0|Hikari-iFrame/WDPF-[^;/]+|THRiVE|Thrive)(?: Build|\\) AppleWebKit)', + None, + 'Toshiba $1', + 'Toshiba', + '$1', + ), + DeviceParser( + '; *(TM-MID\\d+[^;/]+|TOUCHMATE|MID-750)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Touchmate', + '$1', + ), + DeviceParser( + '; *(TM-SM\\d+[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Touchmate', + '$1', + ), + DeviceParser( + '; *(A10 [Bb]asic2?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Treq', + '$1', + ), + DeviceParser( + '; *(TREQ[ _\\-])([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + '$1$2', + 'Treq', + '$2', + ), + DeviceParser( + '; *(X-?5|X-?3)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Umeox', + '$1', + ), + DeviceParser( + '; *(A502\\+?|A936|A603|X1|X2)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Umeox', + '$1', + ), + DeviceParser( + '(TOUCH(?:TAB|PAD).+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Versus $1', + 'Versus', + '$1', + ), + DeviceParser( + '(VERTU) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'Vertu', + '$2', + ), + DeviceParser( + '; *(Videocon)[ _\\-]([^;/]+?) *(?:Build|;)', + None, + '$1 $2', + 'Videocon', + '$2', + ), + DeviceParser( + ' (VT\\d{2}[A-Za-z]*)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Videocon', + '$1', + ), + DeviceParser( + '; *((?:ViewPad|ViewPhone|VSD)[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Viewsonic', + '$1', + ), + DeviceParser( + '; *(ViewSonic-)([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'Viewsonic', + '$2', + ), + DeviceParser( + '; *(GTablet.*?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Viewsonic', + '$1', + ), + DeviceParser( + '; *([Vv]ivo)[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'vivo', + '$2', + ), + DeviceParser( + '(Vodafone) (.*?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(?:Walton[ _\\-]|)(Primo[ _\\-][^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Walton $1', + 'Walton', + '$1', + ), + DeviceParser( + '; *(?:WIKO[ \\-]|)(CINK\\+?|BARRY|BLOOM|DARKFULL|DARKMOON|DARKNIGHT|DARKSIDE|FIZZ|HIGHWAY|IGGY|OZZY|RAINBOW|STAIRWAY|SUBLIM|WAX|CINK [^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Wiko $1', + 'Wiko', + '$1', + ), + DeviceParser( + '; *WellcoM-([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Wellcom $1', + 'Wellcom', + '$1', + ), + DeviceParser( + '(?:(WeTab)-Browser|; (wetab) Build)', + None, + '$1', + 'WeTab', + 'WeTab', + ), + DeviceParser( + '; *(AT-AS[^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Wolfgang $1', + 'Wolfgang', + '$1', + ), + DeviceParser( + '; *(?:Woxter|Wxt) ([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'Woxter $1', + 'Woxter', + '$1', + ), + DeviceParser( + '; *(?:Xenta |Luna |)(TAB[234][0-9]{2}|TAB0[78]-\\d{3}|TAB0?9-\\d{3}|TAB1[03]-\\d{3}|SMP\\d{2}-\\d{3})(?: Build|\\) AppleWebKit)', + None, + 'Yarvik $1', + 'Yarvik', + '$1', + ), + DeviceParser( + '; *([A-Z]{2,4})(M\\d{3,}[A-Z]{2})([^;\\)\\/]*)(?: Build|[;\\)])', + None, + 'Yifang $1$2$3', + 'Yifang', + '$2', + ), + DeviceParser( + '; *((Mi|MI|HM|MI-ONE|Redmi)[ -](NOTE |Note |)[^;/]*) (Build|MIUI)/', + None, + 'XiaoMi $1', + 'XiaoMi', + '$1', + ), + DeviceParser( + '; *((Mi|MI|HM|MI-ONE|Redmi)[ -](NOTE |Note |)[^;/\\)]*)', + None, + 'XiaoMi $1', + 'XiaoMi', + '$1', + ), + DeviceParser( + '; *(MIX) (Build|MIUI)/', + None, + 'XiaoMi $1', + 'XiaoMi', + '$1', + ), + DeviceParser( + '; *((MIX) ([^;/]*)) (Build|MIUI)/', + None, + 'XiaoMi $1', + 'XiaoMi', + '$1', + ), + DeviceParser( + '; *XOLO[ _]([^;/]*tab.*)(?: Build|\\) AppleWebKit)', + 'i', + 'Xolo $1', + 'Xolo', + '$1', + ), + DeviceParser( + '; *XOLO[ _]([^;/]+?)(?: Build|\\) AppleWebKit)', + 'i', + 'Xolo $1', + 'Xolo', + '$1', + ), + DeviceParser( + '; *(q\\d0{2,3}[a-z]?)(?: Build|\\) AppleWebKit)', + 'i', + 'Xolo $1', + 'Xolo', + '$1', + ), + DeviceParser( + '; *(PAD ?[79]\\d+[^;/]*|TelePAD\\d+[^;/])(?: Build|\\) AppleWebKit)', + None, + 'Xoro $1', + 'Xoro', + '$1', + ), + DeviceParser( + '; *(?:(?:ZOPO|Zopo)[ _]([^;/]+?)|(ZP ?(?:\\d{2}[^;/]+|C2))|(C[2379]))(?: Build|\\) AppleWebKit)', + None, + '$1$2$3', + 'Zopo', + '$1$2$3', + ), + DeviceParser( + '; *(ZiiLABS) (Zii[^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'ZiiLabs', + '$2', + ), + DeviceParser( + '; *(Zii)_([^;/]*)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'ZiiLabs', + '$2', + ), + DeviceParser( + '; *(ARIZONA|(?:ATLAS|Atlas) W|D930|Grand (?:[SX][^;]*?|Era|Memo[^;]*?)|JOE|(?:Kis|KIS)\\b[^;]*?|Libra|Light [^;]*?|N8[056][01]|N850L|N8000|N9[15]\\d{2}|N9810|NX501|Optik|(?:Vip )Racer[^;]*?|RacerII|RACERII|San Francisco[^;]*?|V9[AC]|V55|V881|Z[679][0-9]{2}[A-z]?)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'ZTE', + '$1', + ), + DeviceParser( + '; *([A-Z]\\d+)_USA_[^;]*(?: Build|\\) AppleWebKit)', + None, + '$1', + 'ZTE', + '$1', + ), + DeviceParser( + '; *(SmartTab\\d+)[^;]*(?: Build|\\) AppleWebKit)', + None, + '$1', + 'ZTE', + '$1', + ), + DeviceParser( + '; *(?:Blade|BLADE|ZTE-BLADE)([^;/]*)(?: Build|\\) AppleWebKit)', + None, + 'ZTE Blade$1', + 'ZTE', + 'Blade$1', + ), + DeviceParser( + '; *(?:Skate|SKATE|ZTE-SKATE)([^;/]*)(?: Build|\\) AppleWebKit)', + None, + 'ZTE Skate$1', + 'ZTE', + 'Skate$1', + ), + DeviceParser( + '; *(Orange |Optimus )(Monte Carlo|San Francisco)(?: Build|\\) AppleWebKit)', + None, + '$1$2', + 'ZTE', + '$1$2', + ), + DeviceParser( + '; *(?:ZXY-ZTE_|ZTE\\-U |ZTE[\\- _]|ZTE-C[_ ])([^;/]+?)(?: Build|\\) AppleWebKit)', + None, + 'ZTE $1', + 'ZTE', + '$1', + ), + DeviceParser( + '; (BASE) (lutea|Lutea 2|Tab[^;]*?)(?: Build|\\) AppleWebKit)', + None, + '$1 $2', + 'ZTE', + '$1 $2', + ), + DeviceParser( + '; (Avea inTouch 2|soft stone|tmn smart a7|Movistar[ _]Link)(?: Build|\\) AppleWebKit)', + 'i', + '$1', + 'ZTE', + '$1', + ), + DeviceParser( + '; *(vp9plus)\\)', + None, + '$1', + 'ZTE', + '$1', + ), + DeviceParser( + '; ?(Cloud[ _]Z5|z1000|Z99 2G|z99|z930|z999|z990|z909|Z919|z900)(?: Build|\\) AppleWebKit)', + None, + '$1', + 'Zync', + '$1', + ), + DeviceParser( + '; ?(KFOT|Kindle Fire) Build\\b', + None, + 'Kindle Fire', + 'Amazon', + 'Kindle Fire', + ), + DeviceParser( + '; ?(KFOTE|Amazon Kindle Fire2) Build\\b', + None, + 'Kindle Fire 2', + 'Amazon', + 'Kindle Fire 2', + ), + DeviceParser( + '; ?(KFTT) Build\\b', + None, + 'Kindle Fire HD', + 'Amazon', + 'Kindle Fire HD 7"', + ), + DeviceParser( + '; ?(KFJWI) Build\\b', + None, + 'Kindle Fire HD 8.9" WiFi', + 'Amazon', + 'Kindle Fire HD 8.9" WiFi', + ), + DeviceParser( + '; ?(KFJWA) Build\\b', + None, + 'Kindle Fire HD 8.9" 4G', + 'Amazon', + 'Kindle Fire HD 8.9" 4G', + ), + DeviceParser( + '; ?(KFSOWI) Build\\b', + None, + 'Kindle Fire HD 7" WiFi', + 'Amazon', + 'Kindle Fire HD 7" WiFi', + ), + DeviceParser( + '; ?(KFTHWI) Build\\b', + None, + 'Kindle Fire HDX 7" WiFi', + 'Amazon', + 'Kindle Fire HDX 7" WiFi', + ), + DeviceParser( + '; ?(KFTHWA) Build\\b', + None, + 'Kindle Fire HDX 7" 4G', + 'Amazon', + 'Kindle Fire HDX 7" 4G', + ), + DeviceParser( + '; ?(KFAPWI) Build\\b', + None, + 'Kindle Fire HDX 8.9" WiFi', + 'Amazon', + 'Kindle Fire HDX 8.9" WiFi', + ), + DeviceParser( + '; ?(KFAPWA) Build\\b', + None, + 'Kindle Fire HDX 8.9" 4G', + 'Amazon', + 'Kindle Fire HDX 8.9" 4G', + ), + DeviceParser( + '; ?Amazon ([^;/]+) Build\\b', + None, + '$1', + 'Amazon', + '$1', + ), + DeviceParser( + '; ?(Kindle) Build\\b', + None, + 'Kindle', + 'Amazon', + 'Kindle', + ), + DeviceParser( + '; ?(Silk)/(\\d+)\\.(\\d+)(?:\\.([0-9\\-]+)|) Build\\b', + None, + 'Kindle Fire', + 'Amazon', + 'Kindle Fire$2', + ), + DeviceParser( + ' (Kindle)/(\\d+\\.\\d+)', + None, + 'Kindle', + 'Amazon', + '$1 $2', + ), + DeviceParser( + ' (Silk|Kindle)/(\\d+)\\.', + None, + 'Kindle', + 'Amazon', + 'Kindle', + ), + DeviceParser( + '(sprd)\\-([^/]+)/', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '; *(H\\d{2}00\\+?) Build', + None, + '$1', + 'Hero', + '$1', + ), + DeviceParser( + '; *(iphone|iPhone5) Build/', + None, + 'Xianghe $1', + 'Xianghe', + '$1', + ), + DeviceParser( + '; *(e\\d{4}[a-z]?_?v\\d+|v89_[^;/]+)[^;/]+ Build/', + None, + 'Xianghe $1', + 'Xianghe', + '$1', + ), + DeviceParser( + '\\bUSCC[_\\-]?([^ ;/\\)]+)', + None, + '$1', + 'Cellular', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|)(?:ALCATEL)[^;]*; *([^;,\\)]+)', + None, + 'Alcatel $1', + 'Alcatel', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?|)(?:ASUS|Asus)[^;]*; *([^;,\\)]+)', + None, + 'Asus $1', + 'Asus', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|)(?:DELL|Dell)[^;]*; *([^;,\\)]+)', + None, + 'Dell $1', + 'Dell', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?|)(?:HTC|Htc|HTC_blocked[^;]*)[^;]*; *(?:HTC|)([^;,\\)]+)', + None, + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|)(?:HUAWEI)[^;]*; *(?:HUAWEI |)([^;,\\)]+)', + None, + 'Huawei $1', + 'Huawei', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|)(?:LG|Lg)[^;]*; *(?:LG[ \\-]|)([^;,\\)]+)', + None, + 'LG $1', + 'LG', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|)(?:rv:11; |)(?:NOKIA|Nokia)[^;]*; *(?:NOKIA ?|Nokia ?|LUMIA ?|[Ll]umia ?|)(\\d{3,10}[^;\\)]*)', + None, + 'Lumia $1', + 'Nokia', + 'Lumia $1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|)(?:NOKIA|Nokia)[^;]*; *(RM-\\d{3,})', + None, + 'Nokia $1', + 'Nokia', + '$1', + ), + DeviceParser( + '(?:Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)]|WPDesktop;) ?(?:ARM; ?Touch; ?|Touch; ?|)(?:NOKIA|Nokia)[^;]*; *(?:NOKIA ?|Nokia ?|LUMIA ?|[Ll]umia ?|)([^;\\)]+)', + None, + 'Nokia $1', + 'Nokia', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|)(?:Microsoft(?: Corporation|))[^;]*; *([^;,\\)]+)', + None, + 'Microsoft $1', + 'Microsoft', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?|)(?:SAMSUNG)[^;]*; *(?:SAMSUNG |)([^;,\\.\\)]+)', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?|)(?:TOSHIBA|FujitsuToshibaMobileCommun)[^;]*; *([^;,\\)]+)', + None, + 'Toshiba $1', + 'Toshiba', + '$1', + ), + DeviceParser( + 'Windows Phone [^;]+; .*?IEMobile/[^;\\)]+[;\\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?|)([^;]+); *([^;,\\)]+)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '(?:^|; )SAMSUNG\\-([A-Za-z0-9\\-]+).* Bada/', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '\\(Mobile; ALCATEL ?(One|ONE) ?(Touch|TOUCH) ?([^;/]+?)(?:/[^;]+|); rv:[^\\)]+\\) Gecko/[^\\/]+ Firefox/', + None, + 'Alcatel $1 $2 $3', + 'Alcatel', + 'One Touch $3', + ), + DeviceParser( + '\\(Mobile; (?:ZTE([^;]+)|(OpenC)); rv:[^\\)]+\\) Gecko/[^\\/]+ Firefox/', + None, + 'ZTE $1$2', + 'ZTE', + '$1$2', + ), + DeviceParser( + '\\(Mobile; ALCATEL([A-Za-z0-9\\-]+); rv:[^\\)]+\\) Gecko/[^\\/]+ Firefox/[^\\/]+ KaiOS/', + None, + 'Alcatel $1', + 'Alcatel', + '$1', + ), + DeviceParser( + '\\(Mobile; LYF\\/([A-Za-z0-9\\-]+)\\/.+;.+rv:[^\\)]+\\) Gecko/[^\\/]+ Firefox/[^\\/]+ KAIOS/', + None, + 'LYF $1', + 'LYF', + '$1', + ), + DeviceParser( + '\\(Mobile; Nokia_([A-Za-z0-9\\-]+)_.+; rv:[^\\)]+\\) Gecko/[^\\/]+ Firefox/[^\\/]+ KAIOS/', + None, + 'Nokia $1', + 'Nokia', + '$1', + ), + DeviceParser( + 'Nokia(N[0-9]+)([A-Za-z_\\-][A-Za-z0-9_\\-]*)', + None, + 'Nokia $1', + 'Nokia', + '$1$2', + ), + DeviceParser( + '(?:NOKIA|Nokia)(?:\\-| *)(?:([A-Za-z0-9]+)\\-[0-9a-f]{32}|([A-Za-z0-9\\-]+)(?:UCBrowser)|([A-Za-z0-9\\-]+))', + None, + 'Nokia $1$2$3', + 'Nokia', + '$1$2$3', + ), + DeviceParser( + 'Lumia ([A-Za-z0-9\\-]+)', + None, + 'Lumia $1', + 'Nokia', + 'Lumia $1', + ), + DeviceParser( + '\\(Symbian; U; S60 V5; [A-z]{2}\\-[A-z]{2}; (SonyEricsson|Samsung|Nokia|LG)([^;/]+?)\\)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '\\(Symbian(?:/3|); U; ([^;]+);', + None, + 'Nokia $1', + 'Nokia', + '$1', + ), + DeviceParser( + 'BB10; ([A-Za-z0-9\\- ]+)\\)', + None, + 'BlackBerry $1', + 'BlackBerry', + '$1', + ), + DeviceParser( + 'Play[Bb]ook.+RIM Tablet OS', + None, + 'BlackBerry Playbook', + 'BlackBerry', + 'Playbook', + ), + DeviceParser( + 'Black[Bb]erry ([0-9]+);', + None, + 'BlackBerry $1', + 'BlackBerry', + '$1', + ), + DeviceParser( + 'Black[Bb]erry([0-9]+)', + None, + 'BlackBerry $1', + 'BlackBerry', + '$1', + ), + DeviceParser( + 'Black[Bb]erry;', + None, + 'BlackBerry', + 'BlackBerry', + None, + ), + DeviceParser( + '(Pre|Pixi)/\\d+\\.\\d+', + None, + 'Palm $1', + 'Palm', + '$1', + ), + DeviceParser( + 'Palm([0-9]+)', + None, + 'Palm $1', + 'Palm', + '$1', + ), + DeviceParser( + 'Treo([A-Za-z0-9]+)', + None, + 'Palm Treo $1', + 'Palm', + 'Treo $1', + ), + DeviceParser( + 'webOS.*(P160U(?:NA|))/(\\d+).(\\d+)', + None, + 'HP Veer', + 'HP', + 'Veer', + ), + DeviceParser( + '(Touch[Pp]ad)/\\d+\\.\\d+', + None, + 'HP TouchPad', + 'HP', + 'TouchPad', + ), + DeviceParser( + 'HPiPAQ([A-Za-z0-9]+)/\\d+.\\d+', + None, + 'HP iPAQ $1', + 'HP', + 'iPAQ $1', + ), + DeviceParser( + 'PDA; (PalmOS)/sony/model ([a-z]+)/Revision', + None, + '$1', + 'Sony', + '$1 $2', + ), + DeviceParser( + '(Apple\\s?TV)', + None, + 'AppleTV', + 'Apple', + 'AppleTV', + ), + DeviceParser( + '(QtCarBrowser)', + None, + 'Tesla Model S', + 'Tesla', + 'Model S', + ), + DeviceParser( + '(iPhone|iPad|iPod)(\\d+,\\d+)', + None, + '$1', + 'Apple', + '$1$2', + ), + DeviceParser( + '(iPad)(?:;| Simulator;)', + None, + '$1', + 'Apple', + '$1', + ), + DeviceParser( + '(iPod)(?:;| touch;| Simulator;)', + None, + '$1', + 'Apple', + '$1', + ), + DeviceParser( + '(iPhone)(?:;| Simulator;)', + None, + '$1', + 'Apple', + '$1', + ), + DeviceParser( + '(Watch)(\\d+,\\d+)', + None, + 'Apple $1', + 'Apple', + '$1$2', + ), + DeviceParser( + '(Apple Watch)(?:;| Simulator;)', + None, + '$1', + 'Apple', + '$1', + ), + DeviceParser( + '(HomePod)(?:;| Simulator;)', + None, + '$1', + 'Apple', + '$1', + ), + DeviceParser( + 'iPhone', + None, + 'iPhone', + 'Apple', + 'iPhone', + ), + DeviceParser( + 'CFNetwork/.* Darwin/\\d.*\\(((?:Mac|iMac|PowerMac|PowerBook)[^\\d]*)(\\d+)(?:,|%2C)(\\d+)', + None, + '$1$2,$3', + 'Apple', + '$1$2,$3', + ), + DeviceParser( + 'CFNetwork/.* Darwin/\\d+\\.\\d+\\.\\d+ \\(x86_64\\)', + None, + 'Mac', + 'Apple', + 'Mac', + ), + DeviceParser( + 'CFNetwork/.* Darwin/\\d', + None, + 'iOS-Device', + 'Apple', + 'iOS-Device', + ), + DeviceParser( + 'Outlook-(iOS)/\\d+\\.\\d+\\.prod\\.iphone', + None, + 'iPhone', + 'Apple', + 'iPhone', + ), + DeviceParser( + 'acer_([A-Za-z0-9]+)_', + None, + 'Acer $1', + 'Acer', + '$1', + ), + DeviceParser( + '(?:ALCATEL|Alcatel)-([A-Za-z0-9\\-]+)', + None, + 'Alcatel $1', + 'Alcatel', + '$1', + ), + DeviceParser( + '(?:Amoi|AMOI)\\-([A-Za-z0-9]+)', + None, + 'Amoi $1', + 'Amoi', + '$1', + ), + DeviceParser( + '(?:; |\\/|^)((?:Transformer (?:Pad|Prime) |Transformer |PadFone[ _]?)[A-Za-z0-9]*)', + None, + 'Asus $1', + 'Asus', + '$1', + ), + DeviceParser( + '(?:asus.*?ASUS|Asus|ASUS|asus)[\\- ;]*((?:Transformer (?:Pad|Prime) |Transformer |Padfone |Nexus[ _]|)[A-Za-z0-9]+)', + None, + 'Asus $1', + 'Asus', + '$1', + ), + DeviceParser( + '(?:ASUS)_([A-Za-z0-9\\-]+)', + None, + 'Asus $1', + 'Asus', + '$1', + ), + DeviceParser( + '\\bBIRD[ \\-\\.]([A-Za-z0-9]+)', + None, + 'Bird $1', + 'Bird', + '$1', + ), + DeviceParser( + '\\bDell ([A-Za-z0-9]+)', + None, + 'Dell $1', + 'Dell', + '$1', + ), + DeviceParser( + 'DoCoMo/2\\.0 ([A-Za-z0-9]+)', + None, + 'DoCoMo $1', + 'DoCoMo', + '$1', + ), + DeviceParser( + '([A-Za-z0-9]+)_W;FOMA', + None, + 'DoCoMo $1', + 'DoCoMo', + '$1', + ), + DeviceParser( + '([A-Za-z0-9]+);FOMA', + None, + 'DoCoMo $1', + 'DoCoMo', + '$1', + ), + DeviceParser( + '\\b(?:HTC/|HTC/[a-z0-9]+/|)HTC[ _\\-;]? *(.*?)(?:-?Mozilla|fingerPrint|[;/\\(\\)]|$)', + None, + 'HTC $1', + 'HTC', + '$1', + ), + DeviceParser( + 'Huawei([A-Za-z0-9]+)', + None, + 'Huawei $1', + 'Huawei', + '$1', + ), + DeviceParser( + 'HUAWEI-([A-Za-z0-9]+)', + None, + 'Huawei $1', + 'Huawei', + '$1', + ), + DeviceParser( + 'HUAWEI ([A-Za-z0-9\\-]+)', + None, + 'Huawei $1', + 'Huawei', + '$1', + ), + DeviceParser( + 'vodafone([A-Za-z0-9]+)', + None, + 'Huawei Vodafone $1', + 'Huawei', + 'Vodafone $1', + ), + DeviceParser( + 'i\\-mate ([A-Za-z0-9]+)', + None, + 'i-mate $1', + 'i-mate', + '$1', + ), + DeviceParser( + 'Kyocera\\-([A-Za-z0-9]+)', + None, + 'Kyocera $1', + 'Kyocera', + '$1', + ), + DeviceParser( + 'KWC\\-([A-Za-z0-9]+)', + None, + 'Kyocera $1', + 'Kyocera', + '$1', + ), + DeviceParser( + 'Lenovo[_\\-]([A-Za-z0-9]+)', + None, + 'Lenovo $1', + 'Lenovo', + '$1', + ), + DeviceParser( + '(HbbTV)/[0-9]+\\.[0-9]+\\.[0-9]+ \\( ?;(LG)E ?;([^;]{0,30})', + None, + '$1', + '$2', + '$3', + ), + DeviceParser( + '(HbbTV)/1\\.1\\.1.*CE-HTML/1\\.\\d;(Vendor/|)(THOM[^;]*?)[;\\s].{0,30}(LF[^;]+);?', + None, + '$1', + 'Thomson', + '$4', + ), + DeviceParser( + '(HbbTV)(?:/1\\.1\\.1|) ?(?: \\(;;;;;\\)|); *CE-HTML(?:/1\\.\\d|); *([^ ]+) ([^;]+);', + None, + '$1', + '$2', + '$3', + ), + DeviceParser( + '(HbbTV)/1\\.1\\.1 \\(;;;;;\\) Maple_2011', + None, + '$1', + 'Samsung', + None, + ), + DeviceParser( + '(HbbTV)/[0-9]+\\.[0-9]+\\.[0-9]+ \\([^;]{0,30}; ?(?:CUS:([^;]*)|([^;]+)) ?; ?([^;]{0,30})', + None, + '$1', + '$2$3', + '$4', + ), + DeviceParser( + '(HbbTV)/[0-9]+\\.[0-9]+\\.[0-9]+', + None, + '$1', + None, + None, + ), + DeviceParser( + 'LGE; (?:Media\\/|)([^;]*);[^;]*;[^;]*;?\\); "?LG NetCast(\\.TV|\\.Media|)-\\d+', + None, + 'NetCast$2', + 'LG', + '$1', + ), + DeviceParser( + 'InettvBrowser/[0-9]+\\.[0-9A-Z]+ \\([^;]*;(Sony)([^;]*);[^;]*;[^\\)]*\\)', + None, + 'Inettv', + '$1', + '$2', + ), + DeviceParser( + 'InettvBrowser/[0-9]+\\.[0-9A-Z]+ \\([^;]*;([^;]*);[^;]*;[^\\)]*\\)', + None, + 'Inettv', + 'Generic_Inettv', + '$1', + ), + DeviceParser( + '(?:InettvBrowser|TSBNetTV|NETTV|HBBTV)', + None, + 'Inettv', + 'Generic_Inettv', + None, + ), + DeviceParser( + 'Series60/\\d\\.\\d (LG)[\\-]?([A-Za-z0-9 \\-]+)', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + '\\b(?:LGE[ \\-]LG\\-(?:AX|)|LGE |LGE?-LG|LGE?[ \\-]|LG[ /\\-]|lg[\\-])([A-Za-z0-9]+)\\b', + None, + 'LG $1', + 'LG', + '$1', + ), + DeviceParser( + '(?:^LG[\\-]?|^LGE[\\-/]?)([A-Za-z]+[0-9]+[A-Za-z]*)', + None, + 'LG $1', + 'LG', + '$1', + ), + DeviceParser( + '^LG([0-9]+[A-Za-z]*)', + None, + 'LG $1', + 'LG', + '$1', + ), + DeviceParser( + '(KIN\\.[^ ]+) (\\d+)\\.(\\d+)', + None, + 'Microsoft $1', + 'Microsoft', + '$1', + ), + DeviceParser( + '(?:MSIE|XBMC).*\\b(Xbox)\\b', + None, + '$1', + 'Microsoft', + '$1', + ), + DeviceParser( + '; ARM; Trident/6\\.0; Touch[\\);]', + None, + 'Microsoft Surface RT', + 'Microsoft', + 'Surface RT', + ), + DeviceParser( + 'Motorola\\-([A-Za-z0-9]+)', + None, + 'Motorola $1', + 'Motorola', + '$1', + ), + DeviceParser( + 'MOTO\\-([A-Za-z0-9]+)', + None, + 'Motorola $1', + 'Motorola', + '$1', + ), + DeviceParser( + 'MOT\\-([A-z0-9][A-z0-9\\-]*)', + None, + 'Motorola $1', + 'Motorola', + '$1', + ), + DeviceParser( + 'Nintendo WiiU', + None, + 'Nintendo Wii U', + 'Nintendo', + 'Wii U', + ), + DeviceParser( + 'Nintendo (DS|3DS|DSi|Wii);', + None, + 'Nintendo $1', + 'Nintendo', + '$1', + ), + DeviceParser( + '(?:Pantech|PANTECH)[ _-]?([A-Za-z0-9\\-]+)', + None, + 'Pantech $1', + 'Pantech', + '$1', + ), + DeviceParser( + 'Philips([A-Za-z0-9]+)', + None, + 'Philips $1', + 'Philips', + '$1', + ), + DeviceParser( + 'Philips ([A-Za-z0-9]+)', + None, + 'Philips $1', + 'Philips', + '$1', + ), + DeviceParser( + '(SMART-TV); .* Tizen ', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + 'SymbianOS/9\\.\\d.* Samsung[/\\-]([A-Za-z0-9 \\-]+)', + None, + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '(Samsung)(SGH)(i[0-9]+)', + None, + '$1 $2$3', + '$1', + '$2-$3', + ), + DeviceParser( + 'SAMSUNG-ANDROID-MMS/([^;/]+)', + None, + '$1', + 'Samsung', + '$1', + ), + DeviceParser( + 'SAMSUNG(?:; |[ -/])([A-Za-z0-9\\-]+)', + 'i', + 'Samsung $1', + 'Samsung', + '$1', + ), + DeviceParser( + '(Dreamcast)', + None, + 'Sega $1', + 'Sega', + '$1', + ), + DeviceParser( + '^SIE-([A-Za-z0-9]+)', + None, + 'Siemens $1', + 'Siemens', + '$1', + ), + DeviceParser( + 'Softbank/[12]\\.0/([A-Za-z0-9]+)', + None, + 'Softbank $1', + 'Softbank', + '$1', + ), + DeviceParser( + 'SonyEricsson ?([A-Za-z0-9\\-]+)', + None, + 'Ericsson $1', + 'SonyEricsson', + '$1', + ), + DeviceParser( + 'Android [^;]+; ([^ ]+) (Sony)/', + None, + '$2 $1', + '$2', + '$1', + ), + DeviceParser( + '(Sony)(?:BDP\\/|\\/|)([^ /;\\)]+)[ /;\\)]', + None, + '$1 $2', + '$1', + '$2', + ), + DeviceParser( + 'Puffin/[\\d\\.]+IT', + None, + 'iPad', + 'Apple', + 'iPad', + ), + DeviceParser( + 'Puffin/[\\d\\.]+IP', + None, + 'iPhone', + 'Apple', + 'iPhone', + ), + DeviceParser( + 'Puffin/[\\d\\.]+AT', + None, + 'Generic Tablet', + 'Generic', + 'Tablet', + ), + DeviceParser( + 'Puffin/[\\d\\.]+AP', + None, + 'Generic Smartphone', + 'Generic', + 'Smartphone', + ), + DeviceParser( + 'Android[\\- ][\\d]+\\.[\\d]+; [A-Za-z]{2}\\-[A-Za-z]{0,2}; WOWMobile (.+)( Build[/ ]|\\))', + None, + None, + 'Generic_Android', + '$1', + ), + DeviceParser( + 'Android[\\- ][\\d]+\\.[\\d]+\\-update1; [A-Za-z]{2}\\-[A-Za-z]{0,2} *; *(.+?)( Build[/ ]|\\))', + None, + None, + 'Generic_Android', + '$1', + ), + DeviceParser( + 'Android[\\- ][\\d]+(?:\\.[\\d]+)(?:\\.[\\d]+|); *[A-Za-z]{2}[_\\-][A-Za-z]{0,2}\\-? *; *(.+?)( Build[/ ]|\\))', + None, + None, + 'Generic_Android', + '$1', + ), + DeviceParser( + 'Android[\\- ][\\d]+(?:\\.[\\d]+)(?:\\.[\\d]+|); *[A-Za-z]{0,2}\\- *; *(.+?)( Build[/ ]|\\))', + None, + None, + 'Generic_Android', + '$1', + ), + DeviceParser( + 'Android[\\- ][\\d]+(?:\\.[\\d]+)(?:\\.[\\d]+|); *[a-z]{0,2}[_\\-]?[A-Za-z]{0,2};?( Build[/ ]|\\))', + None, + 'Generic Smartphone', + 'Generic', + 'Smartphone', + ), + DeviceParser( + 'Android[\\- ][\\d]+(?:\\.[\\d]+)(?:\\.[\\d]+|); *\\-?[A-Za-z]{2}; *(.+?)( Build[/ ]|\\))', + None, + None, + 'Generic_Android', + '$1', + ), + DeviceParser( + 'Android \\d+?(?:\\.\\d+|)(?:\\.\\d+|); ([^;]+?)(?: Build|\\) AppleWebKit).+? Mobile Safari', + None, + None, + 'Generic_Android', + '$1', + ), + DeviceParser( + 'Android \\d+?(?:\\.\\d+|)(?:\\.\\d+|); ([^;]+?)(?: Build|\\) AppleWebKit).+? Safari', + None, + None, + 'Generic_Android_Tablet', + '$1', + ), + DeviceParser( + 'Android \\d+?(?:\\.\\d+|)(?:\\.\\d+|); ([^;]+?)(?: Build|\\))', + None, + None, + 'Generic_Android', + '$1', + ), + DeviceParser( + '(GoogleTV)', + None, + None, + 'Generic_Inettv', + '$1', + ), + DeviceParser( + '(WebTV)/\\d+.\\d+', + None, + None, + 'Generic_Inettv', + '$1', + ), + DeviceParser( + '^(Roku)/DVP-\\d+\\.\\d+', + None, + None, + 'Generic_Inettv', + '$1', + ), + DeviceParser( + '(Android 3\\.\\d|Opera Tablet|Tablet; .+Firefox/|Android.*(?:Tab|Pad))', + 'i', + 'Generic Tablet', + 'Generic', + 'Tablet', + ), + DeviceParser( + '(Symbian|\\bS60(Version|V\\d)|\\bS60\\b|\\((Series 60|Windows Mobile|Palm OS|Bada); Opera Mini|Windows CE|Opera Mobi|BREW|Brew|Mobile; .+Firefox/|iPhone OS|Android|MobileSafari|Windows *Phone|\\(webOS/|PalmOS)', + None, + 'Generic Smartphone', + 'Generic', + 'Smartphone', + ), + DeviceParser( + '(hiptop|avantgo|plucker|xiino|blazer|elaine)', + 'i', + 'Generic Smartphone', + 'Generic', + 'Smartphone', + ), + DeviceParser( + '(bot|BUbiNG|zao|borg|DBot|oegp|silk|Xenu|zeal|^NING|CCBot|crawl|htdig|lycos|slurp|teoma|voila|yahoo|Sogou|CiBra|Nutch|^Java/|^JNLP/|Daumoa|Daum|Genieo|ichiro|larbin|pompos|Scrapy|snappy|speedy|spider|msnbot|msrbot|vortex|^vortex|crawler|favicon|indexer|Riddler|scooter|scraper|scrubby|WhatWeb|WinHTTP|bingbot|BingPreview|openbot|gigabot|furlbot|polybot|seekbot|^voyager|archiver|Icarus6j|mogimogi|Netvibes|blitzbot|altavista|charlotte|findlinks|Retreiver|TLSProber|WordPress|SeznamBot|ProoXiBot|wsr\\-agent|Squrl Java|EtaoSpider|PaperLiBot|SputnikBot|A6\\-Indexer|netresearch|searchsight|baiduspider|YisouSpider|ICC\\-Crawler|http%20client|Python-urllib|dataparksearch|converacrawler|Screaming Frog|AppEngine-Google|YahooCacheSystem|fast\\-webcrawler|Sogou Pic Spider|semanticdiscovery|Innovazion Crawler|facebookexternalhit|Google.*/\\+/web/snippet|Google-HTTP-Java-Client|BlogBridge|IlTrovatore-Setaccio|InternetArchive|GomezAgent|WebThumbnail|heritrix|NewsGator|PagePeeker|Reaper|ZooShot|holmes|NL-Crawler|Pingdom|StatusCake|WhatsApp|masscan|Google Web Preview|Qwantify|Yeti|OgScrper)', + 'i', + 'Spider', + 'Spider', + 'Desktop', + ), + DeviceParser( + '^(1207|3gso|4thp|501i|502i|503i|504i|505i|506i|6310|6590|770s|802s|a wa|acer|acs\\-|airn|alav|asus|attw|au\\-m|aur |aus |abac|acoo|aiko|alco|alca|amoi|anex|anny|anyw|aptu|arch|argo|bmobile|bell|bird|bw\\-n|bw\\-u|beck|benq|bilb|blac|c55/|cdm\\-|chtm|capi|comp|cond|dall|dbte|dc\\-s|dica|ds\\-d|ds12|dait|devi|dmob|doco|dopo|dorado|el(?:38|39|48|49|50|55|58|68)|el[3456]\\d{2}dual|erk0|esl8|ex300|ez40|ez60|ez70|ezos|ezze|elai|emul|eric|ezwa|fake|fly\\-|fly_|g\\-mo|g1 u|g560|gf\\-5|grun|gene|go.w|good|grad|hcit|hd\\-m|hd\\-p|hd\\-t|hei\\-|hp i|hpip|hs\\-c|htc |htc\\-|htca|htcg)', + 'i', + 'Generic Feature Phone', + 'Generic', + 'Feature Phone', + ), + DeviceParser( + '^(htcp|htcs|htct|htc_|haie|hita|huaw|hutc|i\\-20|i\\-go|i\\-ma|i\\-mobile|i230|iac|iac\\-|iac/|ig01|im1k|inno|iris|jata|kddi|kgt|kgt/|kpt |kwc\\-|klon|lexi|lg g|lg\\-a|lg\\-b|lg\\-c|lg\\-d|lg\\-f|lg\\-g|lg\\-k|lg\\-l|lg\\-m|lg\\-o|lg\\-p|lg\\-s|lg\\-t|lg\\-u|lg\\-w|lg/k|lg/l|lg/u|lg50|lg54|lge\\-|lge/|leno|m1\\-w|m3ga|m50/|maui|mc01|mc21|mcca|medi|meri|mio8|mioa|mo01|mo02|mode|modo|mot |mot\\-|mt50|mtp1|mtv |mate|maxo|merc|mits|mobi|motv|mozz|n100|n101|n102|n202|n203|n300|n302|n500|n502|n505|n700|n701|n710|nec\\-|nem\\-|newg|neon)', + 'i', + 'Generic Feature Phone', + 'Generic', + 'Feature Phone', + ), + DeviceParser( + '^(netf|noki|nzph|o2 x|o2\\-x|opwv|owg1|opti|oran|ot\\-s|p800|pand|pg\\-1|pg\\-2|pg\\-3|pg\\-6|pg\\-8|pg\\-c|pg13|phil|pn\\-2|pt\\-g|palm|pana|pire|pock|pose|psio|qa\\-a|qc\\-2|qc\\-3|qc\\-5|qc\\-7|qc07|qc12|qc21|qc32|qc60|qci\\-|qwap|qtek|r380|r600|raks|rim9|rove|s55/|sage|sams|sc01|sch\\-|scp\\-|sdk/|se47|sec\\-|sec0|sec1|semc|sgh\\-|shar|sie\\-|sk\\-0|sl45|slid|smb3|smt5|sp01|sph\\-|spv |spv\\-|sy01|samm|sany|sava|scoo|send|siem|smar|smit|soft|sony|t\\-mo|t218|t250|t600|t610|t618|tcl\\-|tdg\\-|telm|tim\\-|ts70|tsm\\-|tsm3|tsm5|tx\\-9|tagt)', + 'i', + 'Generic Feature Phone', + 'Generic', + 'Feature Phone', + ), + DeviceParser( + '^(talk|teli|topl|tosh|up.b|upg1|utst|v400|v750|veri|vk\\-v|vk40|vk50|vk52|vk53|vm40|vx98|virg|vertu|vite|voda|vulc|w3c |w3c\\-|wapj|wapp|wapu|wapm|wig |wapi|wapr|wapv|wapy|wapa|waps|wapt|winc|winw|wonu|x700|xda2|xdag|yas\\-|your|zte\\-|zeto|aste|audi|avan|blaz|brew|brvw|bumb|ccwa|cell|cldc|cmd\\-|dang|eml2|fetc|hipt|http|ibro|idea|ikom|ipaq|jbro|jemu|jigs|keji|kyoc|kyok|libw|m\\-cr|midp|mmef|moto|mwbp|mywa|newt|nok6|o2im|pant|pdxg|play|pluc|port|prox|rozo|sama|seri|smal|symb|treo|upsi|vx52|vx53|vx60|vx61|vx70|vx80|vx81|vx83|vx85|wap\\-|webc|whit|wmlb|xda\\-|xda_)', + 'i', + 'Generic Feature Phone', + 'Generic', + 'Feature Phone', + ), + DeviceParser( + '^(Ice)$', + None, + 'Generic Feature Phone', + 'Generic', + 'Feature Phone', + ), + DeviceParser( + '(wap[\\-\\ ]browser|maui|netfront|obigo|teleca|up\\.browser|midp|Opera Mini)', + 'i', + 'Generic Feature Phone', + 'Generic', + 'Feature Phone', + ), + DeviceParser( + 'Mac OS', + None, + 'Mac', + 'Apple', + 'Mac', + ), +] + +OS_PARSERS = [ + OSParser( + 'HbbTV/\\d+\\.\\d+\\.\\d+ \\( ;(LG)E ;NetCast 4.0', + None, + '2013', + None, + None, + None, + ), + OSParser( + 'HbbTV/\\d+\\.\\d+\\.\\d+ \\( ;(LG)E ;NetCast 3.0', + None, + '2012', + None, + None, + None, + ), + OSParser( + 'HbbTV/1.1.1 \\(;;;;;\\) Maple_2011', + 'Samsung', + '2011', + None, + None, + None, + ), + OSParser( + 'HbbTV/\\d+\\.\\d+\\.\\d+ \\(;(Samsung);SmartTV([0-9]{4});.*FXPDEUC', + None, + None, + 'UE40F7000', + None, + None, + ), + OSParser( + 'HbbTV/\\d+\\.\\d+\\.\\d+ \\(;(Samsung);SmartTV([0-9]{4});.*MST12DEUC', + None, + None, + 'UE32F4500', + None, + None, + ), + OSParser( + 'HbbTV/1\\.1\\.1 \\(; (Philips);.*NETTV/4', + None, + '2013', + None, + None, + None, + ), + OSParser( + 'HbbTV/1\\.1\\.1 \\(; (Philips);.*NETTV/3', + None, + '2012', + None, + None, + None, + ), + OSParser( + 'HbbTV/1\\.1\\.1 \\(; (Philips);.*NETTV/2', + None, + '2011', + None, + None, + None, + ), + OSParser( + 'HbbTV/\\d+\\.\\d+\\.\\d+.*(firetv)-firefox-plugin (\\d+).(\\d+).(\\d+)', + 'FireHbbTV', + None, + None, + None, + None, + ), + OSParser( + 'HbbTV/\\d+\\.\\d+\\.\\d+ \\(.*; ?([a-zA-Z]+) ?;.*(201[1-9]).*\\)', + None, + None, + None, + None, + None, + ), + OSParser( + '(Windows Phone) (?:OS[ /])?(\\d+)\\.(\\d+)', + None, + None, + None, + None, + None, + ), + OSParser( + '(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone)[ +]+(\\d+)[_\\.](\\d+)(?:[_\\.](\\d+)|).*Outlook-iOS-Android', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(Android)[ \\-/](\\d+)(?:\\.(\\d+)|)(?:[.\\-]([a-z0-9]+)|)', + None, + None, + None, + None, + None, + ), + OSParser( + '(Android) Donut', + None, + '1', + '2', + None, + None, + ), + OSParser( + '(Android) Eclair', + None, + '2', + '1', + None, + None, + ), + OSParser( + '(Android) Froyo', + None, + '2', + '2', + None, + None, + ), + OSParser( + '(Android) Gingerbread', + None, + '2', + '3', + None, + None, + ), + OSParser( + '(Android) Honeycomb', + None, + '3', + None, + None, + None, + ), + OSParser( + '(Android) (\\d+);', + None, + None, + None, + None, + None, + ), + OSParser( + '^UCWEB.*; (Adr) (\\d+)\\.(\\d+)(?:[.\\-]([a-z0-9]+)|);', + 'Android', + None, + None, + None, + None, + ), + OSParser( + '^UCWEB.*; (iPad|iPh|iPd) OS (\\d+)_(\\d+)(?:_(\\d+)|);', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '^UCWEB.*; (wds) (\\d+)\\.(\\d+)(?:\\.(\\d+)|);', + 'Windows Phone', + None, + None, + None, + None, + ), + OSParser( + '^(JUC).*; ?U; ?(?:Android|)(\\d+)\\.(\\d+)(?:[\\.\\-]([a-z0-9]+)|)', + 'Android', + None, + None, + None, + None, + ), + OSParser( + '(android)\\s(?:mobile\\/)(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)|)|)', + 'Android', + None, + None, + None, + None, + ), + OSParser( + '(Silk-Accelerated=[a-z]{4,5})', + 'Android', + None, + None, + None, + None, + ), + OSParser( + '(x86_64|aarch64)\\ (\\d+)\\.(\\d+)\\.(\\d+).*Chrome.*(?:CitrixChromeApp)$', + 'Chrome OS', + None, + None, + None, + None, + ), + OSParser( + '(XBLWP7)', + 'Windows Phone', + None, + None, + None, + None, + ), + OSParser( + '(Windows ?Mobile)', + 'Windows Mobile', + None, + None, + None, + None, + ), + OSParser( + '(Windows 10)', + 'Windows', + '10', + None, + None, + None, + ), + OSParser( + '(Windows (?:NT 5\\.2|NT 5\\.1))', + 'Windows', + 'XP', + None, + None, + None, + ), + OSParser( + '(Windows NT 6\\.1)', + 'Windows', + '7', + None, + None, + None, + ), + OSParser( + '(Windows NT 6\\.0)', + 'Windows', + 'Vista', + None, + None, + None, + ), + OSParser( + '(Win 9x 4\\.90)', + 'Windows', + 'ME', + None, + None, + None, + ), + OSParser( + '(Windows NT 6\\.2; ARM;)', + 'Windows', + 'RT', + None, + None, + None, + ), + OSParser( + '(Windows NT 6\\.2)', + 'Windows', + '8', + None, + None, + None, + ), + OSParser( + '(Windows NT 6\\.3; ARM;)', + 'Windows', + 'RT 8', + '1', + None, + None, + ), + OSParser( + '(Windows NT 6\\.3)', + 'Windows', + '8', + '1', + None, + None, + ), + OSParser( + '(Windows NT 6\\.4)', + 'Windows', + '10', + None, + None, + None, + ), + OSParser( + '(Windows NT 10\\.0)', + 'Windows', + '10', + None, + None, + None, + ), + OSParser( + '(Windows NT 5\\.0)', + 'Windows', + '2000', + None, + None, + None, + ), + OSParser( + '(WinNT4.0)', + 'Windows', + 'NT 4.0', + None, + None, + None, + ), + OSParser( + '(Windows ?CE)', + 'Windows', + 'CE', + None, + None, + None, + ), + OSParser( + 'Win(?:dows)? ?(95|98|3.1|NT|ME|2000|XP|Vista|7|CE)', + 'Windows', + '$1', + None, + None, + None, + ), + OSParser( + 'Win16', + 'Windows', + '3.1', + None, + None, + None, + ), + OSParser( + 'Win32', + 'Windows', + '95', + None, + None, + None, + ), + OSParser( + '^Box.*Windows/([\\d.]+);', + 'Windows', + '$1', + None, + None, + None, + ), + OSParser( + '(Tizen)[/ ](\\d+)\\.(\\d+)', + None, + None, + None, + None, + None, + ), + OSParser( + '((?:Mac[ +]?|; )OS[ +]X)[\\s+/](?:(\\d+)[_.](\\d+)(?:[_.](\\d+)|)|Mach-O)', + 'Mac OS X', + None, + None, + None, + None, + ), + OSParser( + '\\w+\\s+Mac OS X\\s+\\w+\\s+(\\d+).(\\d+).(\\d+).*', + 'Mac OS X', + '$1', + '$2', + '$3', + None, + ), + OSParser( + ' (Dar)(win)/(9).(\\d+).*\\((?:i386|x86_64|Power Macintosh)\\)', + 'Mac OS X', + '10', + '5', + None, + None, + ), + OSParser( + ' (Dar)(win)/(10).(\\d+).*\\((?:i386|x86_64)\\)', + 'Mac OS X', + '10', + '6', + None, + None, + ), + OSParser( + ' (Dar)(win)/(11).(\\d+).*\\((?:i386|x86_64)\\)', + 'Mac OS X', + '10', + '7', + None, + None, + ), + OSParser( + ' (Dar)(win)/(12).(\\d+).*\\((?:i386|x86_64)\\)', + 'Mac OS X', + '10', + '8', + None, + None, + ), + OSParser( + ' (Dar)(win)/(13).(\\d+).*\\((?:i386|x86_64)\\)', + 'Mac OS X', + '10', + '9', + None, + None, + ), + OSParser( + 'Mac_PowerPC', + 'Mac OS', + None, + None, + None, + None, + ), + OSParser( + '(?:PPC|Intel) (Mac OS X)', + None, + None, + None, + None, + None, + ), + OSParser( + '^Box.*;(Darwin)/(10)\\.(1\\d)(?:\\.(\\d+)|)', + 'Mac OS X', + None, + None, + None, + None, + ), + OSParser( + '(Apple\\s?TV)(?:/(\\d+)\\.(\\d+)|)', + 'ATV OS X', + None, + None, + None, + None, + ), + OSParser( + '(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS)[ +]+(\\d+)[_\\.](\\d+)(?:[_\\.](\\d+)|)', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(iPhone|iPad|iPod); Opera', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(iPhone|iPad|iPod).*Mac OS X.*Version/(\\d+)\\.(\\d+)', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/(5)48\\.0\\.3.* Darwin/11\\.0\\.0', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/(5)48\\.(0)\\.4.* Darwin/(1)1\\.0\\.0', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/(5)48\\.(1)\\.4', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/(4)85\\.1(3)\\.9', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/(6)09\\.(1)\\.4', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/(6)(0)9', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/6(7)2\\.(1)\\.13', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/6(7)2\\.(1)\\.(1)4', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CF)(Network)/6(7)(2)\\.1\\.15', + 'iOS', + '7', + '1', + None, + None, + ), + OSParser( + '(CFNetwork)/6(7)2\\.(0)\\.(?:2|8)', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(CFNetwork)/709\\.1', + 'iOS', + '8', + '0.b5', + None, + None, + ), + OSParser( + '(CF)(Network)/711\\.(\\d)', + 'iOS', + '8', + None, + None, + None, + ), + OSParser( + '(CF)(Network)/(720)\\.(\\d)', + 'Mac OS X', + '10', + '10', + None, + None, + ), + OSParser( + '(CF)(Network)/(760)\\.(\\d)', + 'Mac OS X', + '10', + '11', + None, + None, + ), + OSParser( + 'CFNetwork/7.* Darwin/15\\.4\\.\\d+', + 'iOS', + '9', + '3', + '1', + None, + ), + OSParser( + 'CFNetwork/7.* Darwin/15\\.5\\.\\d+', + 'iOS', + '9', + '3', + '2', + None, + ), + OSParser( + 'CFNetwork/7.* Darwin/15\\.6\\.\\d+', + 'iOS', + '9', + '3', + '5', + None, + ), + OSParser( + '(CF)(Network)/758\\.(\\d)', + 'iOS', + '9', + None, + None, + None, + ), + OSParser( + 'CFNetwork/808\\.3 Darwin/16\\.3\\.\\d+', + 'iOS', + '10', + '2', + '1', + None, + ), + OSParser( + '(CF)(Network)/808\\.(\\d)', + 'iOS', + '10', + None, + None, + None, + ), + OSParser( + 'CFNetwork/.* Darwin/17\\.\\d+.*\\(x86_64\\)', + 'Mac OS X', + '10', + '13', + None, + None, + ), + OSParser( + 'CFNetwork/.* Darwin/16\\.\\d+.*\\(x86_64\\)', + 'Mac OS X', + '10', + '12', + None, + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/15\\.\\d+.*\\(x86_64\\)', + 'Mac OS X', + '10', + '11', + None, + None, + ), + OSParser( + 'CFNetwork/.* Darwin/(9)\\.\\d+', + 'iOS', + '1', + None, + None, + None, + ), + OSParser( + 'CFNetwork/.* Darwin/(10)\\.\\d+', + 'iOS', + '4', + None, + None, + None, + ), + OSParser( + 'CFNetwork/.* Darwin/(11)\\.\\d+', + 'iOS', + '5', + None, + None, + None, + ), + OSParser( + 'CFNetwork/.* Darwin/(13)\\.\\d+', + 'iOS', + '6', + None, + None, + None, + ), + OSParser( + 'CFNetwork/6.* Darwin/(14)\\.\\d+', + 'iOS', + '7', + None, + None, + None, + ), + OSParser( + 'CFNetwork/7.* Darwin/(14)\\.\\d+', + 'iOS', + '8', + '0', + None, + None, + ), + OSParser( + 'CFNetwork/7.* Darwin/(15)\\.\\d+', + 'iOS', + '9', + '0', + None, + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/16\\.5\\.\\d+', + 'iOS', + '10', + '3', + None, + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/16\\.6\\.\\d+', + 'iOS', + '10', + '3', + '2', + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/16\\.7\\.\\d+', + 'iOS', + '10', + '3', + '3', + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/(16)\\.\\d+', + 'iOS', + '10', + None, + None, + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/17\\.0\\.\\d+', + 'iOS', + '11', + '0', + None, + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/17\\.2\\.\\d+', + 'iOS', + '11', + '1', + None, + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/17\\.3\\.\\d+', + 'iOS', + '11', + '2', + None, + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/17\\.4\\.\\d+', + 'iOS', + '11', + '2', + '6', + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/17\\.5\\.\\d+', + 'iOS', + '11', + '3', + None, + None, + ), + OSParser( + 'CFNetwork/9.* Darwin/17\\.6\\.\\d+', + 'iOS', + '11', + '4', + None, + None, + ), + OSParser( + 'CFNetwork/9.* Darwin/17\\.7\\.\\d+', + 'iOS', + '11', + '4', + '1', + None, + ), + OSParser( + 'CFNetwork/8.* Darwin/(17)\\.\\d+', + 'iOS', + '11', + None, + None, + None, + ), + OSParser( + 'CFNetwork/9.* Darwin/18\\.0\\.\\d+', + 'iOS', + '12', + '0', + None, + None, + ), + OSParser( + 'CFNetwork/9.* Darwin/(18)\\.\\d+', + 'iOS', + '12', + None, + None, + None, + ), + OSParser( + 'CFNetwork/.* Darwin/', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '\\b(iOS[ /]|iOS; |iPhone(?:/| v|[ _]OS[/,]|; | OS : |\\d,\\d/|\\d,\\d; )|iPad/)(\\d{1,2})[_\\.](\\d{1,2})(?:[_\\.](\\d+)|)', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '\\((iOS);', + None, + None, + None, + None, + None, + ), + OSParser( + '(watchOS)/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'WatchOS', + None, + None, + None, + None, + ), + OSParser( + 'Outlook-(iOS)/\\d+\\.\\d+\\.prod\\.iphone', + None, + None, + None, + None, + None, + ), + OSParser( + '(iPod|iPhone|iPad)', + 'iOS', + None, + None, + None, + None, + ), + OSParser( + '(tvOS)[/ ](\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'tvOS', + None, + None, + None, + None, + ), + OSParser( + '(CrOS) [a-z0-9_]+ (\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'Chrome OS', + None, + None, + None, + None, + ), + OSParser( + '([Dd]ebian)', + 'Debian', + None, + None, + None, + None, + ), + OSParser( + '(Linux Mint)(?:/(\\d+)|)', + None, + None, + None, + None, + None, + ), + OSParser( + '(Mandriva)(?: Linux|)/(?:[\\d.-]+m[a-z]{2}(\\d+).(\\d)|)', + None, + None, + None, + None, + None, + ), + OSParser( + '(Symbian[Oo][Ss])[/ ](\\d+)\\.(\\d+)', + 'Symbian OS', + None, + None, + None, + None, + ), + OSParser( + '(Symbian/3).+NokiaBrowser/7\\.3', + 'Symbian^3 Anna', + None, + None, + None, + None, + ), + OSParser( + '(Symbian/3).+NokiaBrowser/7\\.4', + 'Symbian^3 Belle', + None, + None, + None, + None, + ), + OSParser( + '(Symbian/3)', + 'Symbian^3', + None, + None, + None, + None, + ), + OSParser( + '\\b(Series 60|SymbOS|S60Version|S60V\\d|S60\\b)', + 'Symbian OS', + None, + None, + None, + None, + ), + OSParser( + '(MeeGo)', + None, + None, + None, + None, + None, + ), + OSParser( + 'Symbian [Oo][Ss]', + 'Symbian OS', + None, + None, + None, + None, + ), + OSParser( + 'Series40;', + 'Nokia Series 40', + None, + None, + None, + None, + ), + OSParser( + 'Series30Plus;', + 'Nokia Series 30 Plus', + None, + None, + None, + None, + ), + OSParser( + '(BB10);.+Version/(\\d+)\\.(\\d+)\\.(\\d+)', + 'BlackBerry OS', + None, + None, + None, + None, + ), + OSParser( + '(Black[Bb]erry)[0-9a-z]+/(\\d+)\\.(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'BlackBerry OS', + None, + None, + None, + None, + ), + OSParser( + '(Black[Bb]erry).+Version/(\\d+)\\.(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'BlackBerry OS', + None, + None, + None, + None, + ), + OSParser( + '(RIM Tablet OS) (\\d+)\\.(\\d+)\\.(\\d+)', + 'BlackBerry Tablet OS', + None, + None, + None, + None, + ), + OSParser( + '(Play[Bb]ook)', + 'BlackBerry Tablet OS', + None, + None, + None, + None, + ), + OSParser( + '(Black[Bb]erry)', + 'BlackBerry OS', + None, + None, + None, + None, + ), + OSParser( + '(K[Aa][Ii]OS)\\/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'KaiOS', + None, + None, + None, + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Gecko/18.0 Firefox/\\d+\\.\\d+', + 'Firefox OS', + '1', + '0', + '1', + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Gecko/18.1 Firefox/\\d+\\.\\d+', + 'Firefox OS', + '1', + '1', + None, + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Gecko/26.0 Firefox/\\d+\\.\\d+', + 'Firefox OS', + '1', + '2', + None, + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Gecko/28.0 Firefox/\\d+\\.\\d+', + 'Firefox OS', + '1', + '3', + None, + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Gecko/30.0 Firefox/\\d+\\.\\d+', + 'Firefox OS', + '1', + '4', + None, + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Gecko/32.0 Firefox/\\d+\\.\\d+', + 'Firefox OS', + '2', + '0', + None, + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Gecko/34.0 Firefox/\\d+\\.\\d+', + 'Firefox OS', + '2', + '1', + None, + None, + ), + OSParser( + '\\((?:Mobile|Tablet);.+Firefox/\\d+\\.\\d+', + 'Firefox OS', + None, + None, + None, + None, + ), + OSParser( + '(BREW)[ /](\\d+)\\.(\\d+)\\.(\\d+)', + None, + None, + None, + None, + None, + ), + OSParser( + '(BREW);', + None, + None, + None, + None, + None, + ), + OSParser( + '(Brew MP|BMP)[ /](\\d+)\\.(\\d+)\\.(\\d+)', + 'Brew MP', + None, + None, + None, + None, + ), + OSParser( + 'BMP;', + 'Brew MP', + None, + None, + None, + None, + ), + OSParser( + '(GoogleTV)(?: (\\d+)\\.(\\d+)(?:\\.(\\d+)|)|/[\\da-z]+)', + None, + None, + None, + None, + None, + ), + OSParser( + '(WebTV)/(\\d+).(\\d+)', + None, + None, + None, + None, + None, + ), + OSParser( + '(CrKey)(?:[/](\\d+)\\.(\\d+)(?:\\.(\\d+)|)|)', + 'Chromecast', + None, + None, + None, + None, + ), + OSParser( + '(hpw|web)OS/(\\d+)\\.(\\d+)(?:\\.(\\d+)|)', + 'webOS', + None, + None, + None, + None, + ), + OSParser( + '(VRE);', + None, + None, + None, + None, + None, + ), + OSParser( + '(Fedora|Red Hat|PCLinuxOS|Puppy|Ubuntu|Kindle|Bada|Sailfish|Lubuntu|BackTrack|Slackware|(?:Free|Open|Net|\\b)BSD)[/ ](\\d+)\\.(\\d+)(?:\\.(\\d+)|)(?:\\.(\\d+)|)', + None, + None, + None, + None, + None, + ), + OSParser( + '(Linux)[ /](\\d+)\\.(\\d+)(?:\\.(\\d+)|).*gentoo', + 'Gentoo', + None, + None, + None, + None, + ), + OSParser( + '\\((Bada);', + None, + None, + None, + None, + None, + ), + OSParser( + '(Windows|Android|WeTab|Maemo|Web0S)', + None, + None, + None, + None, + None, + ), + OSParser( + '(Ubuntu|Kubuntu|Arch Linux|CentOS|Slackware|Gentoo|openSUSE|SUSE|Red Hat|Fedora|PCLinuxOS|Mageia|(?:Free|Open|Net|\\b)BSD)', + None, + None, + None, + None, + None, + ), + OSParser( + '(Linux)(?:[ /](\\d+)\\.(\\d+)(?:\\.(\\d+)|)|)', + None, + None, + None, + None, + None, + ), + OSParser( + 'SunOS', + 'Solaris', + None, + None, + None, + None, + ), + OSParser( + '\\(linux-gnu\\)', + 'Linux', + None, + None, + None, + None, + ), + OSParser( + '\\(x86_64-redhat-linux-gnu\\)', + 'Red Hat', + None, + None, + None, + None, + ), + OSParser( + '\\((freebsd)(\\d+)\\.(\\d+)\\)', + 'FreeBSD', + None, + None, + None, + None, + ), + OSParser( + 'linux', + 'Linux', + None, + None, + None, + None, + ), + OSParser( + '^(Roku)/DVP-(\\d+)\\.(\\d+)', + None, + None, + None, + None, + None, + ), +] diff --git a/app_common/lib/ua_parser/user_agent_parser.py b/app_common/lib/ua_parser/user_agent_parser.py new file mode 100644 index 00000000..45f5a063 --- /dev/null +++ b/app_common/lib/ua_parser/user_agent_parser.py @@ -0,0 +1,544 @@ +# Copyright 2009 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License') +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Python implementation of the UA parser.""" + +from __future__ import absolute_import + +import os +import re + +__author__ = "Lindsey Simon " + + +class UserAgentParser(object): + def __init__( + self, pattern, family_replacement=None, v1_replacement=None, v2_replacement=None + ): + """Initialize UserAgentParser. + + Args: + pattern: a regular expression string + family_replacement: a string to override the matched family (optional) + v1_replacement: a string to override the matched v1 (optional) + v2_replacement: a string to override the matched v2 (optional) + """ + self.pattern = pattern + self.user_agent_re = re.compile(self.pattern) + self.family_replacement = family_replacement + self.v1_replacement = v1_replacement + self.v2_replacement = v2_replacement + + def MatchSpans(self, user_agent_string): + match_spans = [] + match = self.user_agent_re.search(user_agent_string) + if match: + match_spans = [ + match.span(group_index) for group_index in range(1, match.lastindex + 1) + ] + return match_spans + + def Parse(self, user_agent_string): + family, v1, v2, v3 = None, None, None, None + match = self.user_agent_re.search(user_agent_string) + if match: + if self.family_replacement: + if re.search(r"\$1", self.family_replacement): + family = re.sub(r"\$1", match.group(1), self.family_replacement) + else: + family = self.family_replacement + else: + family = match.group(1) + + if self.v1_replacement: + v1 = self.v1_replacement + elif match.lastindex and match.lastindex >= 2: + v1 = match.group(2) or None + + if self.v2_replacement: + v2 = self.v2_replacement + elif match.lastindex and match.lastindex >= 3: + v2 = match.group(3) or None + + if match.lastindex and match.lastindex >= 4: + v3 = match.group(4) or None + + return family, v1, v2, v3 + + +class OSParser(object): + def __init__( + self, + pattern, + os_replacement=None, + os_v1_replacement=None, + os_v2_replacement=None, + os_v3_replacement=None, + os_v4_replacement=None, + ): + """Initialize UserAgentParser. + + Args: + pattern: a regular expression string + os_replacement: a string to override the matched os (optional) + os_v1_replacement: a string to override the matched v1 (optional) + os_v2_replacement: a string to override the matched v2 (optional) + os_v3_replacement: a string to override the matched v3 (optional) + os_v4_replacement: a string to override the matched v4 (optional) + """ + self.pattern = pattern + self.user_agent_re = re.compile(self.pattern) + self.os_replacement = os_replacement + self.os_v1_replacement = os_v1_replacement + self.os_v2_replacement = os_v2_replacement + self.os_v3_replacement = os_v3_replacement + self.os_v4_replacement = os_v4_replacement + + def MatchSpans(self, user_agent_string): + match_spans = [] + match = self.user_agent_re.search(user_agent_string) + if match: + match_spans = [ + match.span(group_index) for group_index in range(1, match.lastindex + 1) + ] + return match_spans + + def Parse(self, user_agent_string): + os, os_v1, os_v2, os_v3, os_v4 = None, None, None, None, None + match = self.user_agent_re.search(user_agent_string) + if match: + if self.os_replacement: + os = MultiReplace(self.os_replacement, match) + elif match.lastindex: + os = match.group(1) + + if self.os_v1_replacement: + os_v1 = MultiReplace(self.os_v1_replacement, match) + elif match.lastindex and match.lastindex >= 2: + os_v1 = match.group(2) + + if self.os_v2_replacement: + os_v2 = MultiReplace(self.os_v2_replacement, match) + elif match.lastindex and match.lastindex >= 3: + os_v2 = match.group(3) + + if self.os_v3_replacement: + os_v3 = MultiReplace(self.os_v3_replacement, match) + elif match.lastindex and match.lastindex >= 4: + os_v3 = match.group(4) + + if self.os_v4_replacement: + os_v4 = MultiReplace(self.os_v4_replacement, match) + elif match.lastindex and match.lastindex >= 5: + os_v4 = match.group(5) + + return os, os_v1, os_v2, os_v3, os_v4 + + +def MultiReplace(string, match): + def _repl(m): + index = int(m.group(1)) - 1 + group = match.groups() + if index < len(group): + return group[index] + return "" + + _string = re.sub(r"\$(\d)", _repl, string) + _string = re.sub(r"^\s+|\s+$", "", _string) + if _string == "": + return None + return _string + + +class DeviceParser(object): + def __init__( + self, + pattern, + regex_flag=None, + device_replacement=None, + brand_replacement=None, + model_replacement=None, + ): + """Initialize UserAgentParser. + + Args: + pattern: a regular expression string + device_replacement: a string to override the matched device (optional) + """ + self.pattern = pattern + if regex_flag == "i": + self.user_agent_re = re.compile(self.pattern, re.IGNORECASE) + else: + self.user_agent_re = re.compile(self.pattern) + self.device_replacement = device_replacement + self.brand_replacement = brand_replacement + self.model_replacement = model_replacement + + def MatchSpans(self, user_agent_string): + match_spans = [] + match = self.user_agent_re.search(user_agent_string) + if match: + match_spans = [ + match.span(group_index) for group_index in range(1, match.lastindex + 1) + ] + return match_spans + + def Parse(self, user_agent_string): + device, brand, model = None, None, None + match = self.user_agent_re.search(user_agent_string) + if match: + if self.device_replacement: + device = MultiReplace(self.device_replacement, match) + else: + device = match.group(1) + + if self.brand_replacement: + brand = MultiReplace(self.brand_replacement, match) + + if self.model_replacement: + model = MultiReplace(self.model_replacement, match) + elif len(match.groups()) > 0: + model = match.group(1) + + return device, brand, model + + +MAX_CACHE_SIZE = 20 +_parse_cache = {} + + +def Parse(user_agent_string, **jsParseBits): + """ Parse all the things + Args: + user_agent_string: the full user agent string + jsParseBits: javascript override bits + Returns: + A dictionary containing all parsed bits + """ + jsParseBits = jsParseBits or {} + key = (user_agent_string, repr(jsParseBits)) + cached = _parse_cache.get(key) + if cached is not None: + return cached + if len(_parse_cache) > MAX_CACHE_SIZE: + _parse_cache.clear() + v = { + "user_agent": ParseUserAgent(user_agent_string, **jsParseBits), + "os": ParseOS(user_agent_string, **jsParseBits), + "device": ParseDevice(user_agent_string, **jsParseBits), + "string": user_agent_string, + } + _parse_cache[key] = v + return v + + +def ParseUserAgent(user_agent_string, **jsParseBits): + """ Parses the user-agent string for user agent (browser) info. + Args: + user_agent_string: The full user-agent string. + jsParseBits: javascript override bits. + Returns: + A dictionary containing parsed bits. + """ + if ( + "js_user_agent_family" in jsParseBits + and jsParseBits["js_user_agent_family"] != "" + ): + family = jsParseBits["js_user_agent_family"] + v1 = jsParseBits.get("js_user_agent_v1") or None + v2 = jsParseBits.get("js_user_agent_v2") or None + v3 = jsParseBits.get("js_user_agent_v3") or None + else: + for uaParser in USER_AGENT_PARSERS: + family, v1, v2, v3 = uaParser.Parse(user_agent_string) + if family: + break + + # Override for Chrome Frame IFF Chrome is enabled. + if "js_user_agent_string" in jsParseBits: + js_user_agent_string = jsParseBits["js_user_agent_string"] + if ( + js_user_agent_string + and js_user_agent_string.find("Chrome/") > -1 + and user_agent_string.find("chromeframe") > -1 + ): + jsOverride = {} + jsOverride = ParseUserAgent(js_user_agent_string) + family = "Chrome Frame (%s %s)" % (family, v1) + v1 = jsOverride["major"] + v2 = jsOverride["minor"] + v3 = jsOverride["patch"] + + family = family or "Other" + return { + "family": family, + "major": v1 or None, + "minor": v2 or None, + "patch": v3 or None, + } + + +def ParseOS(user_agent_string, **jsParseBits): + """ Parses the user-agent string for operating system info + Args: + user_agent_string: The full user-agent string. + jsParseBits: javascript override bits. + Returns: + A dictionary containing parsed bits. + """ + for osParser in OS_PARSERS: + os, os_v1, os_v2, os_v3, os_v4 = osParser.Parse(user_agent_string) + if os: + break + os = os or "Other" + return { + "family": os, + "major": os_v1, + "minor": os_v2, + "patch": os_v3, + "patch_minor": os_v4, + } + + +def ParseDevice(user_agent_string): + """ Parses the user-agent string for device info. + Args: + user_agent_string: The full user-agent string. + ua_family: The parsed user agent family name. + Returns: + A dictionary containing parsed bits. + """ + for deviceParser in DEVICE_PARSERS: + device, brand, model = deviceParser.Parse(user_agent_string) + if device: + break + + if device is None: + device = "Other" + + return {"family": device, "brand": brand, "model": model} + + +def PrettyUserAgent(family, v1=None, v2=None, v3=None): + """Pretty user agent string.""" + if v3: + if v3[0].isdigit(): + return "%s %s.%s.%s" % (family, v1, v2, v3) + else: + return "%s %s.%s%s" % (family, v1, v2, v3) + elif v2: + return "%s %s.%s" % (family, v1, v2) + elif v1: + return "%s %s" % (family, v1) + return family + + +def PrettyOS(os, os_v1=None, os_v2=None, os_v3=None, os_v4=None): + """Pretty os string.""" + if os_v4: + return "%s %s.%s.%s.%s" % (os, os_v1, os_v2, os_v3, os_v4) + if os_v3: + if os_v3[0].isdigit(): + return "%s %s.%s.%s" % (os, os_v1, os_v2, os_v3) + else: + return "%s %s.%s%s" % (os, os_v1, os_v2, os_v3) + elif os_v2: + return "%s %s.%s" % (os, os_v1, os_v2) + elif os_v1: + return "%s %s" % (os, os_v1) + return os + + +def ParseWithJSOverrides( + user_agent_string, + js_user_agent_string=None, + js_user_agent_family=None, + js_user_agent_v1=None, + js_user_agent_v2=None, + js_user_agent_v3=None, +): + """ backwards compatible. use one of the other Parse methods instead! """ + + # Override via JS properties. + if js_user_agent_family is not None and js_user_agent_family != "": + family = js_user_agent_family + v1 = None + v2 = None + v3 = None + if js_user_agent_v1 is not None: + v1 = js_user_agent_v1 + if js_user_agent_v2 is not None: + v2 = js_user_agent_v2 + if js_user_agent_v3 is not None: + v3 = js_user_agent_v3 + else: + for parser in USER_AGENT_PARSERS: + family, v1, v2, v3 = parser.Parse(user_agent_string) + if family: + break + + # Override for Chrome Frame IFF Chrome is enabled. + if ( + js_user_agent_string + and js_user_agent_string.find("Chrome/") > -1 + and user_agent_string.find("chromeframe") > -1 + ): + family = "Chrome Frame (%s %s)" % (family, v1) + ua_dict = ParseUserAgent(js_user_agent_string) + v1 = ua_dict["major"] + v2 = ua_dict["minor"] + v3 = ua_dict["patch"] + + return family or "Other", v1, v2, v3 + + +def Pretty(family, v1=None, v2=None, v3=None): + """ backwards compatible. use PrettyUserAgent instead! """ + if v3: + if v3[0].isdigit(): + return "%s %s.%s.%s" % (family, v1, v2, v3) + else: + return "%s %s.%s%s" % (family, v1, v2, v3) + elif v2: + return "%s %s.%s" % (family, v1, v2) + elif v1: + return "%s %s" % (family, v1) + return family + + +def GetFilters( + user_agent_string, + js_user_agent_string=None, + js_user_agent_family=None, + js_user_agent_v1=None, + js_user_agent_v2=None, + js_user_agent_v3=None, +): + """Return the optional arguments that should be saved and used to query. + + js_user_agent_string is always returned if it is present. We really only need + it for Chrome Frame. However, I added it in the generally case to find other + cases when it is different. When the recording of js_user_agent_string was + added, we created new records for all new user agents. + + Since we only added js_document_mode for the IE 9 preview case, it did not + cause new user agent records the way js_user_agent_string did. + + js_document_mode has since been removed in favor of individual property + overrides. + + Args: + user_agent_string: The full user-agent string. + js_user_agent_string: JavaScript ua string from client-side + js_user_agent_family: This is an override for the family name to deal + with the fact that IE platform preview (for instance) cannot be + distinguished by user_agent_string, but only in javascript. + js_user_agent_v1: v1 override - see above. + js_user_agent_v2: v1 override - see above. + js_user_agent_v3: v1 override - see above. + Returns: + {js_user_agent_string: '[...]', js_family_name: '[...]', etc...} + """ + filters = {} + filterdict = { + "js_user_agent_string": js_user_agent_string, + "js_user_agent_family": js_user_agent_family, + "js_user_agent_v1": js_user_agent_v1, + "js_user_agent_v2": js_user_agent_v2, + "js_user_agent_v3": js_user_agent_v3, + } + for key, value in filterdict.items(): + if value is not None and value != "": + filters[key] = value + return filters + + +# Build the list of user agent parsers from YAML +UA_PARSER_YAML = os.environ.get("UA_PARSER_YAML") +if UA_PARSER_YAML: + # This will raise an ImportError if missing, obviously since it's no + # longer a requirement + import yaml + + try: + # Try and use libyaml bindings if available since faster + from yaml import CSafeLoader as SafeLoader + except ImportError: + from yaml import SafeLoader + + with open(UA_PARSER_YAML) as fp: + regexes = yaml.load(fp, Loader=SafeLoader) + + USER_AGENT_PARSERS = [] + for _ua_parser in regexes["user_agent_parsers"]: + _regex = _ua_parser["regex"] + + _family_replacement = _ua_parser.get("family_replacement") + _v1_replacement = _ua_parser.get("v1_replacement") + _v2_replacement = _ua_parser.get("v2_replacement") + + USER_AGENT_PARSERS.append( + UserAgentParser( + _regex, _family_replacement, _v1_replacement, _v2_replacement + ) + ) + + OS_PARSERS = [] + for _os_parser in regexes["os_parsers"]: + _regex = _os_parser["regex"] + + _os_replacement = _os_parser.get("os_replacement") + _os_v1_replacement = _os_parser.get("os_v1_replacement") + _os_v2_replacement = _os_parser.get("os_v2_replacement") + _os_v3_replacement = _os_parser.get("os_v3_replacement") + _os_v4_replacement = _os_parser.get("os_v4_replacement") + + OS_PARSERS.append( + OSParser( + _regex, + _os_replacement, + _os_v1_replacement, + _os_v2_replacement, + _os_v3_replacement, + _os_v4_replacement, + ) + ) + + DEVICE_PARSERS = [] + for _device_parser in regexes["device_parsers"]: + _regex = _device_parser["regex"] + + _regex_flag = _device_parser.get("regex_flag") + _device_replacement = _device_parser.get("device_replacement") + _brand_replacement = _device_parser.get("brand_replacement") + _model_replacement = _device_parser.get("model_replacement") + + DEVICE_PARSERS.append( + DeviceParser( + _regex, + _regex_flag, + _device_replacement, + _brand_replacement, + _model_replacement, + ) + ) + + # Clean our our temporary vars explicitly + # so they can't be reused or imported + del regexes + del yaml + del SafeLoader +else: + # Just load our pre-compiled versions + from ._regexes import USER_AGENT_PARSERS, DEVICE_PARSERS, OS_PARSERS diff --git a/app_common/lib/ua_parser/user_agent_parser_test.py b/app_common/lib/ua_parser/user_agent_parser_test.py new file mode 100644 index 00000000..c73d742b --- /dev/null +++ b/app_common/lib/ua_parser/user_agent_parser_test.py @@ -0,0 +1,290 @@ +#!/usr/bin/python2.5 +# +# Copyright 2008 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License') +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""User Agent Parser Unit Tests. +Run: +# python -m user_agent_parser_test (runs all the tests, takes awhile) +or like: +# python -m user_agent_parser_test ParseTest.testBrowserscopeStrings +""" + + +from __future__ import unicode_literals, absolute_import + +__author__ = "slamm@google.com (Stephen Lamm)" + +import os +import re +import unittest +import yaml + +try: + # Try and use libyaml bindings if available since faster + from yaml import CSafeLoader as SafeLoader +except ImportError: + from yaml import SafeLoader + +from ua_parser import user_agent_parser + +TEST_RESOURCES_DIR = os.path.join( + os.path.abspath(os.path.dirname(__file__)), "../uap-core" +) + + +class ParseTest(unittest.TestCase): + def testBrowserscopeStrings(self): + self.runUserAgentTestsFromYAML( + os.path.join(TEST_RESOURCES_DIR, "tests/test_ua.yaml") + ) + + def testBrowserscopeStringsOS(self): + self.runOSTestsFromYAML(os.path.join(TEST_RESOURCES_DIR, "tests/test_os.yaml")) + + def testStringsOS(self): + self.runOSTestsFromYAML( + os.path.join(TEST_RESOURCES_DIR, "test_resources/additional_os_tests.yaml") + ) + + def testStringsDevice(self): + self.runDeviceTestsFromYAML( + os.path.join(TEST_RESOURCES_DIR, "tests/test_device.yaml") + ) + + def testMozillaStrings(self): + self.runUserAgentTestsFromYAML( + os.path.join( + TEST_RESOURCES_DIR, "test_resources/firefox_user_agent_strings.yaml" + ) + ) + + # NOTE: The YAML file used here is one output by makePGTSComparisonYAML() + # below, as opposed to the pgts_browser_list-orig.yaml file. The -orig + # file is by no means perfect, but identifies many browsers that we + # classify as "Other". This test itself is mostly useful to know when + # somthing in UA parsing changes. An effort should be made to try and + # reconcile the differences between the two YAML files. + def testPGTSStrings(self): + self.runUserAgentTestsFromYAML( + os.path.join(TEST_RESOURCES_DIR, "test_resources/pgts_browser_list.yaml") + ) + + def testParseAll(self): + user_agent_string = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; fr; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5,gzip(gfe),gzip(gfe)" + expected = { + "device": {"family": "Mac", "brand": "Apple", "model": "Mac"}, + "os": { + "family": "Mac OS X", + "major": "10", + "minor": "4", + "patch": None, + "patch_minor": None, + }, + "user_agent": { + "family": "Firefox", + "major": "3", + "minor": "5", + "patch": "5", + }, + "string": user_agent_string, + } + + result = user_agent_parser.Parse(user_agent_string) + self.assertEqual( + result, + expected, + "UA: {0}\n expected<{1}> != actual<{2}>".format( + user_agent_string, expected, result + ), + ) + + # Make a YAML file for manual comparsion with pgts_browser_list-orig.yaml + def makePGTSComparisonYAML(self): + import codecs + + outfile = codecs.open("outfile.yaml", "w", "utf-8") + print >> outfile, "test_cases:" + + yamlFile = open(os.path.join(TEST_RESOURCES_DIR, "pgts_browser_list.yaml")) + yamlContents = yaml.load(yamlFile, Loader=SafeLoader) + yamlFile.close() + + for test_case in yamlContents["test_cases"]: + user_agent_string = test_case["user_agent_string"] + kwds = {} + if "js_ua" in test_case: + kwds = eval(test_case["js_ua"]) + + (family, major, minor, patch) = user_agent_parser.ParseUserAgent( + user_agent_string, **kwds + ) + + # Escape any double-quotes in the UA string + user_agent_string = re.sub(r'"', '\\"', user_agent_string) + print >> outfile, ' - user_agent_string: "' + user_agent_string + '"' + "\n" + ' family: "' + family + '"\n' + " major: " + ( + "" if (major is None) else "'" + major + "'" + ) + "\n" + " minor: " + ( + "" if (minor is None) else "'" + minor + "'" + ) + "\n" + " patch: " + ( + "" if (patch is None) else "'" + patch + "'" + ) + outfile.close() + + # Run a set of test cases from a YAML file + def runUserAgentTestsFromYAML(self, file_name): + yamlFile = open(os.path.join(TEST_RESOURCES_DIR, file_name)) + yamlContents = yaml.load(yamlFile, Loader=SafeLoader) + yamlFile.close() + + for test_case in yamlContents["test_cases"]: + # Inputs to Parse() + user_agent_string = test_case["user_agent_string"] + kwds = {} + if "js_ua" in test_case: + kwds = eval(test_case["js_ua"]) + + # The expected results + expected = { + "family": test_case["family"], + "major": test_case["major"], + "minor": test_case["minor"], + "patch": test_case["patch"], + } + + result = {} + result = user_agent_parser.ParseUserAgent(user_agent_string, **kwds) + self.assertEqual( + result, + expected, + "UA: {0}\n expected<{1}, {2}, {3}, {4}> != actual<{5}, {6}, {7}, {8}>".format( + user_agent_string, + expected["family"], + expected["major"], + expected["minor"], + expected["patch"], + result["family"], + result["major"], + result["minor"], + result["patch"], + ), + ) + + def runOSTestsFromYAML(self, file_name): + yamlFile = open(os.path.join(TEST_RESOURCES_DIR, file_name)) + yamlContents = yaml.load(yamlFile, Loader=SafeLoader) + yamlFile.close() + + for test_case in yamlContents["test_cases"]: + # Inputs to Parse() + user_agent_string = test_case["user_agent_string"] + kwds = {} + if "js_ua" in test_case: + kwds = eval(test_case["js_ua"]) + + # The expected results + expected = { + "family": test_case["family"], + "major": test_case["major"], + "minor": test_case["minor"], + "patch": test_case["patch"], + "patch_minor": test_case["patch_minor"], + } + + result = user_agent_parser.ParseOS(user_agent_string, **kwds) + self.assertEqual( + result, + expected, + "UA: {0}\n expected<{1} {2} {3} {4} {5}> != actual<{6} {7} {8} {9} {10}>".format( + user_agent_string, + expected["family"], + expected["major"], + expected["minor"], + expected["patch"], + expected["patch_minor"], + result["family"], + result["major"], + result["minor"], + result["patch"], + result["patch_minor"], + ), + ) + + def runDeviceTestsFromYAML(self, file_name): + yamlFile = open(os.path.join(TEST_RESOURCES_DIR, file_name)) + yamlContents = yaml.load(yamlFile, Loader=SafeLoader) + yamlFile.close() + + for test_case in yamlContents["test_cases"]: + # Inputs to Parse() + user_agent_string = test_case["user_agent_string"] + kwds = {} + if "js_ua" in test_case: + kwds = eval(test_case["js_ua"]) + + # The expected results + expected = { + "family": test_case["family"], + "brand": test_case["brand"], + "model": test_case["model"], + } + + result = user_agent_parser.ParseDevice(user_agent_string, **kwds) + self.assertEqual( + result, + expected, + "UA: {0}\n expected<{1} {2} {3}> != actual<{4} {5} {6}>".format( + user_agent_string, + expected["family"], + expected["brand"], + expected["model"], + result["family"], + result["brand"], + result["model"], + ), + ) + + +class GetFiltersTest(unittest.TestCase): + def testGetFiltersNoMatchesGiveEmptyDict(self): + user_agent_string = "foo" + filters = user_agent_parser.GetFilters( + user_agent_string, js_user_agent_string=None + ) + self.assertEqual({}, filters) + + def testGetFiltersJsUaPassedThrough(self): + user_agent_string = "foo" + filters = user_agent_parser.GetFilters( + user_agent_string, js_user_agent_string="bar" + ) + self.assertEqual({"js_user_agent_string": "bar"}, filters) + + def testGetFiltersJsUserAgentFamilyAndVersions(self): + user_agent_string = ( + "Mozilla/4.0 (compatible; MSIE 8.0; " + "Windows NT 5.1; Trident/4.0; GTB6; .NET CLR 2.0.50727; " + ".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)" + ) + filters = user_agent_parser.GetFilters( + user_agent_string, js_user_agent_string="bar", js_user_agent_family="foo" + ) + self.assertEqual( + {"js_user_agent_string": "bar", "js_user_agent_family": "foo"}, filters + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/app_common/lib/user_agents/__init__.py b/app_common/lib/user_agents/__init__.py new file mode 100644 index 00000000..e47740f2 --- /dev/null +++ b/app_common/lib/user_agents/__init__.py @@ -0,0 +1,3 @@ +VERSION = (2, 2, 0) + +from .parsers import parse diff --git a/app_common/lib/user_agents/compat.py b/app_common/lib/user_agents/compat.py new file mode 100644 index 00000000..c4d1a247 --- /dev/null +++ b/app_common/lib/user_agents/compat.py @@ -0,0 +1,14 @@ +import sys + +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str + + def iteritems(d, **kw): + return iter(d.items(**kw)) +else: + string_types = basestring + + def iteritems(d, **kw): + return iter(d.iteritems(**kw)) diff --git a/app_common/lib/user_agents/parsers.py b/app_common/lib/user_agents/parsers.py new file mode 100644 index 00000000..7c309697 --- /dev/null +++ b/app_common/lib/user_agents/parsers.py @@ -0,0 +1,268 @@ +from collections import namedtuple + +from ..ua_parser import user_agent_parser +from .compat import string_types + + +MOBILE_DEVICE_FAMILIES = ( + 'iPhone', + 'iPod', + 'Generic Smartphone', + 'Generic Feature Phone', + 'PlayStation Vita', + 'iOS-Device' +) + +PC_OS_FAMILIES = ( + 'Windows 95', + 'Windows 98', + 'Solaris', +) + +MOBILE_OS_FAMILIES = ( + 'Windows Phone', + 'Windows Phone OS', # Earlier versions of ua-parser returns Windows Phone OS + 'Symbian OS', + 'Bada', + 'Windows CE', + 'Windows Mobile', + 'Maemo', +) + +MOBILE_BROWSER_FAMILIES = ( + 'IE Mobile', + 'Opera Mobile', + 'Opera Mini', + 'Chrome Mobile', + 'Chrome Mobile WebView', + 'Chrome Mobile iOS', +) + +TABLET_DEVICE_FAMILIES = ( + 'iPad', + 'BlackBerry Playbook', + 'Blackberry Playbook', # Earlier versions of ua-parser returns "Blackberry" instead of "BlackBerry" + 'Kindle', + 'Kindle Fire', + 'Kindle Fire HD', + 'Galaxy Tab', + 'Xoom', + 'Dell Streak', +) + +TOUCH_CAPABLE_OS_FAMILIES = ( + 'iOS', + 'Android', + 'Windows Phone', + 'Windows CE', + 'Windows Mobile', + 'Firefox OS', + 'MeeGo', +) + +TOUCH_CAPABLE_DEVICE_FAMILIES = ( + 'BlackBerry Playbook', + 'Blackberry Playbook', + 'Kindle Fire', +) + +EMAIL_PROGRAM_FAMILIES = set(( + 'Outlook', + 'Windows Live Mail', + 'AirMail', + 'Apple Mail', + 'Outlook', + 'Thunderbird', + 'Lightning', + 'ThunderBrowse', + 'Windows Live Mail', + 'The Bat!', + 'Lotus Notes', + 'IBM Notes', + 'Barca', + 'MailBar', + 'kmail2', + 'YahooMobileMail' +)) + +def verify_attribute(attribute): + if isinstance(attribute, string_types) and attribute.isdigit(): + return int(attribute) + + return attribute + + +def parse_version(major=None, minor=None, patch=None, patch_minor=None): + # Returns version number tuple, attributes will be integer if they're numbers + major = verify_attribute(major) + minor = verify_attribute(minor) + patch = verify_attribute(patch) + patch_minor = verify_attribute(patch_minor) + + return tuple( + filter(lambda x: x is not None, (major, minor, patch, patch_minor)) + ) + + +Browser = namedtuple('Browser', ['family', 'version', 'version_string']) + + +def parse_browser(family, major=None, minor=None, patch=None, patch_minor=None): + # Returns a browser object + version = parse_version(major, minor, patch) + version_string = '.'.join([str(v) for v in version]) + return Browser(family, version, version_string) + + +OperatingSystem = namedtuple('OperatingSystem', ['family', 'version', 'version_string']) + + +def parse_operating_system(family, major=None, minor=None, patch=None, patch_minor=None): + version = parse_version(major, minor, patch) + version_string = '.'.join([str(v) for v in version]) + return OperatingSystem(family, version, version_string) + + +Device = namedtuple('Device', ['family', 'brand', 'model']) + + +def parse_device(family, brand, model): + return Device(family, brand, model) + + +class UserAgent(object): + + def __init__(self, user_agent_string): + ua_dict = user_agent_parser.Parse(user_agent_string) + self.ua_string = user_agent_string + self.os = parse_operating_system(**ua_dict['os']) + self.browser = parse_browser(**ua_dict['user_agent']) + self.device = parse_device(**ua_dict['device']) + + def __str__(self): + return "{device} / {os} / {browser}".format( + device=self.get_device(), + os=self.get_os(), + browser=self.get_browser() + ) + + def __unicode__(self): + return unicode(str(self)) + + def _is_android_tablet(self): + # Newer Android tablets don't have "Mobile" in their user agent string, + # older ones like Galaxy Tab still have "Mobile" though they're not + if ('Mobile Safari' not in self.ua_string and + self.browser.family != "Firefox Mobile"): + return True + return False + + def _is_blackberry_touch_capable_device(self): + # A helper to determine whether a BB phone has touch capabilities + # Blackberry Bold Touch series begins with 99XX + if 'Blackberry 99' in self.device.family: + return True + if 'Blackberry 95' in self.device.family: # BB Storm devices + return True + return False + + def get_device(self): + return self.is_pc and "PC" or self.device.family + + def get_os(self): + return ("%s %s" % (self.os.family, self.os.version_string)).strip() + + def get_browser(self): + return ("%s %s" % (self.browser.family, self.browser.version_string)).strip() + + @property + def is_tablet(self): + if self.device.family in TABLET_DEVICE_FAMILIES: + return True + if (self.os.family == 'Android' and self._is_android_tablet()): + return True + if self.os.family == 'Windows' and self.os.version_string.startswith('RT'): + return True + if self.os.family == 'Firefox OS' and 'Mobile' not in self.browser.family: + return True + return False + + @property + def is_mobile(self): + # First check for mobile device and mobile browser families + if self.device.family in MOBILE_DEVICE_FAMILIES: + return True + if self.browser.family in MOBILE_BROWSER_FAMILIES: + return True + # Device is considered Mobile OS is Android and not tablet + # This is not fool proof but would have to suffice for now + if ((self.os.family == 'Android' or self.os.family == 'Firefox OS') + and not self.is_tablet): + return True + if self.os.family == 'BlackBerry OS' and self.device.family != 'Blackberry Playbook': + return True + if self.os.family in MOBILE_OS_FAMILIES: + return True + # TODO: remove after https://github.com/tobie/ua-parser/issues/126 is closed + if 'J2ME' in self.ua_string or 'MIDP' in self.ua_string: + return True + # This is here mainly to detect Google's Mobile Spider + if 'iPhone;' in self.ua_string: + return True + if 'Googlebot-Mobile' in self.ua_string: + return True + # Mobile Spiders should be identified as mobile + if self.device.family == 'Spider' and 'Mobile' in self.browser.family: + return True + # Nokia mobile + if 'NokiaBrowser' in self.ua_string and 'Mobile' in self.ua_string: + return True + return False + + @property + def is_touch_capable(self): + # TODO: detect touch capable Nokia devices + if self.os.family in TOUCH_CAPABLE_OS_FAMILIES: + return True + if self.device.family in TOUCH_CAPABLE_DEVICE_FAMILIES: + return True + if self.os.family == 'Windows': + if self.os.version_string.startswith(('RT', 'CE')): + return True + if self.os.version_string.startswith('8') and 'Touch' in self.ua_string: + return True + if 'BlackBerry' in self.os.family and self._is_blackberry_touch_capable_device(): + return True + return False + + @property + def is_pc(self): + # Returns True for "PC" devices (Windows, Mac and Linux) + if 'Windows NT' in self.ua_string or self.os.family in PC_OS_FAMILIES or \ + self.os.family == 'Windows' and self.os.version_string == 'ME': + return True + # TODO: remove after https://github.com/tobie/ua-parser/issues/127 is closed + if self.os.family == 'Mac OS X' and 'Silk' not in self.ua_string: + return True + # Maemo has 'Linux' and 'X11' in UA, but it is not for PC + if 'Maemo' in self.ua_string: + return False + if 'Chrome OS' in self.os.family: + return True + if 'Linux' in self.ua_string and 'X11' in self.ua_string: + return True + return False + + @property + def is_bot(self): + return True if self.device.family == 'Spider' else False + + @property + def is_email_client(self): + if self.browser.family in EMAIL_PROGRAM_FAMILIES: + return True + return False + + +def parse(user_agent_string): + return UserAgent(user_agent_string) diff --git a/app_common/lib/user_agents/tests.py b/app_common/lib/user_agents/tests.py new file mode 100644 index 00000000..345cf827 --- /dev/null +++ b/app_common/lib/user_agents/tests.py @@ -0,0 +1,268 @@ +import json +import os +import unittest + +from ua_parser import user_agent_parser +from . import compat +from .parsers import parse + + +iphone_ua_string = 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B179 Safari/7534.48.3' +ipad_ua_string = 'Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314 Safari/531.21.10' +galaxy_tab_ua_string = 'Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' +galaxy_s3_ua_string = 'Mozilla/5.0 (Linux; U; Android 4.0.4; en-gb; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30' +kindle_fire_ua_string = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.1.0-80) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true' +playbook_ua_string = 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.0.1; en-US) AppleWebKit/535.8+ (KHTML, like Gecko) Version/7.2.0.1 Safari/535.8+' +nexus_7_ua_string = 'Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19' +windows_phone_ua_string = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; SAMSUNG; SGH-i917)' +blackberry_torch_ua_string = 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; zh-TW) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.448 Mobile Safari/534.8+' +blackberry_bold_ua_string = 'BlackBerry9700/5.0.0.862 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/331 UNTRUSTED/1.0 3gpp-gba' +blackberry_bold_touch_ua_string = 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9930; en-US) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0.241 Mobile Safari/534.11+' +windows_rt_ua_string = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; ARM; Trident/6.0)' +j2me_opera_ua_string = 'Opera/9.80 (J2ME/MIDP; Opera Mini/9.80 (J2ME/22.478; U; en) Presto/2.5.25 Version/10.54' +ie_ua_string = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)' +ie_touch_ua_string = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0; Touch)' +mac_safari_ua_string = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2' +windows_ie_ua_string = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)' +ubuntu_firefox_ua_string = 'Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1' +google_bot_ua_string = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' +nokia_n97_ua_string = 'Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/12.0.024; Profile/MIDP-2.1 Configuration/CLDC-1.1; en-us) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.12344' +android_firefox_aurora_ua_string = 'Mozilla/5.0 (Android; Mobile; rv:27.0) Gecko/27.0 Firefox/27.0' +thunderbird_ua_string = 'Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Thunderbird/38.2.0 Lightning/4.0.2' +outlook_usa_string = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; Microsoft Outlook 15.0.4420)' +chromebook_ua_string = 'Mozilla/5.0 (X11; CrOS i686 0.12.433) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.77 Safari/534.30' + +iphone_ua = parse(iphone_ua_string) +ipad_ua = parse(ipad_ua_string) +galaxy_tab = parse(galaxy_tab_ua_string) +galaxy_s3_ua = parse(galaxy_s3_ua_string) +kindle_fire_ua = parse(kindle_fire_ua_string) +playbook_ua = parse(playbook_ua_string) +nexus_7_ua = parse(nexus_7_ua_string) +windows_phone_ua = parse(windows_phone_ua_string) +windows_rt_ua = parse(windows_rt_ua_string) +blackberry_torch_ua = parse(blackberry_torch_ua_string) +blackberry_bold_ua = parse(blackberry_bold_ua_string) +blackberry_bold_touch_ua = parse(blackberry_bold_touch_ua_string) +j2me_opera_ua = parse(j2me_opera_ua_string) +ie_ua = parse(ie_ua_string) +ie_touch_ua = parse(ie_touch_ua_string) +mac_safari_ua = parse(mac_safari_ua_string) +windows_ie_ua = parse(windows_ie_ua_string) +ubuntu_firefox_ua = parse(ubuntu_firefox_ua_string) +google_bot_ua = parse(google_bot_ua_string) +nokia_n97_ua = parse(nokia_n97_ua_string) +android_firefox_aurora_ua = parse(android_firefox_aurora_ua_string) +thunderbird_ua = parse(thunderbird_ua_string) +outlook_ua = parse(outlook_usa_string) +chromebook_ua = parse(chromebook_ua_string) + + +class UserAgentsTest(unittest.TestCase): + + def test_user_agent_object_assignments(self): + ua_dict = user_agent_parser.Parse(devices['iphone']['ua_string']) + iphone_ua = devices['iphone']['user_agent'] + + # Ensure browser attributes are assigned correctly + self.assertEqual(iphone_ua.browser.family, + ua_dict['user_agent']['family']) + self.assertEqual( + iphone_ua.browser.version, + (int(ua_dict['user_agent']['major']), + int(ua_dict['user_agent']['minor'])) + ) + + # Ensure os attributes are assigned correctly + self.assertEqual(iphone_ua.os.family, ua_dict['os']['family']) + self.assertEqual( + iphone_ua.os.version, + (int(ua_dict['os']['major']), int(ua_dict['os']['minor'])) + ) + + # Ensure device attributes are assigned correctly + self.assertEqual(iphone_ua.device.family, + ua_dict['device']['family']) + + def test_is_tablet_property(self): + self.assertFalse(iphone_ua.is_tablet) + self.assertFalse(galaxy_s3_ua.is_tablet) + self.assertFalse(blackberry_torch_ua.is_tablet) + self.assertFalse(blackberry_bold_ua.is_tablet) + self.assertFalse(windows_phone_ua.is_tablet) + self.assertFalse(ie_ua.is_tablet) + self.assertFalse(ie_touch_ua.is_tablet) + self.assertFalse(mac_safari_ua.is_tablet) + self.assertFalse(windows_ie_ua.is_tablet) + self.assertFalse(ubuntu_firefox_ua.is_tablet) + self.assertFalse(j2me_opera_ua.is_tablet) + self.assertFalse(google_bot_ua.is_tablet) + self.assertFalse(nokia_n97_ua.is_tablet) + self.assertTrue(windows_rt_ua.is_tablet) + self.assertTrue(ipad_ua.is_tablet) + self.assertTrue(playbook_ua.is_tablet) + self.assertTrue(kindle_fire_ua.is_tablet) + self.assertTrue(nexus_7_ua.is_tablet) + self.assertFalse(android_firefox_aurora_ua.is_tablet) + + def test_is_mobile_property(self): + self.assertTrue(iphone_ua.is_mobile) + self.assertTrue(galaxy_s3_ua.is_mobile) + self.assertTrue(blackberry_torch_ua.is_mobile) + self.assertTrue(blackberry_bold_ua.is_mobile) + self.assertTrue(windows_phone_ua.is_mobile) + self.assertTrue(j2me_opera_ua.is_mobile) + self.assertTrue(nokia_n97_ua.is_mobile) + self.assertFalse(windows_rt_ua.is_mobile) + self.assertFalse(ipad_ua.is_mobile) + self.assertFalse(playbook_ua.is_mobile) + self.assertFalse(kindle_fire_ua.is_mobile) + self.assertFalse(nexus_7_ua.is_mobile) + self.assertFalse(ie_ua.is_mobile) + self.assertFalse(ie_touch_ua.is_mobile) + self.assertFalse(mac_safari_ua.is_mobile) + self.assertFalse(windows_ie_ua.is_mobile) + self.assertFalse(ubuntu_firefox_ua.is_mobile) + self.assertFalse(google_bot_ua.is_mobile) + self.assertTrue(android_firefox_aurora_ua.is_mobile) + + def test_is_touch_property(self): + self.assertTrue(iphone_ua.is_touch_capable) + self.assertTrue(galaxy_s3_ua.is_touch_capable) + self.assertTrue(ipad_ua.is_touch_capable) + self.assertTrue(playbook_ua.is_touch_capable) + self.assertTrue(kindle_fire_ua.is_touch_capable) + self.assertTrue(nexus_7_ua.is_touch_capable) + self.assertTrue(windows_phone_ua.is_touch_capable) + self.assertTrue(ie_touch_ua.is_touch_capable) + self.assertTrue(blackberry_bold_touch_ua.is_mobile) + self.assertTrue(blackberry_torch_ua.is_mobile) + self.assertFalse(j2me_opera_ua.is_touch_capable) + self.assertFalse(ie_ua.is_touch_capable) + self.assertFalse(blackberry_bold_ua.is_touch_capable) + self.assertFalse(mac_safari_ua.is_touch_capable) + self.assertFalse(windows_ie_ua.is_touch_capable) + self.assertFalse(ubuntu_firefox_ua.is_touch_capable) + self.assertFalse(google_bot_ua.is_touch_capable) + self.assertFalse(nokia_n97_ua.is_touch_capable) + self.assertTrue(android_firefox_aurora_ua.is_touch_capable) + + def test_is_pc(self): + self.assertFalse(iphone_ua.is_pc) + self.assertFalse(galaxy_s3_ua.is_pc) + self.assertFalse(ipad_ua.is_pc) + self.assertFalse(playbook_ua.is_pc) + self.assertFalse(kindle_fire_ua.is_pc) + self.assertFalse(nexus_7_ua.is_pc) + self.assertFalse(windows_phone_ua.is_pc) + self.assertFalse(blackberry_bold_touch_ua.is_pc) + self.assertFalse(blackberry_torch_ua.is_pc) + self.assertFalse(blackberry_bold_ua.is_pc) + self.assertFalse(j2me_opera_ua.is_pc) + self.assertFalse(google_bot_ua.is_pc) + self.assertFalse(nokia_n97_ua.is_pc) + self.assertTrue(mac_safari_ua.is_pc) + self.assertTrue(windows_ie_ua.is_pc) + self.assertTrue(ubuntu_firefox_ua.is_pc) + self.assertTrue(ie_touch_ua.is_pc) + self.assertTrue(ie_ua.is_pc) + self.assertFalse(android_firefox_aurora_ua.is_pc) + self.assertTrue(chromebook_ua.is_pc) + + def test_is_bot(self): + self.assertTrue(google_bot_ua.is_bot) + self.assertFalse(iphone_ua.is_bot) + self.assertFalse(galaxy_s3_ua.is_bot) + self.assertFalse(ipad_ua.is_bot) + self.assertFalse(playbook_ua.is_bot) + self.assertFalse(kindle_fire_ua.is_bot) + self.assertFalse(nexus_7_ua.is_bot) + self.assertFalse(windows_phone_ua.is_bot) + self.assertFalse(blackberry_bold_touch_ua.is_bot) + self.assertFalse(blackberry_torch_ua.is_bot) + self.assertFalse(blackberry_bold_ua.is_bot) + self.assertFalse(j2me_opera_ua.is_bot) + self.assertFalse(mac_safari_ua.is_bot) + self.assertFalse(windows_ie_ua.is_bot) + self.assertFalse(ubuntu_firefox_ua.is_bot) + self.assertFalse(ie_touch_ua.is_bot) + self.assertFalse(ie_ua.is_bot) + self.assertFalse(nokia_n97_ua.is_bot) + self.assertFalse(android_firefox_aurora_ua.is_bot) + + def test_is_email_client(self): + self.assertTrue(thunderbird_ua.is_email_client) + self.assertTrue(outlook_ua.is_email_client) + self.assertFalse(playbook_ua.is_email_client) + self.assertFalse(kindle_fire_ua.is_email_client) + self.assertFalse(nexus_7_ua.is_email_client) + self.assertFalse(windows_phone_ua.is_email_client) + self.assertFalse(blackberry_bold_touch_ua.is_email_client) + self.assertFalse(blackberry_torch_ua.is_email_client) + self.assertFalse(blackberry_bold_ua.is_email_client) + self.assertFalse(j2me_opera_ua.is_email_client) + self.assertFalse(mac_safari_ua.is_email_client) + self.assertFalse(windows_ie_ua.is_email_client) + self.assertFalse(ubuntu_firefox_ua.is_email_client) + self.assertFalse(ie_touch_ua.is_email_client) + self.assertFalse(ie_ua.is_email_client) + self.assertFalse(nokia_n97_ua.is_email_client) + self.assertFalse(android_firefox_aurora_ua.is_email_client) + + + def test_strings(self): + self.assertEqual(str(iphone_ua), "iPhone / iOS 5.1 / Mobile Safari 5.1") + self.assertEqual(str(ipad_ua), "iPad / iOS 3.2 / Mobile Safari 4.0.4") + self.assertEqual(str(galaxy_tab), "Samsung SCH-I800 / Android 2.2 / Android 2.2") + self.assertEqual(str(galaxy_s3_ua), "Samsung GT-I9300 / Android 4.0.4 / Android 4.0.4") + self.assertEqual(str(kindle_fire_ua), "Kindle / Android / Amazon Silk 1.1.0-80") + self.assertEqual(str(playbook_ua), "BlackBerry Playbook / BlackBerry Tablet OS 2.0.1 / BlackBerry WebKit 2.0.1") + self.assertEqual(str(nexus_7_ua), "Asus Nexus 7 / Android 4.1.1 / Chrome 18.0.1025") + self.assertEqual(str(windows_phone_ua), "Samsung SGH-i917 / Windows Phone 7.5 / IE Mobile 9.0") + self.assertEqual(str(windows_rt_ua), "PC / Windows RT / IE 10.0") + self.assertEqual(str(blackberry_torch_ua), "BlackBerry 9800 / BlackBerry OS 6.0.0 / BlackBerry WebKit 6.0.0") + self.assertEqual(str(blackberry_bold_ua), "BlackBerry 9700 / BlackBerry OS 5.0.0 / BlackBerry 9700") + self.assertEqual(str(blackberry_bold_touch_ua), "BlackBerry 9930 / BlackBerry OS 7.0.0 / BlackBerry WebKit 7.0.0") + self.assertEqual(str(j2me_opera_ua), "Generic Feature Phone / Other / Opera Mini 9.80") + self.assertEqual(str(ie_ua), "PC / Windows 8 / IE 10.0") + self.assertEqual(str(ie_touch_ua), "PC / Windows 8 / IE 10.0") + self.assertEqual(str(mac_safari_ua), "PC / Mac OS X 10.6.8 / WebKit Nightly 537.13") + self.assertEqual(str(windows_ie_ua), "PC / Windows 7 / IE 9.0") + self.assertEqual(str(ubuntu_firefox_ua), "PC / Ubuntu / Firefox 15.0.1") + self.assertEqual(str(google_bot_ua), "Spider / Other / Googlebot 2.1") + self.assertEqual(str(nokia_n97_ua), "Nokia N97 / Symbian OS 9.4 / Nokia Browser 7.1.12344") + self.assertEqual(str(android_firefox_aurora_ua), "Generic Smartphone / Android / Firefox Mobile 27.0") + + def test_unicode_strings(self): + try: + # Python 2 + unicode_ua_str = unicode(devices['iphone']['user_agent']) + self.assertEqual(unicode_ua_str, + u"iPhone / iOS 5.1 / Mobile Safari 5.1") + self.assertTrue(isinstance(unicode_ua_str, unicode)) + except NameError: + # Python 3 + unicode_ua_str = str(devices['iphone']['user_agent']) + self.assertEqual(unicode_ua_str, + "iPhone / iOS 5.1 / Mobile Safari 5.1") + + +with open(os.path.join(os.path.dirname(__file__), 'devices.json')) as f: + devices = json.load(f) + + +def test_wrapper(items): + def test_func(self): + attrs = ('is_bot', 'is_mobile', + 'is_pc', 'is_tablet', 'is_touch_capable') + for attr in attrs: + self.assertEqual( + getattr(items['user_agent'], attr), items[attr], msg=attr) + # Temporarily commenting this out since UserAgent.device + # may return different string depending ua-parser version + # self.assertEqual(str(items['user_agent']), items['str']) + return test_func + +for device, items in compat.iteritems(devices): + items['user_agent'] = parse(items['ua_string']) + setattr(UserAgentsTest, 'test_' + device, test_wrapper(items)) diff --git a/app_common/models/__init__.py b/app_common/models/__init__.py index e98cf58e..cbd3a54e 100644 --- a/app_common/models/__init__.py +++ b/app_common/models/__init__.py @@ -23,7 +23,10 @@ # description: from . import base -# from . import fields -# from . import validator -# from . import ir_ui_view +from . import fields +from . import view_validation +from . import ir_ui_view +from . import ir_cron +from . import res_users + diff --git a/app_common/models/base.py b/app_common/models/base.py index 5f27377e..26d72f9a 100644 --- a/app_common/models/base.py +++ b/app_common/models/base.py @@ -2,34 +2,86 @@ from odoo import models, fields, api, _ from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +import requests +import base64 +from io import BytesIO + from datetime import date, datetime, time import pytz +import logging + +_logger = logging.getLogger(__name__) + +# 常规的排除的fields +EXCLU_FIELDS = [ + '__last_update', + 'access_token', + 'access_url', + 'access_warning', + 'activity_date_deadline', + 'activity_exception_decoration', + 'activity_exception_icon', + 'activity_ids', + 'activity_state', + 'activity_summary', + 'activity_type_id', + 'activity_user_id', + 'display_name', + 'message_attachment_count', + 'message_channel_ids', + 'message_follower_ids', + 'message_has_error', + 'message_has_error_counter', + 'message_has_sms_error', + 'message_ids', + 'message_is_follower', + 'message_main_attachment_id', + 'message_needaction', + 'message_needaction_counter', + 'message_partner_ids', + 'message_unread', + 'message_unread_counter', + 'website_message_ids', + 'write_date', + 'write_uid', +] + + class Base(models.AbstractModel): _inherit = 'base' + @api.model + def _get_normal_fields(self): + f_list = [] + for k, v in self._fields.items(): + if k not in EXCLU_FIELDS: + f_list.append(k) + return f_list + @api.model def _app_get_m2o_default(self, fieldname, domain=[]): if hasattr(self, fieldname) and self._fields[fieldname].type == 'many2one': if self._context.get(fieldname) or self._context.get('default_%s' % fieldname): return self._context.get(fieldname) or self._context.get('default_%s' % fieldname) else: - rec = self.env[self._fields[fieldname].comodel_name].search(domain, limit=1) + rec = self.env[self._fields[fieldname].comodel_name].sudo().search(domain, limit=1) return rec.id if rec else False return False def _app_dt2local(self, value, return_format=DEFAULT_SERVER_DATETIME_FORMAT): """ - 将value中时间,按格式转为用户本地时间 + 将value中时间,按格式转为用户本地时间.注意只处理in str为字符串类型,如果是时间类型直接用 datetime.now(tz) """ if not value: return value if isinstance(value, datetime): value = value.strftime(return_format) dt = datetime.strptime(value, return_format) - pytz_timezone = pytz.timezone(self.env.user.tz or 'Etc/GMT-8') + user_tz = pytz.timezone(self.env.user.tz or 'Etc/GMT-8') + _logger.warning('============= user2 tz: %s' % user_tz) dt = dt.replace(tzinfo=pytz.timezone('UTC')) - return dt.astimezone(pytz_timezone).strftime(return_format) + return dt.astimezone(user_tz).strftime(return_format) def _app_dt2utc(self, value, return_format=DEFAULT_SERVER_DATETIME_FORMAT): """ @@ -40,6 +92,17 @@ class Base(models.AbstractModel): if isinstance(value, datetime): value = value.strftime(return_format) dt = datetime.strptime(value, return_format) - user_tz = pytz.timezone(self.env.user.tz or 'Etc/GMT+8') + pytz_timezone = pytz.timezone('Etc/GMT+8') dt = dt.replace(tzinfo=pytz.timezone('UTC')) - return dt.astimezone(user_tz).strftime(return_format) + return dt.astimezone(pytz_timezone).strftime(return_format) + + @api.model + def get_image_from_url(self, url): + if not url: + return None + try: + response = requests.get(url) # 将这个图片保存在内存 + except Exception as e: + return None + # 返回这个图片的base64编码 + return base64.b64encode(BytesIO(response.content).read()) diff --git a/app_common/models/ir_cron.py b/app_common/models/ir_cron.py new file mode 100644 index 00000000..0d5955ad --- /dev/null +++ b/app_common/models/ir_cron.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging +from odoo import api, fields, models, modules, tools, _ + +_logger = logging.getLogger(__name__) + +class IrCron(models.Model): + _inherit = "ir.cron" + + trigger_user_id = fields.Many2one('res.users', string='Last Trigger User') + + def method_direct_trigger(self): + self.write({'trigger_user_id': self.env.user.id}) + return super(IrCron, self).method_direct_trigger() diff --git a/app_common/models/ir_ui_view.py b/app_common/models/ir_ui_view.py index 166f8240..a864de21 100644 --- a/app_common/models/ir_ui_view.py +++ b/app_common/models/ir_ui_view.py @@ -22,7 +22,7 @@ def app_relaxng(view_type): """ Return a validator for the given view type, or None. """ if view_type not in _relaxng_cache: # tree, search 特殊 - if view_type in ['tree', 'search']: + if view_type in ['tree', 'search', 'pivot']: _file = get_resource_path('app_common', 'rng', '%s_view.rng' % view_type) else: _file = get_resource_path('base', 'rng', '%s_view.rng' % view_type) @@ -31,7 +31,7 @@ def app_relaxng(view_type): relaxng_doc = etree.parse(frng) _relaxng_cache[view_type] = etree.RelaxNG(relaxng_doc) except Exception: - _logger.exception('Failed to load RelaxNG XML schema for views validation') + _logger.error('Failed to load RelaxNG XML schema for views validation') _relaxng_cache[view_type] = None return _relaxng_cache[view_type] diff --git a/app_common/models/res_users.py b/app_common/models/res_users.py new file mode 100644 index 00000000..1c18b7ad --- /dev/null +++ b/app_common/models/res_users.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, tools, _ + + +class ResUsers(models.Model): + _inherit = 'res.users' + + login = fields.Char(index=True) diff --git a/app_common/models/view_validation.py b/app_common/models/view_validation.py new file mode 100644 index 00000000..e6bac36e --- /dev/null +++ b/app_common/models/view_validation.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +import ast +from odoo.tools import view_validation +from odoo.tools.view_validation import get_attrs_field_names as old_gafn +from odoo.tools.view_validation import _get_attrs_symbols +import logging + +_logger = logging.getLogger(__name__) + +ATTRS_WITH_FIELD_NAMES2 = { + 'context', + 'domain', + 'decoration-bf', + 'decoration-it', + 'decoration-danger', + 'decoration-info', + 'decoration-muted', + 'decoration-primary', + 'decoration-success', + 'decoration-warning', + 'decoration-black', + 'decoration-white', + 'bg-danger', + 'bg-info', + 'bg-muted', + 'bg-primary', + 'bg-success', + 'bg-warning', + 'bg-black', + 'bg-white', +} + +def app_get_attrs_field_names(env, arch, model, editable): + symbols = _get_attrs_symbols() | {None} + result = [] + + def get_name(node): + """ return the name from an AST node, or None """ + if isinstance(node, ast.Name): + return node.id + + def process_expr(expr, get, key, val): + """ parse `expr` and collect triples """ + for node in ast.walk(ast.parse(expr.strip(), mode='eval')): + name = get(node) + if name not in symbols: + result.append((name, key, val)) + + def add_bg(node, model, editable, get=get_name): + for key, val in node.items(): + if not val: + continue + if key in ATTRS_WITH_FIELD_NAMES2: + process_expr(val, get, key, val) + + res = old_gafn(env, arch, model, editable) + add_bg(arch, model, editable) + res += result + return res + +# 使用猴子补丁方式更新 +view_validation.get_attrs_field_names = app_get_attrs_field_names diff --git a/app_common/rng/common.rng b/app_common/rng/common.rng index a62f159a..5aff83fc 100644 --- a/app_common/rng/common.rng +++ b/app_common/rng/common.rng @@ -268,6 +268,7 @@ + @@ -276,7 +277,16 @@ - + + + + + + + + + + diff --git a/app_common/rng/tree_view.rng b/app_common/rng/tree_view.rng index a04eb2b3..fd800c55 100644 --- a/app_common/rng/tree_view.rng +++ b/app_common/rng/tree_view.rng @@ -28,6 +28,7 @@ + @@ -45,6 +46,16 @@ + + + + + + + + + + diff --git a/app_common/static/description/index.html b/app_common/static/description/index.html index cc311fb7..47d7a9c6 100644 --- a/app_common/static/description/index.html +++ b/app_common/static/description/index.html @@ -36,6 +36,17 @@ +
+
+

Setup, please run the follow command to install the lib.

+

pip install pyyaml ua-parser user-agents

+
+ +
+
+
+ +

So Easy to navigator and search any data.

diff --git a/app_common/views/ir_cron_views.xml b/app_common/views/ir_cron_views.xml new file mode 100644 index 00000000..4ba63f0b --- /dev/null +++ b/app_common/views/ir_cron_views.xml @@ -0,0 +1,13 @@ + + + + app.ir.cron.tree + ir.cron + + + + + + + + \ No newline at end of file