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}