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