@unavatar/core 3.23.0 → 3.24.1

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 CHANGED
@@ -68,13 +68,14 @@ It's proudly powered by [microlink.io](https://microlink.io/), the headless brow
68
68
 
69
69
  The service is exposed in **unavatar.io** via provider endpoints:
70
70
 
71
- - an **email (auto-detect)**: [unavatar.io/hello@microlink.io](https://unavatar.io/hello@microlink.io) — tries Gravatar, then GitHub
71
+ - an **email**: [unavatar.io/email/hello@microlink.io](https://unavatar.io/email/hello@microlink.io)
72
+ - a **domain**: [unavatar.io/domain/reddit.com](https://unavatar.io/domain/reddit.com)
72
73
  - an **email** via Gravatar: [unavatar.io/gravatar/hello@microlink.io](https://unavatar.io/gravatar/hello@microlink.io)
73
74
  - an **email** via GitHub: [unavatar.io/github/sindresorhus@gmail.com](https://unavatar.io/github/sindresorhus@gmail.com)
74
75
  - an **username**: [unavatar.io/github/kikobeats](https://unavatar.io/github/kikobeats)
75
76
  - a **domain**: [unavatar.io/google/reddit.com](https://unavatar.io/google/reddit.com)
76
77
 
77
- Use `/:provider/:key` for provider-specific lookups, or pass an email as the only path segment for automatic resolution. You can read more in [Email avatars](https://unavatar.io/email) and [providers](https://unavatar.io/docs#providers).
78
+ Use `/email/:key` and `/domain/:key` for type-based resolution. You can read more in [Email avatars](https://unavatar.io/email) and [providers](https://unavatar.io/docs#providers).
78
79
 
79
80
  ## Authentication
80
81
 
@@ -651,7 +652,7 @@ Available inputs:
651
652
 
652
653
  ### YouTube
653
654
 
654
- Get any YouTube channel's thumbnail by their handle, legacy username, or channel ID.
655
+ Get any YouTube channel's thumbnail by their handle, username, or channel ID.
655
656
 
656
657
  e.g., [unavatar.io/youtube/casey](https://unavatar.io/youtube/casey)
657
658
 
@@ -745,4 +746,4 @@ Expected errors are known operational cases returned with stable codes.
745
746
 
746
747
  ## Contact
747
748
 
748
- If you have any suggestion or bug to report, please contact to ust mailing to [hello@unavatar.io](mailto:hello@unavatar.io).
749
+ If you have any suggestion or bug to report, please contact to ust mailing to [hello@unavatar.io](mailto:hello@unavatar.io).
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.23.0",
5
+ "version": "3.24.1",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js",
package/src/index.js CHANGED
@@ -61,6 +61,11 @@ module.exports = ({ constants: userConstants, redis, onFetchHTML } = {}) => {
61
61
  unavatar[name] = input => getAvatar(providers[name], name, input, {})
62
62
  })
63
63
 
64
+ const createTypedAutoResolver = inputType => input => auto(inputType)(input, {})
65
+
66
+ unavatar.email = createTypedAutoResolver('email')
67
+ unavatar.domain = createTypedAutoResolver('domain')
68
+
64
69
  unavatar.auto = auto
65
70
  unavatar.getInputType = getInputType
66
71
  unavatar.getAvatar = getAvatar
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { $jsonld } = require('@metascraper/helpers')
4
- const memoize = require('@keyvhq/memoize')
4
+ const { createGetSearchResults } = require('../util/itunes-search')
5
5
 
6
6
  const APPLE_MUSIC_ID_REGEX = /^\d+$/
7
7
  const APPLE_MUSIC_STOREFRONT = 'us'
@@ -36,26 +36,19 @@ const parseInput = input => {
36
36
  }
37
37
 
38
38
  module.exports = ({ createHtmlProvider, itunesSearchCache, got }) => {
39
- const searchEntityId = memoize(
40
- async ({ query, type }) => {
41
- const entityConfig = APPLE_MUSIC_ENTITY_TYPES[type]
42
- if (!entityConfig) return
43
- const { entity, idKey } = entityConfig
44
-
45
- const url = `https://itunes.apple.com/search?term=${encodeURIComponent(
46
- query
47
- )}&entity=${entity}&limit=1`
48
- const body = await got(url, {
49
- responseType: 'json',
50
- resolveBodyOnly: true
51
- })
52
-
53
- const entityId = body?.results?.[0]?.[idKey]
54
- return isNumericId(entityId) ? String(entityId) : null
55
- },
56
- itunesSearchCache,
57
- { key: ({ query, type }) => `${type}:${query.trim().toLowerCase()}` }
58
- )
39
+ const getSearchResults = createGetSearchResults({ got, itunesSearchCache })
40
+
41
+ const searchEntityId = async ({ query, type }) => {
42
+ const entityConfig = APPLE_MUSIC_ENTITY_TYPES[type]
43
+ if (!entityConfig) return
44
+ const { entity, idKey } = entityConfig
45
+
46
+ const [result] = await getSearchResults(
47
+ `term=${encodeURIComponent(query)}&entity=${entity}&limit=1`
48
+ )
49
+ const entityId = result?.[idKey]
50
+ return isNumericId(entityId) ? String(entityId) : null
51
+ }
59
52
 
60
53
  const appleMusicURI = async input => {
61
54
  const { hasExplicitType, type, id } = parseInput(input)
@@ -0,0 +1,126 @@
1
+ 'use strict'
2
+
3
+ const { createGetSearchResults } = require('../util/itunes-search')
4
+
5
+ const ITUNES_LOOKUP_URL = 'https://itunes.apple.com/lookup'
6
+ const COUNTRY_CODE_REGEX = /^[a-z]{2}$/i
7
+
8
+ const getArtworkUrl = result =>
9
+ result?.artworkUrl512 || result?.artworkUrl100 || result?.artworkUrl60
10
+
11
+ const withCountry = ({ query, country }) =>
12
+ country ? `${query}&country=${encodeURIComponent(country)}` : query
13
+
14
+ const parseInput = input => {
15
+ const separatorIndex = input.indexOf(':')
16
+ if (separatorIndex === -1) return { type: 'app', value: input }
17
+
18
+ return {
19
+ type: input.slice(0, separatorIndex),
20
+ value: input.slice(separatorIndex + 1)
21
+ }
22
+ }
23
+
24
+ const parseCountry = value => {
25
+ const separatorIndex = value.lastIndexOf('@')
26
+ if (separatorIndex === -1) return { value, country: undefined }
27
+
28
+ const country = value.slice(separatorIndex + 1)
29
+ if (!COUNTRY_CODE_REGEX.test(country)) return { value, country: undefined }
30
+
31
+ const nextValue = value.slice(0, separatorIndex)
32
+ if (!nextValue) return { value, country: undefined }
33
+
34
+ return {
35
+ value: nextValue,
36
+ country: country.toLowerCase()
37
+ }
38
+ }
39
+
40
+ const getLookupResults = async ({ got, query }) => {
41
+ const body = await got(`${ITUNES_LOOKUP_URL}?${query}`, {
42
+ responseType: 'json',
43
+ resolveBodyOnly: true
44
+ })
45
+ return body?.results ?? []
46
+ }
47
+
48
+ const getAppAvatar = async ({ got, id, country }) => {
49
+ const query = withCountry({
50
+ query: `id=${encodeURIComponent(id)}&entity=software&limit=1`,
51
+ country
52
+ })
53
+ const [result] = await getLookupResults({ got, query })
54
+ return getArtworkUrl(result)
55
+ }
56
+
57
+ const getDeveloperAvatar = async ({ got, id, country }) => {
58
+ const query = withCountry({
59
+ query: `id=${encodeURIComponent(id)}&entity=software&limit=200`,
60
+ country
61
+ })
62
+ const results = await getLookupResults({ got, query })
63
+ const softwareResult = results.find(result => result?.kind === 'software')
64
+ return getArtworkUrl(softwareResult)
65
+ }
66
+
67
+ const getBundleAvatar = async ({ got, bundleId, country }) => {
68
+ const query = withCountry({
69
+ query: `bundleId=${encodeURIComponent(bundleId)}&entity=software&limit=1`,
70
+ country
71
+ })
72
+ const [result] = await getLookupResults({ got, query })
73
+ return getArtworkUrl(result)
74
+ }
75
+
76
+ const getAppNameAvatar = async ({ got, name, country, searchResults }) => {
77
+ const getSearchResults = searchResults || createGetSearchResults({ got })
78
+ const query = withCountry({
79
+ query: `term=${encodeURIComponent(name)}&entity=software&limit=1`,
80
+ country
81
+ })
82
+ const [result] = await getSearchResults(query)
83
+ return getArtworkUrl(result)
84
+ }
85
+
86
+ const getDeveloperNameAvatar = async ({ got, name, country, searchResults }) => {
87
+ const getSearchResults = searchResults || createGetSearchResults({ got })
88
+ const query = withCountry({
89
+ query: `term=${encodeURIComponent(
90
+ name
91
+ )}&entity=software&attribute=softwareDeveloper&limit=1`,
92
+ country
93
+ })
94
+ const [result] = await getSearchResults(query)
95
+ return getArtworkUrl(result)
96
+ }
97
+
98
+ module.exports = ({ got, itunesSearchCache }) => {
99
+ const searchResults = createGetSearchResults({ got, itunesSearchCache })
100
+
101
+ return async function appleStore (input) {
102
+ const { type, value: rawValue } = parseInput(input)
103
+ const { value, country } = parseCountry(rawValue)
104
+
105
+ switch (type) {
106
+ case 'app':
107
+ return getAppAvatar({ got, id: value, country })
108
+ case 'dev':
109
+ return getDeveloperAvatar({ got, id: value, country })
110
+ case 'bundle':
111
+ return getBundleAvatar({ got, bundleId: value, country })
112
+ case 'app-name':
113
+ return getAppNameAvatar({ got, name: value, country, searchResults })
114
+ case 'dev-name':
115
+ return getDeveloperNameAvatar({ got, name: value, country, searchResults })
116
+ default:
117
+ throw new Error(`Unsupported Apple Store type: ${type}`)
118
+ }
119
+ }
120
+ }
121
+
122
+ module.exports.getAppAvatar = getAppAvatar
123
+ module.exports.getDeveloperAvatar = getDeveloperAvatar
124
+ module.exports.getBundleAvatar = getBundleAvatar
125
+ module.exports.getAppNameAvatar = getAppNameAvatar
126
+ module.exports.getDeveloperNameAvatar = getDeveloperNameAvatar
@@ -4,24 +4,33 @@ const BUDDY_ICON_URL_PATTERN = /(?:https?:)?\/\/[^"'()\s]*\/buddyicons\/[^"'()\s
4
4
  const CSS_URL_PATTERN = /url\((['"]?)([^)'"]+)\1\)/i
5
5
  const unescapePathSlashes = value => value?.replace(/\\\//g, '/')
6
6
 
7
+ const parseInput = input => {
8
+ const [first, second] = input.split(':')
9
+ return {
10
+ type: second ? first : 'user',
11
+ id: second ?? first
12
+ }
13
+ }
14
+
7
15
  const getBuddyIconFromText = value => {
8
16
  const normalizedValue = unescapePathSlashes(value)
9
17
  if (typeof normalizedValue !== 'string') return
10
18
  return normalizedValue.match(BUDDY_ICON_URL_PATTERN)?.[0]
11
19
  }
12
20
 
21
+ const getUserProfileUrl = userId => `https://www.flickr.com/photos/${userId}/`
22
+ const getGroupProfileUrl = groupId => `https://www.flickr.com/groups/${groupId}/`
23
+
13
24
  const getAvatarUrl = input => {
14
- const [first, second] = input.split(':')
15
- const type = second ? first : 'user'
16
- const id = second ?? first
25
+ const { type, id } = parseInput(input)
17
26
 
18
27
  switch (type) {
19
28
  case 'user':
20
29
  case 'photos':
21
- return `https://www.flickr.com/photos/${id}/`
30
+ return getUserProfileUrl(id)
22
31
  case 'group':
23
32
  case 'groups':
24
- return `https://www.flickr.com/groups/${id}/`
33
+ return getGroupProfileUrl(id)
25
34
  default:
26
35
  throw new Error(`Unsupported Flickr type: ${type}`)
27
36
  }
@@ -4,6 +4,7 @@ const providersBy = {
4
4
  email: ['gravatar', 'github'],
5
5
  username: [
6
6
  'apple-music',
7
+ 'apple-store',
7
8
  'behance',
8
9
  'bluesky',
9
10
  'buymeacoffee',
@@ -49,6 +50,7 @@ const providersBy = {
49
50
  module.exports = ctx => {
50
51
  const providers = {
51
52
  'apple-music': require('./apple-music')(ctx),
53
+ 'apple-store': require('./apple-store')(ctx),
52
54
  behance: require('./behance')(ctx),
53
55
  bluesky: require('./bluesky')(ctx),
54
56
  buymeacoffee: require('./buymeacoffee')(ctx),
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ const memoize = require('@keyvhq/memoize')
4
+
5
+ const ITUNES_SEARCH_URL = 'https://itunes.apple.com/search'
6
+
7
+ const getSearchResults = async ({ got, query }) => {
8
+ const body = await got(`${ITUNES_SEARCH_URL}?${query}`, {
9
+ responseType: 'json',
10
+ resolveBodyOnly: true
11
+ })
12
+ return body?.results ?? []
13
+ }
14
+
15
+ const createGetSearchResults = ({ got, itunesSearchCache }) => {
16
+ const searchResults = query => getSearchResults({ got, query })
17
+ if (!itunesSearchCache) return searchResults
18
+
19
+ return memoize(searchResults, itunesSearchCache, {
20
+ key: query => query.trim().toLowerCase()
21
+ })
22
+ }
23
+
24
+ module.exports = { createGetSearchResults }