import uuid from 'uuid' ; import type { TransistorErrorResponse , TransistorCreateEpisodeData , TransistorPublishedJson , TransistorCreatedJson , TransistorAuthorizedJson , TransistorEpisodesJson , CallKentEpisode , TransistorUpdateEpisodeData , Team , } from '~/types' ; import { getDomainUrl , getRequiredServerEnvVar , toBase64 } from './misc' ; import { redisCache } from './redis.server' ; import { cachified } from './cache.server' ; import { getEpisodePath } from './call-kent' ; import { getDirectAvatarForUser } from './user-info.server' ; import { stripHtml } from './markdown.server' ; const transistorApiSecret = getRequiredServerEnvVar ( 'TRANSISTOR_API_SECRET' ) ; const podcastId = getRequiredServerEnvVar ( 'CALL_KENT_PODCAST_ID' , '67890' ) ; async function fetchTransitor < JsonResponse > ( { endpoint , method = 'GET' , query = { } , data , } ) { const url = new URL ( endpoint , 'https://api.transistor.fm' ) ; for ( const [ key , value ] of Object . entries ( query ) ) { url . searchParams . set ( key , value ) ; } const config = { method , headers : { 'x-api-key' : transistorApiSecret , } , } ; if ( data ) { config . body = JSON . stringify ( data ) ; Object . assign ( config . headers , { 'Content-Type' : 'application/json' } ) ; } const res = await fetch ( url . toString ( ) , config ) ; const json = await res . json ( ) ; if ( json . errors ) { throw new Error ( ( json as TransistorErrorResponse ) . errors . map ( e => e . title ) . join ( '\n' ) ) ; } return json as JsonResponse ; } async function createEpisode ( { audio , title , summary , description , keywords , user , request , avatar : providedAvatar , } ) { const id = uuid . v4 ( ) ; const authorized = await fetchTransitor < TransistorAuthorizedJson > ( { endpoint : 'v1/episodes/authorize_upload' , query : { filename : `${ id }.mp3` } , } ) ; const { upload_url , audio_url , content_type } = authorized . data . attributes ; await fetch ( upload_url , { method : 'PUT' , body : audio , headers : { 'Content-Type' : content_type } , } ) ; const createData = { episode : { show_id : podcastId , season : 1 , audio_url , title , summary , description , keywords , increment_number : true , } , } ; const created = await fetchTransitor < TransistorCreatedJson > ( { endpoint : 'v1/episodes' , method : 'POST' , data : createData , } ) ; await fetchTransitor < TransistorPublishedJson > ( { endpoint : `/v1/episodes/${ encodeURIComponent ( created . data . id ) }/publish` , method : 'PATCH' , data : { episode : { status : 'published' , } , } , } ) ; const { number , season } = created . data . attributes ; if ( typeof number === 'number' && typeof season === 'number' ) { const { default : slugify } = await import ( '@sindresorhus/slugify' ) ; const slug = slugify ( created . data . attributes . title ) ; const episodePath = getEpisodePath ( { episodeNumber : number , seasonNumber : season , slug , } ) ; const domainUrl = 'https://kentcdodds.com' ; const shortEpisodePath = getEpisodePath ( { episodeNumber : number , seasonNumber : season , } ) ; const shortDomain = domainUrl . replace ( /^https?:\/\// , '' ) ; const encodedTitle = encodeURIComponent ( encodeURIComponent ( title ) ) ; const encodedUrl = encodeURIComponent ( encodeURIComponent ( `${ shortDomain }${ shortEpisodePath }` ) ) ; const encodedName = encodeURIComponent ( encodeURIComponent ( `- ${ user . firstName }` ) ) ; let radius , encodedAvatar ; if ( providedAvatar ) { encodedAvatar = toBase64 ( providedAvatar ) ; radius = ',r_max' ; } else { const { hasGravatar , avatar } = await getDirectAvatarForUser ( user , { size : 1400 , origin : getDomainUrl ( request ) , } ) ; encodedAvatar = toBase64 ( avatar ) ; radius = hasGravatar ? ',r_max' : '' ; } const textLines = Number ( Math . ceil ( Math . min ( title . length , 50 ) / 18 ) . toFixed ( ) ) ; const avatarYPosition = textLines + 0.6 ; const nameYPosition = - textLines + 5.2 ; const imageUrl = `https://res.cloudinary.com/kentcdodds-com/image/upload/$th_3000,$tw_3000,$gw_$tw_div_12,$gh_$th_div_12/w_$tw,h_$th,l_kentcdodds.com:social-background/co_white,c_fit,g_north_west,w_$gw_mul_6,h_$gh_mul_2.6,x_$gw_mul_0.8,y_$gh_mul_0.8,l_text:kentcdodds.com:Matter-Medium.woff2_180:${ encodedTitle }/c_crop${ radius },g_north_west,h_$gh_mul_5.5,w_$gh_mul_5.5,x_$gw_mul_0.8,y_$gh_mul_${ avatarYPosition },l_fetch:${ encodedAvatar }/co_rgb:a9adc1,c_fit,g_south_west,w_$gw_mul_8,h_$gh_mul_4,x_$gw_mul_0.8,y_$gh_mul_0.8,l_text:kentcdodds.com:Matter-Regular.woff2_120:${ encodedUrl }/co_rgb:a9adc1,c_fit,g_south_west,w_$gw_mul_8,h_$gh_mul_4,x_$gw_mul_0.8,y_$gh_mul_${ nameYPosition },l_text:kentcdodds.com:Matter-Regular.woff2_140:${ encodedName }/c_fit,g_east,w_$gw_mul_11,h_$gh_mul_11,x_$gw,l_kentcdodds.com:illustrations:mic/c_fill,w_$tw,h_$th/kentcdodds.com/social-background.png` ; const updateData = { id : created . data . id , episode : { alternate_url : `${ domainUrl }${ episodePath }` , image_url : imageUrl , } , } ; await fetchTransitor < TransistorPublishedJson > ( { endpoint : `/v1/episodes/${ encodeURIComponent ( created . data . id ) }` , method : 'PATCH' , data : updateData , } ) ; } await getCachedEpisodes ( { forceFresh : true } ) ; } async function getEpisodes ( ) { const { default : slugify } = await import ( '@sindresorhus/slugify' ) ; const transistorEpisodes = await fetchTransitor < TransistorEpisodesJson > ( { endpoint : `/v1/episodes` , query : { 'pagination[per]' : '5000' } , } ) ; const sortedTransistorEpisodes = transistorEpisodes . data . sort ( ( a , b ) => { const aNumber = a . attributes . number ?? 0 ; const bNumber = b . attributes . number ?? 0 ; if ( aNumber < bNumber ) { return - 1 ; } else if ( aNumber > bNumber ) { return 1 ; } return 0 ; } ) ; const episodes = [ ] ; for ( const episode of sortedTransistorEpisodes ) { if ( episode . attributes . audio_processing ) continue ; if ( episode . attributes . status !== 'published' ) continue ; if ( ! episode . attributes . number ) continue ; if ( ! episode . attributes . duration ) continue ; episodes . push ( { seasonNumber : episode . attributes . season , episodeNumber : episode . attributes . number , slug : slugify ( episode . attributes . title ) , title : episode . attributes . title , summary : episode . attributes . summary , descriptionHTML : episode . attributes . description , description : await stripHtml ( episode . attributes . description ) , keywords : episode . attributes . keywords , duration : episode . attributes . duration , shareUrl : episode . attributes . share_url , mediaUrl : episode . attributes . media_url , embedHtml : episode . attributes . embed_html , embedHtmlDark : episode . attributes . embed_html_dark , imageUrl : episode . attributes . image_url , publishedAt : episode . attributes . published_at , updatedAt : episode . attributes . updated_at , } ) ; } return episodes ; } const episodesCacheKey = `transistor:episodes:${ podcastId }` ; async function getCachedEpisodes ( { request , forceFresh , } ) { return cachified ( { cache : redisCache , key : episodesCacheKey , getFreshValue : getEpisodes , maxAge : 1000 * 60 * 60 * 24 , forceFresh , request , checkValue : ( value ) => Array . isArray ( value ) && value . every ( v => typeof v . slug === 'string' && typeof v . title === 'string' ) , } ) ; } export { createEpisode , getCachedEpisodes as getEpisodes } ;