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