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