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