@unavatar/core 3.19.1 → 3.19.3
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/README.md +47 -8
- package/package.json +1 -1
- package/src/index.js +7 -5
- package/src/providers/psnprofiles.js +9 -2
- package/src/util/html-provider.js +71 -42
package/README.md
CHANGED
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
- [Discord](#discord)
|
|
20
20
|
- [Dribbble](#dribbble)
|
|
21
21
|
- [DuckDuckGo](#duckduckgo)
|
|
22
|
+
- [Facebook](#facebook)
|
|
22
23
|
- [GitHub](#github)
|
|
23
24
|
- [GitLab](#gitlab)
|
|
24
25
|
- [LinkedIn](#linkedin)
|
|
25
26
|
- [Google](#google)
|
|
27
|
+
- [Gravatar](#gravatar)
|
|
26
28
|
- [Instagram](#instagram)
|
|
27
29
|
- [Ko-fi](#ko-fi)
|
|
28
30
|
- [Medium](#medium)
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
- [Patreon](#patreon)
|
|
34
36
|
- [Pinterest](#pinterest)
|
|
35
37
|
- [Printables](#printables)
|
|
38
|
+
- [PSN Profiles](#psn-profiles)
|
|
36
39
|
- [Reddit](#reddit)
|
|
37
40
|
- [Snapchat](#snapchat)
|
|
38
41
|
- [SoundCloud](#soundcloud)
|
|
@@ -42,12 +45,16 @@
|
|
|
42
45
|
- [Telegram](#telegram)
|
|
43
46
|
- [Threads](#threads)
|
|
44
47
|
- [TikTok](#tiktok)
|
|
48
|
+
- [Tumblr](#tumblr)
|
|
45
49
|
- [Twitch](#twitch)
|
|
46
50
|
- [Vimeo](#vimeo)
|
|
47
51
|
- [WhatsApp](#whatsapp)
|
|
52
|
+
- [X/Twitter](#xtwitter)
|
|
53
|
+
- [Xbox Gamertag](#xbox-gamertag)
|
|
48
54
|
- [YouTube](#youtube)
|
|
49
55
|
- [Response Format](#response-format)
|
|
50
56
|
- [Response Headers](#response-headers)
|
|
57
|
+
- [Response Errors](#response-errors)
|
|
51
58
|
|
|
52
59
|
---
|
|
53
60
|
|
|
@@ -55,14 +62,6 @@
|
|
|
55
62
|
|
|
56
63
|
Welcome to **unavatar.io**, the ultimate avatar service that offers everything you need to easily retrieve user avatars:
|
|
57
64
|
|
|
58
|
-
- **Versatile**: A wide range of platforms and services including [TikTok](https://unavatar.io/docs#tiktok), [Instagram](https://unavatar.io/docs#instagram), [YouTube](https://unavatar.io/docs#youtube), [X/Twitter](https://unavatar.io/docs#xtwitter), [Gravatar](https://unavatar.io/docs#gravatar), etc., meaning you can rule all of them just querying against unavatar.
|
|
59
|
-
|
|
60
|
-
- **Speed**: Designed to be fast and efficient with a 93% cache hit rate, serving 21.7 TB of data across 728M requests.
|
|
61
|
-
|
|
62
|
-
- **Optimize**: All the images are not only compressed on-the-fly to reduce their size and save bandwith, but also optimized to maintain a high-quality ratio. They are ready for immediate use, enhancing the overall optimization of your website or application.
|
|
63
|
-
|
|
64
|
-
- **Integration**: The service seamlessly incorporates into your current applications or websites with ease. We offer straightforward documentation and comprehensive support to ensure a quick and effortless onboarding experience.
|
|
65
|
-
|
|
66
65
|
It's proudly powered by [microlink.io](https://microlink.io/), the headless browser API that handles all the heavy lifting behind the scenes to ensure your avatars are always ready.
|
|
67
66
|
|
|
68
67
|
## Quick start
|
|
@@ -350,6 +349,16 @@ Available inputs:
|
|
|
350
349
|
|
|
351
350
|
- Domain, e.g., [unavatar.io/duckduckgo/microsoft.com](https://unavatar.io/duckduckgo/microsoft.com)
|
|
352
351
|
|
|
352
|
+
### Facebook
|
|
353
|
+
|
|
354
|
+
Get any Facebook user, page, or group profile picture by their username or ID.
|
|
355
|
+
|
|
356
|
+
Available inputs:
|
|
357
|
+
|
|
358
|
+
- Username, e.g., [unavatar.io/facebook/zuck](https://unavatar.io/facebook/zuck)
|
|
359
|
+
- Page, e.g., [unavatar.io/facebook/windtodayco](https://unavatar.io/facebook/windtodayco)
|
|
360
|
+
- Group ID, e.g., [unavatar.io/facebook/1426378481098002](https://unavatar.io/facebook/1426378481098002)
|
|
361
|
+
|
|
353
362
|
### GitHub
|
|
354
363
|
|
|
355
364
|
Get any GitHub user or organization's profile picture by their username.
|
|
@@ -391,6 +400,8 @@ Available inputs:
|
|
|
391
400
|
|
|
392
401
|
- Domain, e.g., [unavatar.io/google/stremio.com](https://unavatar.io/google/stremio.com)
|
|
393
402
|
|
|
403
|
+
### Gravatar
|
|
404
|
+
|
|
394
405
|
Get any user's avatar by their email address via Gravatar. The most widely used global avatar service — if your users have a Gravatar set up, this is the fastest way to retrieve it.
|
|
395
406
|
|
|
396
407
|
Available inputs:
|
|
@@ -478,6 +489,14 @@ Available inputs:
|
|
|
478
489
|
|
|
479
490
|
- Username, e.g., [unavatar.io/printables/DukeDoks](https://unavatar.io/printables/DukeDoks)
|
|
480
491
|
|
|
492
|
+
### PSN Profiles
|
|
493
|
+
|
|
494
|
+
Get any PlayStation Network user's profile picture by their PSN username.
|
|
495
|
+
|
|
496
|
+
Available inputs:
|
|
497
|
+
|
|
498
|
+
- Username, e.g., [unavatar.io/psnprofiles/P3](https://unavatar.io/psnprofiles/P3)
|
|
499
|
+
|
|
481
500
|
### Reddit
|
|
482
501
|
|
|
483
502
|
Get any Reddit user's avatar by their username.
|
|
@@ -571,6 +590,14 @@ Available inputs:
|
|
|
571
590
|
|
|
572
591
|
- Username, e.g., [unavatar.io/tiktok/carlosazaustre](https://unavatar.io/tiktok/carlosazaustre)
|
|
573
592
|
|
|
593
|
+
### Tumblr
|
|
594
|
+
|
|
595
|
+
Get any Tumblr blog's profile picture by their username.
|
|
596
|
+
|
|
597
|
+
Available inputs:
|
|
598
|
+
|
|
599
|
+
- Username, e.g., [unavatar.io/tumblr/nasa](https://unavatar.io/tumblr/nasa)
|
|
600
|
+
|
|
574
601
|
### Twitch
|
|
575
602
|
|
|
576
603
|
Get any Twitch streamer's profile picture by their username.
|
|
@@ -603,12 +630,22 @@ Available URI format inputs:
|
|
|
603
630
|
- `channel`: [unavatar.io/whatsapp/channel:0029VaARuQ7KwqSXh9fiMc0m](https://unavatar.io/whatsapp/channel:0029VaARuQ7KwqSXh9fiMc0m)
|
|
604
631
|
- `chat`: [unavatar.io/whatsapp/chat:D2FFycjQXrEIKG8qQjbwZz](https://unavatar.io/whatsapp/chat:D2FFycjQXrEIKG8qQjbwZz)
|
|
605
632
|
|
|
633
|
+
### X/Twitter
|
|
634
|
+
|
|
606
635
|
Get any X (formerly Twitter) user's profile picture by their username.
|
|
607
636
|
|
|
608
637
|
Available inputs:
|
|
609
638
|
|
|
610
639
|
- Username, e.g., [unavatar.io/x/elonmusk](https://unavatar.io/x/elonmusk)
|
|
611
640
|
|
|
641
|
+
### Xbox Gamertag
|
|
642
|
+
|
|
643
|
+
Get any Xbox player's profile picture by their gamertag.
|
|
644
|
+
|
|
645
|
+
Available inputs:
|
|
646
|
+
|
|
647
|
+
- Gamertag, e.g., [unavatar.io/xboxgamertag/P3](https://unavatar.io/xboxgamertag/P3)
|
|
648
|
+
|
|
612
649
|
### YouTube
|
|
613
650
|
|
|
614
651
|
Get any YouTube channel's thumbnail by their handle, legacy username, or channel ID.
|
|
@@ -668,6 +705,8 @@ x-rate-limit-remaining: 49
|
|
|
668
705
|
x-rate-limit-reset: 1744243200
|
|
669
706
|
```
|
|
670
707
|
|
|
708
|
+
## Response Errors
|
|
709
|
+
|
|
671
710
|
Expected errors are known operational cases returned with stable codes.
|
|
672
711
|
|
|
673
712
|
- **Client-side issues** return `status: "fail"` (HTTP `4xx`).
|
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.19.
|
|
5
|
+
"version": "3.19.3",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js",
|
package/src/index.js
CHANGED
|
@@ -29,16 +29,18 @@ module.exports = ({ constants: userConstants, redis, onFetchHTML } = {}) => {
|
|
|
29
29
|
})
|
|
30
30
|
const createBrowser = require('./util/browserless')(constants)
|
|
31
31
|
const getHTML = require('./util/html-get')({ createBrowser, got })
|
|
32
|
-
const { createHtmlProvider, getOgImage } =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const { createHtmlProvider, getOgImage, NOT_FOUND } =
|
|
33
|
+
require('./util/html-provider')({
|
|
34
|
+
...constants,
|
|
35
|
+
getHTML,
|
|
36
|
+
onFetchHTML
|
|
37
|
+
})
|
|
37
38
|
|
|
38
39
|
const providerCtx = {
|
|
39
40
|
constants,
|
|
40
41
|
createHtmlProvider,
|
|
41
42
|
getOgImage,
|
|
43
|
+
NOT_FOUND,
|
|
42
44
|
got,
|
|
43
45
|
isReservedIp,
|
|
44
46
|
itunesSearchCache: cache.itunesSearchCache
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const getAvatarUrl = ({ $, getOgImage, NOT_FOUND }) => {
|
|
4
|
+
const ogImage = getOgImage($)
|
|
5
|
+
return ogImage === undefined ? NOT_FOUND : ogImage
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
module.exports = ({ createHtmlProvider, getOgImage, NOT_FOUND }) =>
|
|
4
9
|
createHtmlProvider({
|
|
5
10
|
name: 'psnprofiles',
|
|
6
11
|
url: input => `https://psnprofiles.com/${input}`,
|
|
7
|
-
getter: getOgImage
|
|
12
|
+
getter: $ => getAvatarUrl({ $, getOgImage, NOT_FOUND })
|
|
8
13
|
})
|
|
14
|
+
|
|
15
|
+
module.exports.getAvatarUrl = getAvatarUrl
|
|
@@ -28,22 +28,26 @@ const createProviderError = ({ provider, statusCode, cause, code }) =>
|
|
|
28
28
|
message: 'Empty value returned by the provider.'
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
+
const createErrorCause = ({ html, headers, statusCode }) => ({
|
|
32
|
+
html,
|
|
33
|
+
headers,
|
|
34
|
+
statusCode
|
|
35
|
+
})
|
|
36
|
+
|
|
31
37
|
module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
|
|
32
38
|
/**
|
|
33
39
|
* @param {object} opts
|
|
34
40
|
* @param {string} opts.name - Provider identifier used in logs and metrics.
|
|
35
41
|
* @param {(input: string) => string | Promise<string>} opts.url - Builds the URL to fetch for a given input.
|
|
36
|
-
* @param {($: cheerio.CheerioAPI) => string | undefined} opts.getter
|
|
42
|
+
* @param {($: cheerio.CheerioAPI) => string | symbol | undefined} opts.getter
|
|
37
43
|
* Extracts the avatar URL from the fetched HTML.
|
|
38
|
-
* - `string`
|
|
44
|
+
* - `string` — avatar URL found (success).
|
|
45
|
+
* - `NOT_FOUND` — provider-level miss.
|
|
39
46
|
* - `undefined` — avatar not found (normal failure, no retry).
|
|
40
|
-
* @param {(context: { $: cheerio.CheerioAPI, statusCode: number }) => boolean} [opts.isBlocked]
|
|
41
|
-
* Optional provider-specific blocked-page detector, checked after the
|
|
42
|
-
* default `is-antibot` check when getter returns empty/undefined.
|
|
43
47
|
* @param {() => object} [opts.htmlOpts] - Returns extra options merged into the fetch call.
|
|
44
48
|
*/
|
|
45
|
-
const createHtmlProvider = ({ name, url, getter,
|
|
46
|
-
|
|
49
|
+
const createHtmlProvider = ({ name, url, getter, htmlOpts }) => {
|
|
50
|
+
async function provider (input, context) {
|
|
47
51
|
const providerUrl = await url(input)
|
|
48
52
|
|
|
49
53
|
const attempt = async gotOpts => {
|
|
@@ -56,7 +60,8 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
|
|
|
56
60
|
},
|
|
57
61
|
timeout: PROXY_TIMEOUT
|
|
58
62
|
}
|
|
59
|
-
const fetchOpts =
|
|
63
|
+
const fetchOpts = { ...defaultOpts, ...gotOpts }
|
|
64
|
+
const userAgent = fetchOpts.headers['user-agent']
|
|
60
65
|
const tier = fetchOpts.tier ?? 'origin'
|
|
61
66
|
|
|
62
67
|
const log = debug.duration({ provider: name, input, providerUrl, tier })
|
|
@@ -73,6 +78,11 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
|
|
|
73
78
|
: undefined
|
|
74
79
|
attempt.lastHeaders = responseHeaders
|
|
75
80
|
attempt.lastStatusCode = statusCode
|
|
81
|
+
const errorCause = createErrorCause({
|
|
82
|
+
html: attempt.lastHtml,
|
|
83
|
+
headers: responseHeaders,
|
|
84
|
+
statusCode
|
|
85
|
+
})
|
|
76
86
|
|
|
77
87
|
if (isStatusCodeMissing(statusCode)) {
|
|
78
88
|
const code = EMPTY_PROVIDER_VALUE_CODE.MISSING_STATUS_CODE
|
|
@@ -80,67 +90,87 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
|
|
|
80
90
|
throw createProviderError({
|
|
81
91
|
provider: name,
|
|
82
92
|
statusCode,
|
|
83
|
-
cause:
|
|
84
|
-
html: attempt.lastHtml,
|
|
85
|
-
headers: responseHeaders,
|
|
86
|
-
statusCode
|
|
87
|
-
},
|
|
93
|
+
cause: errorCause,
|
|
88
94
|
code
|
|
89
95
|
})
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
const result = getter($)
|
|
92
99
|
if (statusCode === httpStatus.NOT_FOUND) {
|
|
93
100
|
log.error({ statusCode, status: 'not_found' })
|
|
94
101
|
return NOT_FOUND
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const error = createProviderError({
|
|
104
|
+
function createEmptyGetterResultError () {
|
|
105
|
+
return createProviderError({
|
|
100
106
|
provider: name,
|
|
101
107
|
statusCode,
|
|
102
108
|
code: EMPTY_PROVIDER_VALUE_CODE.EMPTY_GETTER_RESULT,
|
|
103
|
-
cause:
|
|
104
|
-
html: attempt.lastHtml,
|
|
105
|
-
headers: responseHeaders,
|
|
106
|
-
statusCode
|
|
107
|
-
}
|
|
109
|
+
cause: errorCause
|
|
108
110
|
})
|
|
111
|
+
}
|
|
109
112
|
|
|
113
|
+
function getBlockedStatus () {
|
|
110
114
|
const isRateLimited = statusCode === httpStatus.TOO_MANY_REQUESTS
|
|
111
|
-
|
|
115
|
+
if (isRateLimited) return { isBlocked: true, antibotProvider: null }
|
|
112
116
|
|
|
113
117
|
const { detected: antibotDetected, provider: antibotProvider } =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
error.blocked = true
|
|
118
|
+
isAntibot({
|
|
119
|
+
url: providerUrl,
|
|
120
|
+
statusCode,
|
|
121
|
+
headers: responseHeaders,
|
|
122
|
+
body: attempt.lastHtml
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
isBlocked: antibotDetected,
|
|
127
|
+
antibotProvider
|
|
125
128
|
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof result === 'string' && result !== '') {
|
|
132
|
+
const normalizedResult = normalizeUrl(providerUrl, result)
|
|
133
|
+
log({
|
|
134
|
+
statusCode,
|
|
135
|
+
status: 'success',
|
|
136
|
+
result: normalizedResult
|
|
137
|
+
})
|
|
138
|
+
return normalizedResult
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Some providers encode not-found via getter output. Check antibot
|
|
142
|
+
// first so challenge pages are retried as blocked, not treated as 404.
|
|
143
|
+
const { isBlocked: shouldMarkBlocked, antibotProvider } =
|
|
144
|
+
getBlockedStatus()
|
|
145
|
+
|
|
146
|
+
if (shouldMarkBlocked) {
|
|
147
|
+
const error = createEmptyGetterResultError()
|
|
148
|
+
error.blocked = true
|
|
126
149
|
|
|
127
150
|
log.error({
|
|
128
151
|
statusCode,
|
|
129
|
-
status:
|
|
152
|
+
status: 'blocked',
|
|
130
153
|
antibot: antibotProvider ?? undefined,
|
|
131
|
-
userAgent
|
|
154
|
+
userAgent
|
|
132
155
|
})
|
|
133
156
|
|
|
134
157
|
throw error
|
|
135
158
|
}
|
|
136
159
|
|
|
137
|
-
|
|
138
|
-
|
|
160
|
+
if (result === NOT_FOUND) {
|
|
161
|
+
log.error({ statusCode, status: 'not_found' })
|
|
162
|
+
return NOT_FOUND
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const error = createEmptyGetterResultError()
|
|
166
|
+
|
|
167
|
+
log.error({
|
|
139
168
|
statusCode,
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
antibot: antibotProvider ?? undefined,
|
|
170
|
+
userAgent
|
|
142
171
|
})
|
|
143
|
-
|
|
172
|
+
|
|
173
|
+
throw error
|
|
144
174
|
}
|
|
145
175
|
|
|
146
176
|
if (typeof onFetchHTML === 'function') {
|
|
@@ -148,8 +178,7 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
|
|
|
148
178
|
}
|
|
149
179
|
|
|
150
180
|
const result = await attempt()
|
|
151
|
-
|
|
152
|
-
return result
|
|
181
|
+
return result === NOT_FOUND ? undefined : result
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
provider.getUrl = url
|