@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 +2 -1
- package/src/index.js +11 -6
- package/src/providers/apple-music.js +2 -12
- package/src/util/cache.js +9 -0
- package/src/util/cacheable-lookup.js +2 -2
- package/src/util/html-provider.js +39 -144
- package/src/util/index.js +2 -1
- package/src/util/keyv.js +6 -6
- package/src/util/reachable-url.js +1 -3
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.
|
|
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 {
|
|
16
|
-
const
|
|
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,
|
|
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')({
|
|
22
|
+
const { createHtmlProvider, getOgImage } = require('./util/html-provider')({
|
|
23
|
+
...constants,
|
|
24
|
+
getHTML,
|
|
25
|
+
onFetchHTML
|
|
26
|
+
})
|
|
22
27
|
|
|
23
|
-
const providerCtx = { constants, createHtmlProvider, getOgImage, got,
|
|
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,
|
|
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,
|
|
6
|
+
module.exports = ({ TTL_DEFAULT, DNS_TIMEOUT, DNS_SERVERS, cache }) =>
|
|
7
7
|
new CacheableLookup({
|
|
8
8
|
maxTtl: TTL_DEFAULT,
|
|
9
|
-
cache
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
34
|
+
const log = debug.duration({ ...context, tier })
|
|
69
35
|
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
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 =>
|
|
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 {
|
|
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,
|
|
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
|
})
|