From d8a88c53e2bfb132edd10144d18387b5062a4575 Mon Sep 17 00:00:00 2001 From: amerkurev Date: Mon, 15 Jan 2024 00:36:56 +0300 Subject: [PATCH] feat(query_params): add device query-parameter - Add support for the `device` parameter in BrowserQueryParams class to simulate specific device behaviors. - Deprecate explicit `viewport-width`, `viewport-height`, `screen-width`, and `screen-height` in favor of using the `device` parameter. BREAKING CHANGE: Default values for `viewport-width`, `viewport-height`, `screen-width`, and `screen-height` have been removed. Users should now use the `device` parameter to specify device-specific behaviors. --- README.md | 37 +- app/dependencies.py | 3 +- app/internal/browser.py | 52 +- app/internal/deviceDescriptorsSource.json | 1549 +++++++++++++++++++++ app/main.py | 3 +- app/routers/article.py | 4 +- app/routers/links.py | 4 +- app/routers/query_params.py | 48 +- app/settings.py | 14 + load_testing/load_test.py | 4 +- 10 files changed, 1658 insertions(+), 60 deletions(-) create mode 100644 app/internal/deviceDescriptorsSource.json diff --git a/README.md b/README.md index e72972d..37968ca 100644 --- a/README.md +++ b/README.md @@ -124,24 +124,25 @@ All other request parameters are optional and have default values. However, you | `user-scripts-timeout` | Waits for the given timeout in milliseconds after users scripts injection. For example if you want to navigate through page to specific content, set a longer period (higher value). The default value is 0, which means no sleep. | `0` | #### Browser settings -| Parameter | Description | Default | -|:-----------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------| -| `incognito` | Allows creating `incognito` browser contexts. Incognito browser contexts don't write any browsing data to disk. | `true` | -| `timeout` | Maximum operation time to navigate to the page in milliseconds; defaults to 60000 (60 seconds). Pass 0 to disable the timeout. | `60000` | -| `wait-until` | When to consider navigation succeeded, defaults to `domcontentloaded`. Events can be either:
`load` - consider operation to be finished when the `load` event is fired.
`domcontentloaded` - consider operation to be finished when the DOMContentLoaded event is fired.
`networkidle` - consider operation to be finished when there are no network connections for at least 500 ms.
`commit` - consider operation to be finished when network response is received and the document started loading. | `domcontentloaded` | -| `sleep` | Waits for the given timeout in milliseconds before parsing the article, and after the page has loaded. In many cases, a sleep timeout is not necessary. However, for some websites, it can be quite useful. Other waiting mechanisms, such as waiting for selector visibility, are not currently supported. The default value is 0, which means no sleep. | `0` | -| `resource` | List of resource types allowed to be loaded on the page. All other resources will not be allowed, and their network requests will be aborted. **By default, all resource types are allowed.** The following resource types are supported: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`. Example: `document,stylesheet,fetch`. | | -| `viewport-width` | The viewport width in pixels. | `414` | -| `viewport-height` | The viewport height in pixels. | `896` | -| `screen-width` | The page width in pixels. Emulates consistent window screen size available inside web page via window.screen. Is only used when the viewport is set. | `828` | -| `screen-height` | The page height in pixels. | `1792` | -| `scroll-down` | Scroll down the page by a specified number of pixels. This is particularly useful when dealing with lazy-loading pages (pages that are loaded only as you scroll down). This parameter is used in conjunction with the `sleep` parameter. Make sure to set a positive value for the `sleep` parameter, otherwise, the scroll function won't work. | `0` | -| `ignore-https-errors` | Whether to ignore HTTPS errors when sending network requests. The default setting is to ignore HTTPS errors. | `true` | -| `user-agent` | Specific user agent. | | -| `locale` | Specify user locale, for example en-GB, de-DE, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting rules. | | -| `timezone` | Changes the timezone of the context. See ICU's metaZones.txt for a list of supported timezone IDs. | | -| `http-credentials` | Credentials for HTTP authentication (string containing username and password separated by a colon, e.g. `username:password`). | | -| `extra-http-headers` | Contains additional HTTP headers to be sent with every request. Example: `X-API-Key:123456;X-Auth-Token:abcdef`. | | +| Parameter | Description | Default | +|:----------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------| +| `incognito` | Allows creating `incognito` browser contexts. Incognito browser contexts don't write any browsing data to disk. | `true` | +| `timeout` | Maximum operation time to navigate to the page in milliseconds; defaults to 60000 (60 seconds). Pass 0 to disable the timeout. | `60000` | +| `wait-until` | When to consider navigation succeeded, defaults to `domcontentloaded`. Events can be either:
`load` - consider operation to be finished when the `load` event is fired.
`domcontentloaded` - consider operation to be finished when the DOMContentLoaded event is fired.
`networkidle` - consider operation to be finished when there are no network connections for at least 500 ms.
`commit` - consider operation to be finished when network response is received and the document started loading. | `domcontentloaded` | +| `sleep` | Waits for the given timeout in milliseconds before parsing the article, and after the page has loaded. In many cases, a sleep timeout is not necessary. However, for some websites, it can be quite useful. Other waiting mechanisms, such as waiting for selector visibility, are not currently supported. The default value is 0, which means no sleep. | `0` | +| `resource` | List of resource types allowed to be loaded on the page. All other resources will not be allowed, and their network requests will be aborted. **By default, all resource types are allowed.** The following resource types are supported: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`. Example: `document,stylesheet,fetch`. | | +| `viewport-width` | The viewport width in pixels. It's better to use the `device` parameter instead of specifying it explicitly. | | +| `viewport-height` | The viewport height in pixels. It's better to use the `device` parameter instead of specifying it explicitly. | | +| `screen-width` | The page width in pixels. Emulates consistent window screen size available inside web page via window.screen. Is only used when the viewport is set. | | +| `screen-height` | The page height in pixels. | | +| `device` | Simulates browser behavior for a specific device, such as user agent, screen size, viewport, and whether it has touch enabled.
Individual parameters like `user-agent`, `viewport-width`, and `viewport-height` can also be used; in such cases, they will override the `device` settings.
List of [available devices](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json). | `iPhone 12` | +| `scroll-down` | Scroll down the page by a specified number of pixels. This is particularly useful when dealing with lazy-loading pages (pages that are loaded only as you scroll down). This parameter is used in conjunction with the `sleep` parameter. Make sure to set a positive value for the `sleep` parameter, otherwise, the scroll function won't work. | `0` | +| `ignore-https-errors` | Whether to ignore HTTPS errors when sending network requests. The default setting is to ignore HTTPS errors. | `true` | +| `user-agent` | Specific user agent. It's better to use the `device` parameter instead of specifying it explicitly. | | +| `locale` | Specify user locale, for example en-GB, de-DE, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting rules. | | +| `timezone` | Changes the timezone of the context. See ICU's metaZones.txt for a list of supported timezone IDs. | | +| `http-credentials` | Credentials for HTTP authentication (string containing username and password separated by a colon, e.g. `username:password`). | | +| `extra-http-headers` | Contains additional HTTP headers to be sent with every request. Example: `X-API-Key:123456;X-Auth-Token:abcdef`. | | #### Network proxy settings | Parameter | Description | Default | diff --git a/app/dependencies.py b/app/dependencies.py index 7b4d0bb..6f446db 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -12,6 +12,7 @@ class State(TypedDict): # https://playwright.dev/python/docs/api/class-browsertype browser: Browser + semaphore: asyncio.Semaphore @contextlib.asynccontextmanager @@ -22,4 +23,4 @@ async def lifespan(_: FastAPI): async with async_playwright() as playwright: firefox = playwright.firefox browser = await firefox.launch(headless=True) - yield {'browser': browser, 'semaphore': semaphore} + yield State(browser=browser, semaphore=semaphore) diff --git a/app/internal/browser.py b/app/internal/browser.py index 9ac89f8..b0d8579 100644 --- a/app/internal/browser.py +++ b/app/internal/browser.py @@ -1,4 +1,5 @@ import contextlib +import copy from collections.abc import Sequence from playwright.async_api import Browser, BrowserContext, Page, Route @@ -11,56 +12,73 @@ STEALTH_SCRIPTS_DIR, SCREENSHOT_TYPE, SCREENSHOT_QUALITY, + DEVICE_REGISTRY, ) +def get_device(device: str) -> dict: + return copy.deepcopy(DEVICE_REGISTRY[device]) + + @contextlib.asynccontextmanager async def new_context( browser: Browser, params: BrowserQueryParams, proxy: ProxyQueryParams, ) -> BrowserContext: - # https://playwright.dev/python/docs/api/class-browser#browser-new-context - browser_args = { + # https://playwright.dev/python/docs/emulation + options = get_device(params.device) + + # PlaywrightError: options.isMobile is not supported in Firefox + del options['is_mobile'] + + options |= { 'bypass_csp': True, - 'viewport': { - 'width': params.viewport_width, - 'height': params.viewport_height, - }, - 'screen': { - 'width': params.screen_width, - 'height': params.screen_height, - }, 'ignore_https_errors': params.ignore_https_errors, - 'user_agent': params.user_agent, 'locale': params.locale, 'timezone_id': params.timezone, 'http_credentials': params.http_credentials, 'extra_http_headers': params.extra_http_headers, } + + # viewport and screen settings: + if params.viewport_width: + options['viewport']['width'] = params.viewport_width + if params.viewport_height: + options['viewport']['height'] = params.viewport_height + if params.screen_width: + options['screen']['width'] = params.screen_width + if params.screen_height: + options['screen']['height'] = params.screen_height + + # user agent settings: + if params.user_agent: + options['user_agent'] = params.user_agent + # proxy settings: if proxy.proxy_server: - browser_args['proxy'] = { + options['proxy'] = { 'server': proxy.proxy_server, } if proxy.proxy_username: - browser_args['proxy']['username'] = proxy.proxy_username + options['proxy']['username'] = proxy.proxy_username if proxy.proxy_password: - browser_args['proxy']['password'] = proxy.proxy_password + options['proxy']['password'] = proxy.proxy_password if proxy.proxy_bypass: - browser_args['proxy']['bypass'] = proxy.proxy_bypass + options['proxy']['bypass'] = proxy.proxy_bypass + # https://playwright.dev/python/docs/api/class-browser#browser-new-context if params.incognito: # create a new incognito browser context # (more efficient way, because it doesn't create a new browser instance) - context = await browser.new_context(**browser_args) + context = await browser.new_context(**options) else: # create a persistent browser context # (less efficient way, because it creates a new browser instance) context = await browser.browser_type.launch_persistent_context( headless=True, user_data_dir=USER_DATA_DIR, - **browser_args, + **options, ) try: yield context diff --git a/app/internal/deviceDescriptorsSource.json b/app/internal/deviceDescriptorsSource.json new file mode 100644 index 0000000..d2670c4 --- /dev/null +++ b/app/internal/deviceDescriptorsSource.json @@ -0,0 +1,1549 @@ +{ + "Blackberry PlayBook": { + "user_agent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/17.4 Safari/536.2+", + "viewport": { + "width": 600, + "height": 1024 + }, + "device_scale_factor": 1, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Blackberry PlayBook landscape": { + "user_agent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/17.4 Safari/536.2+", + "viewport": { + "width": 1024, + "height": 600 + }, + "device_scale_factor": 1, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "BlackBerry Z30": { + "user_agent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/17.4 Mobile Safari/537.10+", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "BlackBerry Z30 landscape": { + "user_agent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/17.4 Mobile Safari/537.10+", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Galaxy Note 3": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/17.4 Mobile Safari/534.30", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Galaxy Note 3 landscape": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/17.4 Mobile Safari/534.30", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Galaxy Note II": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/17.4 Mobile Safari/534.30", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Galaxy Note II landscape": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/17.4 Mobile Safari/534.30", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Galaxy S III": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/17.4 Mobile Safari/534.30", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Galaxy S III landscape": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/17.4 Mobile Safari/534.30", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Galaxy S5": { + "user_agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Galaxy S5 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Galaxy S8": { + "user_agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 360, + "height": 740 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Galaxy S8 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 740, + "height": 360 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Galaxy S9+": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 320, + "height": 658 + }, + "device_scale_factor": 4.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Galaxy S9+ landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 658, + "height": 320 + }, + "device_scale_factor": 4.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Galaxy Tab S4": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "viewport": { + "width": 712, + "height": 1138 + }, + "device_scale_factor": 2.25, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Galaxy Tab S4 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "viewport": { + "width": 1138, + "height": 712 + }, + "device_scale_factor": 2.25, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "iPad (gen 5)": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 768, + "height": 1024 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad (gen 5) landscape": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 1024, + "height": 768 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad (gen 6)": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 768, + "height": 1024 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad (gen 6) landscape": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 1024, + "height": 768 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad (gen 7)": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 810, + "height": 1080 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad (gen 7) landscape": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 1080, + "height": 810 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad Mini": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 768, + "height": 1024 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad Mini landscape": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 1024, + "height": 768 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad Pro 11": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 834, + "height": 1194 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPad Pro 11 landscape": { + "user_agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 1194, + "height": 834 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 6": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 375, + "height": 667 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 6 landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 667, + "height": 375 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 6 Plus": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 414, + "height": 736 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 6 Plus landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 736, + "height": 414 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 7": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 375, + "height": 667 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 7 landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 667, + "height": 375 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 7 Plus": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 414, + "height": 736 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 7 Plus landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 736, + "height": 414 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 8": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 375, + "height": 667 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 8 landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 667, + "height": 375 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 8 Plus": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 414, + "height": 736 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 8 Plus landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 736, + "height": 414 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone SE": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/17.4 Mobile/14E304 Safari/602.1", + "viewport": { + "width": 320, + "height": 568 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone SE landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/17.4 Mobile/14E304 Safari/602.1", + "viewport": { + "width": 568, + "height": 320 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone X": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 375, + "height": 812 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone X landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1", + "viewport": { + "width": 812, + "height": 375 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone XR": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 414, + "height": 896 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone XR landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "viewport": { + "width": 896, + "height": 414 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 11": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 414, + "height": 896 + }, + "viewport": { + "width": 414, + "height": 715 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 11 landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 414, + "height": 896 + }, + "viewport": { + "width": 800, + "height": 364 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 11 Pro": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 375, + "height": 812 + }, + "viewport": { + "width": 375, + "height": 635 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 11 Pro landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 375, + "height": 812 + }, + "viewport": { + "width": 724, + "height": 325 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 11 Pro Max": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 414, + "height": 896 + }, + "viewport": { + "width": 414, + "height": 715 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 11 Pro Max landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 414, + "height": 896 + }, + "viewport": { + "width": 808, + "height": 364 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 390, + "height": 664 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12 landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 750, + "height": 340 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12 Pro": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 390, + "height": 664 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12 Pro landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 750, + "height": 340 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12 Pro Max": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 428, + "height": 926 + }, + "viewport": { + "width": 428, + "height": 746 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12 Pro Max landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 428, + "height": 926 + }, + "viewport": { + "width": 832, + "height": 378 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12 Mini": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 375, + "height": 812 + }, + "viewport": { + "width": 375, + "height": 629 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 12 Mini landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 375, + "height": 812 + }, + "viewport": { + "width": 712, + "height": 325 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 390, + "height": 664 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13 landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 750, + "height": 342 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13 Pro": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 390, + "height": 664 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13 Pro landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 750, + "height": 342 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13 Pro Max": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 428, + "height": 926 + }, + "viewport": { + "width": 428, + "height": 746 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13 Pro Max landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 428, + "height": 926 + }, + "viewport": { + "width": 832, + "height": 380 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13 Mini": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 375, + "height": 812 + }, + "viewport": { + "width": 375, + "height": 629 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 13 Mini landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 375, + "height": 812 + }, + "viewport": { + "width": 712, + "height": 327 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 390, + "height": 664 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14 landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 390, + "height": 844 + }, + "viewport": { + "width": 750, + "height": 340 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14 Plus": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 428, + "height": 926 + }, + "viewport": { + "width": 428, + "height": 746 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14 Plus landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 428, + "height": 926 + }, + "viewport": { + "width": 832, + "height": 378 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14 Pro": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 393, + "height": 852 + }, + "viewport": { + "width": 393, + "height": 660 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14 Pro landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 393, + "height": 852 + }, + "viewport": { + "width": 734, + "height": 343 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14 Pro Max": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 430, + "height": 932 + }, + "viewport": { + "width": 430, + "height": 740 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "iPhone 14 Pro Max landscape": { + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "screen": { + "width": 430, + "height": 932 + }, + "viewport": { + "width": 814, + "height": 380 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Kindle Fire HDX": { + "user_agent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", + "viewport": { + "width": 800, + "height": 1280 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Kindle Fire HDX landscape": { + "user_agent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", + "viewport": { + "width": 1280, + "height": 800 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "LG Optimus L70": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 384, + "height": 640 + }, + "device_scale_factor": 1.25, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "LG Optimus L70 landscape": { + "user_agent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 640, + "height": 384 + }, + "device_scale_factor": 1.25, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Microsoft Lumia 550": { + "user_agent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36 Edge/14.14263", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Microsoft Lumia 550 landscape": { + "user_agent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36 Edge/14.14263", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Microsoft Lumia 950": { + "user_agent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36 Edge/14.14263", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 4, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Microsoft Lumia 950 landscape": { + "user_agent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36 Edge/14.14263", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 4, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 10": { + "user_agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "viewport": { + "width": 800, + "height": 1280 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 10 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "viewport": { + "width": 1280, + "height": 800 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 4": { + "user_agent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 384, + "height": 640 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 4 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 640, + "height": 384 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 5": { + "user_agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 5 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 5X": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 412, + "height": 732 + }, + "device_scale_factor": 2.625, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 5X landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 732, + "height": 412 + }, + "device_scale_factor": 2.625, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 6": { + "user_agent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 412, + "height": 732 + }, + "device_scale_factor": 3.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 6 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 732, + "height": 412 + }, + "device_scale_factor": 3.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 6P": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 412, + "height": 732 + }, + "device_scale_factor": 3.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 6P landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 732, + "height": 412 + }, + "device_scale_factor": 3.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 7": { + "user_agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "viewport": { + "width": 600, + "height": 960 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nexus 7 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "viewport": { + "width": 960, + "height": 600 + }, + "device_scale_factor": 2, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nokia Lumia 520": { + "user_agent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)", + "viewport": { + "width": 320, + "height": 533 + }, + "device_scale_factor": 1.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nokia Lumia 520 landscape": { + "user_agent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)", + "viewport": { + "width": 533, + "height": 320 + }, + "device_scale_factor": 1.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Nokia N9": { + "user_agent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13", + "viewport": { + "width": 480, + "height": 854 + }, + "device_scale_factor": 1, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Nokia N9 landscape": { + "user_agent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13", + "viewport": { + "width": 854, + "height": 480 + }, + "device_scale_factor": 1, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "webkit" + }, + "Pixel 2": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 411, + "height": 731 + }, + "device_scale_factor": 2.625, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 2 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 731, + "height": 411 + }, + "device_scale_factor": 2.625, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 2 XL": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 411, + "height": 823 + }, + "device_scale_factor": 3.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 2 XL landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 823, + "height": 411 + }, + "device_scale_factor": 3.5, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 3": { + "user_agent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 393, + "height": 786 + }, + "device_scale_factor": 2.75, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 3 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 786, + "height": 393 + }, + "device_scale_factor": 2.75, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 4": { + "user_agent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 353, + "height": 745 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 4 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 745, + "height": 353 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 4a (5G)": { + "user_agent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "screen": { + "width": 412, + "height": 892 + }, + "viewport": { + "width": 412, + "height": 765 + }, + "device_scale_factor": 2.63, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 4a (5G) landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "screen": { + "height": 892, + "width": 412 + }, + "viewport": { + "width": 840, + "height": 312 + }, + "device_scale_factor": 2.63, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 5": { + "user_agent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "screen": { + "width": 393, + "height": 851 + }, + "viewport": { + "width": 393, + "height": 727 + }, + "device_scale_factor": 2.75, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 5 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "screen": { + "width": 851, + "height": 393 + }, + "viewport": { + "width": 802, + "height": 293 + }, + "device_scale_factor": 2.75, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 7": { + "user_agent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "screen": { + "width": 412, + "height": 915 + }, + "viewport": { + "width": 412, + "height": 839 + }, + "device_scale_factor": 2.625, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Pixel 7 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "screen": { + "width": 915, + "height": 412 + }, + "viewport": { + "width": 863, + "height": 360 + }, + "device_scale_factor": 2.625, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Moto G4": { + "user_agent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 360, + "height": 640 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Moto G4 landscape": { + "user_agent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Mobile Safari/537.36", + "viewport": { + "width": 640, + "height": 360 + }, + "device_scale_factor": 3, + "is_mobile": true, + "has_touch": true, + "default_browser_type": "chromium" + }, + "Desktop Chrome HiDPI": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "screen": { + "width": 1792, + "height": 1120 + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "device_scale_factor": 2, + "is_mobile": false, + "has_touch": false, + "default_browser_type": "chromium" + }, + "Desktop Edge HiDPI": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36 Edg/121.0.6167.57", + "screen": { + "width": 1792, + "height": 1120 + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "device_scale_factor": 2, + "is_mobile": false, + "has_touch": false, + "default_browser_type": "chromium" + }, + "Desktop Firefox HiDPI": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", + "screen": { + "width": 1792, + "height": 1120 + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "device_scale_factor": 2, + "is_mobile": false, + "has_touch": false, + "default_browser_type": "firefox" + }, + "Desktop Safari": { + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15", + "screen": { + "width": 1792, + "height": 1120 + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "device_scale_factor": 2, + "is_mobile": false, + "has_touch": false, + "default_browser_type": "webkit" + }, + "Desktop Chrome": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36", + "screen": { + "width": 1920, + "height": 1080 + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "device_scale_factor": 1, + "is_mobile": false, + "has_touch": false, + "default_browser_type": "chromium" + }, + "Desktop Edge": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.57 Safari/537.36 Edg/121.0.6167.57", + "screen": { + "width": 1920, + "height": 1080 + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "device_scale_factor": 1, + "is_mobile": false, + "has_touch": false, + "default_browser_type": "chromium" + }, + "Desktop Firefox": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", + "screen": { + "width": 1920, + "height": 1080 + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "device_scale_factor": 1, + "is_mobile": false, + "has_touch": false, + "default_browser_type": "firefox" + } +} diff --git a/app/main.py b/app/main.py index dabe7d8..d8aa1b5 100644 --- a/app/main.py +++ b/app/main.py @@ -54,8 +54,7 @@ async def root(request: Request): 'timeout=60000', 'wait-until=domcontentloaded', 'sleep=0', - 'viewport-width=414', - 'viewport-height=896', + 'device=iPhone 12', ) context = { 'request': request, diff --git a/app/routers/article.py b/app/routers/article.py index bdc253d..022a6be 100644 --- a/app/routers/article.py +++ b/app/routers/article.py @@ -101,8 +101,8 @@ async def parse_article( 'nbTopCandidates': readability_params.nb_top_candidates, 'charThreshold': readability_params.char_threshold, } - with open(PARSER_SCRIPTS_DIR / 'article.js', encoding='utf-8') as fd: - article = await page.evaluate(fd.read() % parser_args) + with open(PARSER_SCRIPTS_DIR / 'article.js', encoding='utf-8') as f: + article = await page.evaluate(f.read() % parser_args) if article is None: raise ArticleParsingError(page_url, "The page doesn't contain any articles.") # pragma: no cover diff --git a/app/routers/links.py b/app/routers/links.py index 557ce8a..4d50538 100644 --- a/app/routers/links.py +++ b/app/routers/links.py @@ -93,8 +93,8 @@ async def parser_links( # evaluating JavaScript: parse DOM and extract links of articles parser_args = {} - with open(PARSER_SCRIPTS_DIR / 'links.js', encoding='utf-8') as fd: - links = await page.evaluate(fd.read() % parser_args) + with open(PARSER_SCRIPTS_DIR / 'links.js', encoding='utf-8') as f: + links = await page.evaluate(f.read() % parser_args) # parser error: links are not extracted, result has 'err' field if 'err' in links: diff --git a/app/routers/query_params.py b/app/routers/query_params.py index f241242..ee881f2 100644 --- a/app/routers/query_params.py +++ b/app/routers/query_params.py @@ -10,7 +10,7 @@ from fastapi import Query from internal.errors import QueryParsingError -from settings import USER_SCRIPTS_DIR +from settings import USER_SCRIPTS_DIR, DEVICE_REGISTRY class WaitUntilEnum(str, Enum): @@ -191,41 +191,53 @@ def __init__( ), ] = None, viewport_width: Annotated[ - int, + int | None, Query( alias='viewport-width', - description='The viewport width in pixels. The default value is 414 (iPhone 11 Viewport).

