@unavatar/core 3.7.57 → 3.7.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@unavatar/core",
3
3
  "description": "Get unified user avatar from social networks, including Instagram, SoundCloud, Telegram, Twitter, YouTube & more.",
4
4
  "homepage": "https://unavatar.io",
5
- "version": "3.7.57",
5
+ "version": "3.7.58",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js",
@@ -115,6 +115,7 @@
115
115
  "@keyvhq/core": "~2.1.15",
116
116
  "@keyvhq/memoize": "~2.1.16",
117
117
  "@keyvhq/multi": "~2.1.15",
118
+ "@keyvhq/offline": "~2.2.0",
118
119
  "@keyvhq/redis": "~2.1.15",
119
120
  "@metascraper/helpers": "~5.49.24",
120
121
  "@microlink/mql": "~0.14.2",
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const DEFAULTS = require('./constant')
4
4
 
5
- module.exports = ({ constants: userConstants } = {}) => {
5
+ module.exports = ({ constants: userConstants, redis, onFetchHTML } = {}) => {
6
6
  const constants = { ...DEFAULTS, ...userConstants }
7
7
 
8
8
  if (userConstants?.REQUEST_TIMEOUT && !userConstants?.PROXY_TIMEOUT) {
@@ -12,15 +12,20 @@ module.exports = ({ constants: userConstants } = {}) => {
12
12
  constants.DNS_TIMEOUT = Math.floor(constants.REQUEST_TIMEOUT * (1 / 5))
13
13
  }
14
14
 
15
- const { createMemoryCache, createRedisCache } = require('./util/keyv')(constants)
16
- const cacheableLookup = require('./util/cacheable-lookup')({ ...constants, createMemoryCache })
15
+ const { createMultiCache, createRedisCache } = require('./util/keyv')({ ...constants, redis })
16
+ const cache = require('./util/cache')({ createMultiCache, createRedisCache })
17
+ const cacheableLookup = require('./util/cacheable-lookup')({ ...constants, cache: cache.dnsCache })
17
18
  const got = require('./util/got')({ cacheableLookup })
18
- const reachableUrl = require('./util/reachable-url')({ got, createMemoryCache })
19
+ const reachableUrl = require('./util/reachable-url')({ got, pingCache: cache.pingCache })
19
20
  const createBrowser = require('./util/browserless')(constants)
20
21
  const getHTML = require('./util/html-get')({ createBrowser, got })
21
- const { createHtmlProvider, getOgImage } = require('./util/html-provider')({ ...constants, getHTML })
22
+ const { createHtmlProvider, getOgImage } = require('./util/html-provider')({
23
+ ...constants,
24
+ getHTML,
25
+ onFetchHTML
26
+ })
22
27
 
23
- const providerCtx = { constants, createHtmlProvider, getOgImage, got, createRedisCache }
28
+ const providerCtx = { constants, createHtmlProvider, getOgImage, got, itunesSearchCache: cache.itunesSearchCache }
24
29
  const { providers, providersBy } = require('./providers')(providerCtx)
25
30
 
26
31
  const { auto, getInputType, getAvatar } = require('./avatar/auto')({
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { $jsonld } = require('@metascraper/helpers')
4
4
  const memoize = require('@keyvhq/memoize')
5
- const ms = require('ms')
6
5
 
7
6
  const APPLE_MUSIC_ID_REGEX = /^\d+$/
8
7
  const APPLE_MUSIC_STOREFRONT = 'us'
@@ -13,7 +12,6 @@ const APPLE_MUSIC_ENTITY_TYPES = {
13
12
  }
14
13
 
15
14
  const APPLE_MUSIC_SEARCH_TYPES = ['artist', 'song', 'album']
16
- const ITUNES_SEARCH_CACHE_TTL = ms('7d')
17
15
 
18
16
  const isNumericId = value => {
19
17
  if (typeof value === 'number') {
@@ -37,12 +35,7 @@ const parseInput = input => {
37
35
  return { hasExplicitType, type, id }
38
36
  }
39
37
 
40
- module.exports = ({ createHtmlProvider, createRedisCache, got }) => {
41
- const itunesSearchCache = createRedisCache({
42
- namespace: 'itunes-search',
43
- ttl: ITUNES_SEARCH_CACHE_TTL
44
- })
45
-
38
+ module.exports = ({ createHtmlProvider, itunesSearchCache, got }) => {
46
39
  const searchEntityId = memoize(
47
40
  async ({ query, type }) => {
48
41
  const entityConfig = APPLE_MUSIC_ENTITY_TYPES[type]
@@ -61,10 +54,7 @@ module.exports = ({ createHtmlProvider, createRedisCache, got }) => {
61
54
  return isNumericId(entityId) ? String(entityId) : null
62
55
  },
63
56
  itunesSearchCache,
64
- {
65
- key: ({ query, type }) => `${type}:${query.trim().toLowerCase()}`,
66
- ttl: ITUNES_SEARCH_CACHE_TTL
67
- }
57
+ { key: ({ query, type }) => `${type}:${query.trim().toLowerCase()}` }
68
58
  )
69
59
 
70
60
  const appleMusicURI = async input => {
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const ms = require('ms')
4
+
5
+ module.exports = ({ createMultiCache, createRedisCache }) => ({
6
+ dnsCache: createMultiCache(createRedisCache({ namespace: 'dns', ttl: ms('1d') })),
7
+ pingCache: createMultiCache(createRedisCache({ namespace: 'ping', ttl: ms('1d') })),
8
+ itunesSearchCache: createRedisCache({ namespace: 'itunes-search', ttl: ms('7d') })
9
+ })
@@ -3,10 +3,10 @@
3
3
  const CacheableLookup = require('cacheable-lookup')
4
4
  const Tangerine = require('tangerine')
5
5
 
6
- module.exports = ({ TTL_DEFAULT, DNS_TIMEOUT, DNS_SERVERS, createMemoryCache }) =>
6
+ module.exports = ({ TTL_DEFAULT, DNS_TIMEOUT, DNS_SERVERS, cache }) =>
7
7
  new CacheableLookup({
8
8
  maxTtl: TTL_DEFAULT,
9
- cache: createMemoryCache({ namespace: 'dns' }),
9
+ cache,
10
10
  resolver: new Tangerine(
11
11
  {
12
12
  cache: false,
@@ -1,14 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const { mkdir, writeFile } = require('fs/promises')
4
3
  const { normalizeUrl } = require('@metascraper/helpers')
5
4
  const debug = require('debug-logfmt')('html-provider')
6
- const path = require('path')
7
5
 
8
6
  const httpStatus = require('./http-status')
9
7
  const ExtendableError = require('./error')
10
8
 
11
- const HTML_DEBUG_DIR = '/tmp/html'
9
+ const NOT_FOUND = Symbol('NOT_FOUND')
12
10
 
13
11
  const isStatusCodeMissing = statusCode =>
14
12
  statusCode === undefined || statusCode === null || statusCode === ''
@@ -20,82 +18,49 @@ const createEmptyProviderValueError = ({ provider, statusCode }) =>
20
18
  message: 'Empty value returned by the provider.'
21
19
  })
22
20
 
23
- const NOT_FOUND = Symbol('NOT_FOUND')
24
-
25
- const UNRESOLVED = Symbol('UNRESOLVED')
26
-
27
21
  const getOgImage = $ => $('meta[property="og:image"]').attr('content')
28
22
 
29
- const sanitizeFileToken = input =>
30
- String(input || '')
31
- .trim()
32
- .toLowerCase()
33
- .replace(/[^a-z0-9._-]+/g, '-')
34
- .replace(/^-+|-+$/g, '') || 'unknown'
35
-
36
- const writeHtmlDebugFile = async ({ debugEnabled, provider, tier, requestId, html }) => {
37
- if (!debugEnabled || typeof html !== 'string' || html === '') return
38
-
39
- const fileName = `${sanitizeFileToken(provider)}-${sanitizeFileToken(tier)}-${sanitizeFileToken(
40
- requestId
41
- )}.html`
42
- const filePath = path.join(HTML_DEBUG_DIR, fileName)
43
- await mkdir(path.dirname(filePath), { recursive: true })
44
- await writeFile(filePath, html, 'utf8')
45
-
46
- return filePath
47
- }
48
-
49
- const getHtmlDebugInfo = async $ => {
50
- if (typeof $ !== 'function') return {}
51
-
52
- const html = typeof $.html === 'function' ? $.html() : ''
53
-
54
- return {
55
- html,
56
- htmlLength: html.length
57
- }
58
- }
59
-
60
- module.exports = ({ PROXY_TIMEOUT, DEBUG_HTML_TO_FILE, getHTML }) => {
23
+ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
61
24
  const createHtmlProvider = ({ name, url, getter, htmlOpts }) => {
62
- const provider = async function ({ input, opts, req = {}, res = {} }) {
25
+ const provider = async function ({ input, req = {}, res = {} }) {
63
26
  const providerUrl = await url(input)
64
27
  const context = { provider: name, input, providerUrl }
65
28
 
66
- const forceProxy = req.query?.proxy === true && typeof opts === 'function'
29
+ const attempt = async gotOpts => {
30
+ const defaultOpts = { ...htmlOpts?.(), timeout: PROXY_TIMEOUT }
31
+ const fetchOpts = gotOpts ? { ...defaultOpts, ...gotOpts } : defaultOpts
32
+ const tier = fetchOpts.tier ?? 'origin'
67
33
 
68
- const logProviderError = payload => debug.error({ ...context, ...payload })
34
+ const log = debug.duration({ ...context, tier })
69
35
 
70
- const logProviderLookup = payload => debug({ ...context, ...payload })
36
+ const { $, statusCode } = await getHTML(providerUrl, fetchOpts)
37
+
38
+ if (isStatusCodeMissing(statusCode)) {
39
+ log.error({ statusCode, status: 'missing_status_code' })
40
+ throw createEmptyProviderValueError({ provider: name, statusCode })
41
+ }
42
+
43
+ if (statusCode === httpStatus.NOT_FOUND) {
44
+ log.error({ statusCode, status: 'not_found' })
45
+ return NOT_FOUND
46
+ }
71
47
 
72
- const getResult = async ($, statusCode, log, tier) => {
73
48
  const result = getter($)
74
49
  if (typeof result !== 'string' || result === '') {
75
- let htmlDebugInfo = {}
76
-
77
- if (DEBUG_HTML_TO_FILE) {
78
- const { html, ...info } = await getHtmlDebugInfo($)
79
- const requestId = typeof res.getHeader === 'function'
80
- ? res.getHeader('x-request-id')
81
- : undefined
82
- const htmlFile = await writeHtmlDebugFile({
83
- debugEnabled: true,
84
- provider: name,
85
- tier,
86
- requestId,
87
- html
88
- }).catch(() => undefined)
89
- htmlDebugInfo = { ...info, ...(htmlFile ? { htmlFile } : {}) }
90
- }
91
-
92
- log.error({
93
- statusCode,
94
- ...htmlDebugInfo
50
+ const error = createEmptyProviderValueError({
51
+ provider: name,
52
+ statusCode
95
53
  })
54
+ error.html =
55
+ typeof $ === 'function' && typeof $.html === 'function'
56
+ ? $.html()
57
+ : undefined
96
58
 
97
- throw createEmptyProviderValueError({ provider: name, statusCode })
59
+ log.error({ statusCode })
60
+
61
+ throw error
98
62
  }
63
+
99
64
  const normalizedResult = normalizeUrl(providerUrl, result)
100
65
  log({
101
66
  statusCode,
@@ -105,90 +70,20 @@ module.exports = ({ PROXY_TIMEOUT, DEBUG_HTML_TO_FILE, getHTML }) => {
105
70
  return normalizedResult
106
71
  }
107
72
 
108
- const reportSuccess = (requestType, result) => {
109
- if (typeof res.setHeader === 'function') {
110
- res.setHeader('x-proxy-tier', requestType)
111
- }
112
- return result
113
- }
114
-
115
- const getResultOrUndefined = async ({
116
- tier,
117
- resolve,
118
- onError
119
- } = {}) => {
120
- const log = debug.duration({ ...context, tier })
121
- try {
122
- return await resolve(log)
123
- } catch (error) {
124
- if (error?.provider !== name && error?.name !== 'TimeoutError') {
125
- logProviderError({ tier, status: 'failed', message: error.message })
126
- }
127
- onError?.(error)
128
- return UNRESOLVED
129
- }
130
- }
131
-
132
- const getProxyResultOrUndefined = async ({ superProxy = false } = {}) => {
133
- const tier = superProxy ? 'residential' : 'datacenter'
134
- return getResultOrUndefined({
135
- tier,
136
- resolve: async log => {
137
- const { $, statusCode } = await getHTML(
138
- providerUrl,
139
- await opts(providerUrl, { superProxy, timeout: PROXY_TIMEOUT })
140
- )
141
- return getResult($, statusCode, log, tier)
142
- }
143
- })
144
- }
145
-
146
- const getOriginResultOrUndefined = () =>
147
- getResultOrUndefined({
148
- tier: 'origin',
149
- resolve: async log => {
150
- const { $, statusCode } = await getHTML(providerUrl, {
151
- ...htmlOpts?.(),
152
- timeout: PROXY_TIMEOUT
153
- })
154
-
155
- if (isStatusCodeMissing(statusCode)) {
156
- log.error({ statusCode, status: 'missing_status_code' })
157
- return UNRESOLVED
158
- }
159
-
160
- if (statusCode === httpStatus.NOT_FOUND) {
161
- log.error({ statusCode, status: 'not_found' })
162
- return NOT_FOUND
163
- }
164
-
165
- return getResult($, statusCode, log, 'origin')
166
- }
167
- })
168
-
169
- if (forceProxy) {
170
- logProviderLookup({ tier: 'origin', status: 'skipped', reason: 'force_proxy' })
73
+ if (typeof onFetchHTML === 'function') {
74
+ return onFetchHTML({ attempt, provider: name, providerUrl, req, res })
171
75
  }
172
76
 
173
- if (!forceProxy) {
174
- const resultOrigin = await getOriginResultOrUndefined()
175
- if (resultOrigin === NOT_FOUND) return
176
- if (resultOrigin !== UNRESOLVED) return reportSuccess('origin', resultOrigin)
177
- }
178
-
179
- if (typeof opts !== 'function') return
180
-
181
- const resultDatacenter = await getProxyResultOrUndefined()
182
- if (resultDatacenter !== UNRESOLVED) return reportSuccess('datacenter', resultDatacenter)
183
-
184
- const resultResidential = await getProxyResultOrUndefined({ superProxy: true })
185
- if (resultResidential === UNRESOLVED) return
186
-
187
- return reportSuccess('residential', resultResidential)
77
+ const result = await attempt()
78
+ if (result === NOT_FOUND) return undefined
79
+ return result
188
80
  }
189
81
 
82
+ provider.getUrl = url
190
83
  return provider
191
84
  }
192
85
 
193
- return { createHtmlProvider, getOgImage }
86
+ return { createHtmlProvider, getOgImage, NOT_FOUND }
194
87
  }
88
+
89
+ module.exports.NOT_FOUND = NOT_FOUND
package/src/util/index.js CHANGED
@@ -4,5 +4,6 @@ module.exports = {
4
4
  error: require('./error'),
5
5
  httpStatus: require('./http-status'),
6
6
  isIterable: require('./is-iterable'),
7
- stringify: require('./stringify')
7
+ stringify: require('./stringify'),
8
+ NOT_FOUND: require('./html-provider').NOT_FOUND
8
9
  }
package/src/util/keyv.js CHANGED
@@ -1,13 +1,15 @@
1
1
  'use strict'
2
2
 
3
3
  const keyvCompress = require('@keyvhq/compress')
4
+ const KeyvOffline = require('@keyvhq/offline')
4
5
  const KeyvRedis = require('@keyvhq/redis')
5
6
  const KeyvMulti = require('@keyvhq/multi')
6
7
  const Keyv = require('@keyvhq/core')
7
8
  const assert = require('assert')
8
9
 
9
- module.exports = ({ TTL_DEFAULT }) => {
10
- const createMultiCache = remote => new Keyv({ store: new KeyvMulti({ remote }) })
10
+ module.exports = ({ TTL_DEFAULT, redis }) => {
11
+ const createMultiCache = remote =>
12
+ new Keyv({ store: new KeyvMulti({ remote }) })
11
13
 
12
14
  const createKeyv = opts => new Keyv({ ttl: TTL_DEFAULT, ...opts })
13
15
 
@@ -16,12 +18,10 @@ module.exports = ({ TTL_DEFAULT }) => {
16
18
  return keyvCompress(createKeyv(opts))
17
19
  }
18
20
 
19
- const createMemoryCache = opts => createKeyvNamespace({ ...opts, store: new Map() })
20
-
21
21
  const createRedisCache = (opts = {}) => {
22
- const store = new Map()
22
+ const store = redis ? new KeyvOffline(new KeyvRedis(redis)) : new Map()
23
23
  return createKeyvNamespace({ ...opts, store })
24
24
  }
25
25
 
26
- return { createMemoryCache, createMultiCache, createRedisCache }
26
+ return { createMultiCache, createRedisCache }
27
27
  }
@@ -2,9 +2,7 @@
2
2
 
3
3
  const createPingUrl = require('@microlink/ping-url')
4
4
 
5
- module.exports = ({ got, createMemoryCache }) => {
6
- const pingCache = createMemoryCache({ namespace: 'ping' })
7
-
5
+ module.exports = ({ got, pingCache }) => {
8
6
  const pingUrl = createPingUrl(pingCache, {
9
7
  value: ({ url, statusCode }) => ({ url, statusCode })
10
8
  })