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