', + description='The viewport width in pixels. ' + "It's better to use the `device` parameter instead of specifying it explicitly.

", ge=1, ), - ] = 414, + ] = None, viewport_height: Annotated[ - int, + int | None, Query( alias='viewport-height', - description='The viewport height in pixels. The default value is 896 (iPhone 11 Viewport).

', + description='The viewport height in pixels. ' + "It's better to use the `device` parameter instead of specifying it explicitly.

", ge=1, ), - ] = 896, + ] = None, screen_width: Annotated[ - int, + int | None, Query( alias='screen-width', description='Emulates consistent window screen size available inside web page via window.screen. ' - 'Is only used when the viewport is set.
' - 'The page width in pixels. Defaults to 828 (iPhone 11 Resolution).

', + 'Is only used when the viewport is set. The page width in pixels.

', ge=1, ), - ] = 828, + ] = None, screen_height: Annotated[ - int, + int | None, Query( alias='screen-height', description='Emulates consistent window screen size available inside web page via window.screen. ' - 'Is only used when the viewport is set.
' - 'The page height in pixels. Defaults to 1792 (iPhone 11 Resolution).

', + 'Is only used when the viewport is set. The page height in pixels.

', ge=1, ), - ] = 1792, + ] = None, + device: Annotated[ + str, + Query( + description=( + 'Simulates browser behavior for a specific device, such as user agent, screen size, viewport, ' + 'and whether it has touch enabled.
Individual parameters like `user-agent`, `viewport-width`, and `viewport-height` ' + 'can also be used; in such cases, they will override the `device` settings.
' + 'List of [available devices]' + '(https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json).

