mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {Dimensions, Platform} from 'react-native'
2
3import {isWeb} from 'platform/detection'
4const {height: SCREEN_HEIGHT} = Dimensions.get('window')
5
6const IFRAME_HOST = isWeb
7 ? // @ts-ignore only for web
8 window.location.host === 'localhost:8100'
9 ? 'http://localhost:8100'
10 : 'https://bsky.app'
11 : __DEV__ && !process.env.JEST_WORKER_ID
12 ? 'http://localhost:8100'
13 : 'https://bsky.app'
14
15export const embedPlayerSources = [
16 'youtube',
17 'youtubeShorts',
18 'twitch',
19 'spotify',
20 'soundcloud',
21 'appleMusic',
22 'vimeo',
23 'giphy',
24 'tenor',
25] as const
26
27export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
28
29export type EmbedPlayerType =
30 | 'youtube_video'
31 | 'youtube_short'
32 | 'twitch_video'
33 | 'spotify_album'
34 | 'spotify_playlist'
35 | 'spotify_song'
36 | 'soundcloud_track'
37 | 'soundcloud_set'
38 | 'apple_music_playlist'
39 | 'apple_music_album'
40 | 'apple_music_song'
41 | 'vimeo_video'
42 | 'giphy_gif'
43 | 'tenor_gif'
44
45export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
46 youtube: 'YouTube',
47 youtubeShorts: 'YouTube Shorts',
48 vimeo: 'Vimeo',
49 twitch: 'Twitch',
50 giphy: 'GIPHY',
51 tenor: 'Tenor',
52 spotify: 'Spotify',
53 appleMusic: 'Apple Music',
54 soundcloud: 'SoundCloud',
55}
56
57export interface EmbedPlayerParams {
58 type: EmbedPlayerType
59 playerUri: string
60 isGif?: boolean
61 source: EmbedPlayerSource
62 metaUri?: string
63 hideDetails?: boolean
64 dimensions?: {
65 height: number
66 width: number
67 }
68}
69
70const giphyRegex = /media(?:[0-4]\.giphy\.com|\.giphy\.com)/i
71const gifFilenameRegex = /^(\S+)\.(webp|gif|mp4)$/i
72
73export function parseEmbedPlayerFromUrl(
74 url: string,
75): EmbedPlayerParams | undefined {
76 let urlp
77 try {
78 urlp = new URL(url)
79 } catch (e) {
80 return undefined
81 }
82
83 // youtube
84 if (urlp.hostname === 'youtu.be') {
85 const videoId = urlp.pathname.split('/')[1]
86 const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0)
87 if (videoId) {
88 return {
89 type: 'youtube_video',
90 source: 'youtube',
91 playerUri: `${IFRAME_HOST}/iframe/youtube.html?videoId=${videoId}&start=${seek}`,
92 }
93 }
94 }
95 if (
96 urlp.hostname === 'www.youtube.com' ||
97 urlp.hostname === 'youtube.com' ||
98 urlp.hostname === 'm.youtube.com' ||
99 urlp.hostname === 'music.youtube.com'
100 ) {
101 const [_, page, shortVideoId] = urlp.pathname.split('/')
102 const videoId =
103 page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string)
104 const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0)
105
106 if (videoId) {
107 return {
108 type: page === 'shorts' ? 'youtube_short' : 'youtube_video',
109 source: page === 'shorts' ? 'youtubeShorts' : 'youtube',
110 hideDetails: page === 'shorts' ? true : undefined,
111 playerUri: `${IFRAME_HOST}/iframe/youtube.html?videoId=${videoId}&start=${seek}`,
112 }
113 }
114 }
115
116 // twitch
117 if (
118 urlp.hostname === 'twitch.tv' ||
119 urlp.hostname === 'www.twitch.tv' ||
120 urlp.hostname === 'm.twitch.tv'
121 ) {
122 const parent = isWeb
123 ? // @ts-ignore only for web
124 window.location.hostname
125 : 'localhost'
126
127 const [_, channelOrVideo, clipOrId, id] = urlp.pathname.split('/')
128
129 if (channelOrVideo === 'videos') {
130 return {
131 type: 'twitch_video',
132 source: 'twitch',
133 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&video=${clipOrId}&parent=${parent}`,
134 }
135 } else if (clipOrId === 'clip') {
136 return {
137 type: 'twitch_video',
138 source: 'twitch',
139 playerUri: `https://clips.twitch.tv/embed?volume=0.5&autoplay=true&clip=${id}&parent=${parent}`,
140 }
141 } else if (channelOrVideo) {
142 return {
143 type: 'twitch_video',
144 source: 'twitch',
145 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${channelOrVideo}&parent=${parent}`,
146 }
147 }
148 }
149
150 // spotify
151 if (urlp.hostname === 'open.spotify.com') {
152 const [_, typeOrLocale, idOrType, id] = urlp.pathname.split('/')
153
154 if (idOrType) {
155 if (typeOrLocale === 'playlist' || idOrType === 'playlist') {
156 return {
157 type: 'spotify_playlist',
158 source: 'spotify',
159 playerUri: `https://open.spotify.com/embed/playlist/${
160 id ?? idOrType
161 }`,
162 }
163 }
164 if (typeOrLocale === 'album' || idOrType === 'album') {
165 return {
166 type: 'spotify_album',
167 source: 'spotify',
168 playerUri: `https://open.spotify.com/embed/album/${id ?? idOrType}`,
169 }
170 }
171 if (typeOrLocale === 'track' || idOrType === 'track') {
172 return {
173 type: 'spotify_song',
174 source: 'spotify',
175 playerUri: `https://open.spotify.com/embed/track/${id ?? idOrType}`,
176 }
177 }
178 }
179 }
180
181 // soundcloud
182 if (
183 urlp.hostname === 'soundcloud.com' ||
184 urlp.hostname === 'www.soundcloud.com'
185 ) {
186 const [_, user, trackOrSets, set] = urlp.pathname.split('/')
187
188 if (user && trackOrSets) {
189 if (trackOrSets === 'sets' && set) {
190 return {
191 type: 'soundcloud_set',
192 source: 'soundcloud',
193 playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
194 }
195 }
196
197 return {
198 type: 'soundcloud_track',
199 source: 'soundcloud',
200 playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
201 }
202 }
203 }
204
205 if (
206 urlp.hostname === 'music.apple.com' ||
207 urlp.hostname === 'music.apple.com'
208 ) {
209 // This should always have: locale, type (playlist or album), name, and id. We won't use spread since we want
210 // to check if the length is correct
211 const pathParams = urlp.pathname.split('/')
212 const type = pathParams[2]
213 const songId = urlp.searchParams.get('i')
214
215 if (pathParams.length === 5 && (type === 'playlist' || type === 'album')) {
216 // We want to append the songId to the end of the url if it exists
217 const embedUri = `https://embed.music.apple.com${urlp.pathname}${
218 urlp.search ? '?i=' + songId : ''
219 }`
220
221 if (type === 'playlist') {
222 return {
223 type: 'apple_music_playlist',
224 source: 'appleMusic',
225 playerUri: embedUri,
226 }
227 } else if (type === 'album') {
228 if (songId) {
229 return {
230 type: 'apple_music_song',
231 source: 'appleMusic',
232 playerUri: embedUri,
233 }
234 } else {
235 return {
236 type: 'apple_music_album',
237 source: 'appleMusic',
238 playerUri: embedUri,
239 }
240 }
241 }
242 }
243 }
244
245 if (urlp.hostname === 'vimeo.com' || urlp.hostname === 'www.vimeo.com') {
246 const [_, videoId] = urlp.pathname.split('/')
247 if (videoId) {
248 return {
249 type: 'vimeo_video',
250 source: 'vimeo',
251 playerUri: `https://player.vimeo.com/video/${videoId}?autoplay=1`,
252 }
253 }
254 }
255
256 if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') {
257 const [_, gifs, nameAndId] = urlp.pathname.split('/')
258
259 /*
260 * nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name)
261 * We want to get the id of the gif, then direct to media.giphy.com/media/{id}/giphy.webp so we can
262 * use it in an <Image> component
263 */
264
265 if (gifs === 'gifs' && nameAndId) {
266 const gifId = nameAndId.split('-').pop()
267
268 if (gifId) {
269 return {
270 type: 'giphy_gif',
271 source: 'giphy',
272 isGif: true,
273 hideDetails: true,
274 metaUri: `https://giphy.com/gifs/${gifId}`,
275 playerUri: `https://i.giphy.com/media/${gifId}/200.webp`,
276 }
277 }
278 }
279 }
280
281 // There are five possible hostnames that also can be giphy urls: media.giphy.com and media0-4.giphy.com
282 // These can include (presumably) a tracking id in the path name, so we have to check for that as well
283 if (giphyRegex.test(urlp.hostname)) {
284 // We can link directly to the gif, if its a proper link
285 const [_, media, trackingOrId, idOrFilename, filename] =
286 urlp.pathname.split('/')
287
288 if (media === 'media') {
289 if (idOrFilename && gifFilenameRegex.test(idOrFilename)) {
290 return {
291 type: 'giphy_gif',
292 source: 'giphy',
293 isGif: true,
294 hideDetails: true,
295 metaUri: `https://giphy.com/gifs/${trackingOrId}`,
296 playerUri: `https://i.giphy.com/media/${trackingOrId}/200.webp`,
297 }
298 } else if (filename && gifFilenameRegex.test(filename)) {
299 return {
300 type: 'giphy_gif',
301 source: 'giphy',
302 isGif: true,
303 hideDetails: true,
304 metaUri: `https://giphy.com/gifs/${idOrFilename}`,
305 playerUri: `https://i.giphy.com/media/${idOrFilename}/200.webp`,
306 }
307 }
308 }
309 }
310
311 // Finally, we should see if it is a link to i.giphy.com. These links don't necessarily end in .gif but can also
312 // be .webp
313 if (urlp.hostname === 'i.giphy.com' || urlp.hostname === 'www.i.giphy.com') {
314 const [_, mediaOrFilename, filename] = urlp.pathname.split('/')
315
316 if (mediaOrFilename === 'media' && filename) {
317 const gifId = filename.split('.')[0]
318 return {
319 type: 'giphy_gif',
320 source: 'giphy',
321 isGif: true,
322 hideDetails: true,
323 metaUri: `https://giphy.com/gifs/${gifId}`,
324 playerUri: `https://i.giphy.com/media/${gifId}/200.webp`,
325 }
326 } else if (mediaOrFilename) {
327 const gifId = mediaOrFilename.split('.')[0]
328 return {
329 type: 'giphy_gif',
330 source: 'giphy',
331 isGif: true,
332 hideDetails: true,
333 metaUri: `https://giphy.com/gifs/${gifId}`,
334 playerUri: `https://i.giphy.com/media/${
335 mediaOrFilename.split('.')[0]
336 }/200.webp`,
337 }
338 }
339 }
340
341 if (urlp.hostname === 'media.tenor.com') {
342 let [_, id, filename] = urlp.pathname.split('/')
343
344 const h = urlp.searchParams.get('hh')
345 const w = urlp.searchParams.get('ww')
346 let dimensions
347 if (h && w) {
348 dimensions = {
349 height: Number(h),
350 width: Number(w),
351 }
352 }
353
354 if (id && filename && dimensions && id.includes('AAAAC')) {
355 if (Platform.OS === 'web') {
356 const isSafari = /^((?!chrome|android).)*safari/i.test(
357 navigator.userAgent,
358 )
359
360 if (isSafari) {
361 id = id.replace('AAAAC', 'AAAP1')
362 filename = filename.replace('.gif', '.mp4')
363 } else {
364 id = id.replace('AAAAC', 'AAAP3')
365 filename = filename.replace('.gif', '.webm')
366 }
367 } else {
368 id = id.replace('AAAAC', 'AAAAM')
369 }
370
371 return {
372 type: 'tenor_gif',
373 source: 'tenor',
374 isGif: true,
375 hideDetails: true,
376 playerUri: `https://t.gifs.bsky.app/${id}/${filename}`,
377 dimensions,
378 }
379 }
380 }
381}
382
383export function getPlayerAspect({
384 type,
385 hasThumb,
386 width,
387}: {
388 type: EmbedPlayerParams['type']
389 hasThumb: boolean
390 width: number
391}): {aspectRatio?: number; height?: number} {
392 if (!hasThumb) return {aspectRatio: 16 / 9}
393
394 switch (type) {
395 case 'youtube_video':
396 case 'twitch_video':
397 case 'vimeo_video':
398 return {aspectRatio: 16 / 9}
399 case 'youtube_short':
400 if (SCREEN_HEIGHT < 600) {
401 return {aspectRatio: (9 / 16) * 1.75}
402 } else {
403 return {aspectRatio: (9 / 16) * 1.5}
404 }
405 case 'spotify_album':
406 case 'apple_music_album':
407 case 'apple_music_playlist':
408 case 'spotify_playlist':
409 case 'soundcloud_set':
410 return {height: 380}
411 case 'spotify_song':
412 if (width <= 300) {
413 return {height: 155}
414 }
415 return {height: 232}
416 case 'soundcloud_track':
417 return {height: 165}
418 case 'apple_music_song':
419 return {height: 150}
420 default:
421 return {aspectRatio: 16 / 9}
422 }
423}
424
425export function getGifDims(
426 originalHeight: number,
427 originalWidth: number,
428 viewWidth: number,
429) {
430 const scaledHeight = (originalHeight / originalWidth) * viewWidth
431
432 return {
433 height: scaledHeight > 250 ? 250 : scaledHeight,
434 width: (250 / scaledHeight) * viewWidth,
435 }
436}
437
438export function getGiphyMetaUri(url: URL) {
439 if (giphyRegex.test(url.hostname) || url.hostname === 'i.giphy.com') {
440 const params = parseEmbedPlayerFromUrl(url.toString())
441 if (params && params.type === 'giphy_gif') {
442 return params.metaUri
443 }
444 }
445}