' + ), + ), + ] = 'iPhone 12', scroll_down: Annotated[ int, Query( @@ -253,7 +265,7 @@ def __init__( Query( alias='user-agent', description='Specify user agent to emulate.
' - 'By default, Playwright uses a user agent that matches the browser version.', + "It's better to use the `device` parameter instead of specifying it explicitly.

", ), ] = None, locale: Annotated[ @@ -296,6 +308,7 @@ def __init__( self.viewport_height = viewport_height self.screen_width = screen_width self.screen_height = screen_height + self.device = device self.scroll_down = scroll_down self.ignore_https_errors = ignore_https_errors self.user_agent = user_agent @@ -309,6 +322,9 @@ def __init__( if resource: self.resource = resource + if device not in DEVICE_REGISTRY: + raise QueryParsingError('device', 'Device not found', device) + if http_credentials: fake_url = f'http://{http_credentials}@localhost' try: diff --git a/app/settings.py b/app/settings.py index 227f37d..94fd99f 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,4 +1,6 @@ +import json import os +from functools import cache from pathlib import Path @@ -21,3 +23,15 @@ assert BROWSER_CONTEXT_LIMIT > 0, 'BROWSER_CONTEXT_LIMIT must be greater than 0' assert SCREENSHOT_TYPE in ('jpeg', 'png'), 'SCREENSHOT_TYPE must be jpeg or png' assert 0 <= SCREENSHOT_QUALITY <= 100, 'SCREENSHOT_QUALITY must be between 0 and 100' + + +@cache +def load_device_registry(): + # https://playwright.dev/python/docs/emulation#devices + # https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json + src_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'internal', 'deviceDescriptorsSource.json') + with open(src_file, encoding='utf-8') as f: + return json.load(f) + + +DEVICE_REGISTRY = load_device_registry() diff --git a/load_testing/load_test.py b/load_testing/load_test.py index 02d5b06..b23bf72 100644 --- a/load_testing/load_test.py +++ b/load_testing/load_test.py @@ -142,8 +142,8 @@ def process_args() -> Options: def get_pages_from_file(filename: str) -> list[str]: - with open(filename, 'r') as fd: - pages = list(filter(None, map(str.strip, fd))) + with open(filename, 'r') as f: + pages = list(filter(None, map(str.strip, f))) return pages