@@ -0,0 +1,26 @@ | |||||
import WaveSurfer from 'wavesurfer.js'; | |||||
import { AudioMetadata } from './common'; | |||||
export const getMetadataFromUrl = (audioUrl: string) => new Promise<Partial<AudioMetadata>>((resolve, reject) => { | |||||
try { | |||||
const dummyContainer = window.document.createElement('div'); | |||||
// TODO remove wavesurfer dependency | |||||
const waveSurferInstance = WaveSurfer.create({ | |||||
container: dummyContainer, | |||||
}); | |||||
waveSurferInstance.on('ready', () => { | |||||
const metadata = { | |||||
duration: waveSurferInstance.getDuration(), | |||||
}; | |||||
waveSurferInstance.destroy(); | |||||
dummyContainer.remove(); | |||||
resolve(metadata); | |||||
}); | |||||
void waveSurferInstance.load(audioUrl); | |||||
} catch (err) { | |||||
reject(err); | |||||
} | |||||
}); |
@@ -0,0 +1,3 @@ | |||||
export interface AudioMetadata { | |||||
duration?: number; | |||||
} |
@@ -1,39 +1,17 @@ | |||||
import WaveSurfer from 'wavesurfer.js'; | |||||
import {AudioMetadata} from './common'; | |||||
export interface AudioMetadata { | |||||
duration?: number; | |||||
} | |||||
export const getAudioMetadata = (audioUrl?: string, fileType?: string) => new Promise<AudioMetadata>(async (resolve, reject) => { | |||||
// TODO server side | |||||
if (fileType === 'audio/mid') { | |||||
resolve({}); | |||||
return; | |||||
export const getMetadataFromUrl = async (audioUrl?: string) => { | |||||
if (typeof audioUrl === 'undefined') { | |||||
return {} as Partial<AudioMetadata>; | |||||
} | } | ||||
try { | |||||
const dummyContainer = window.document.createElement('div'); | |||||
const waveSurferInstance = WaveSurfer.create({ | |||||
container: dummyContainer, | |||||
}); | |||||
waveSurferInstance.on('ready', async () => { | |||||
const metadata = { | |||||
duration: waveSurferInstance.getDuration(), | |||||
}; | |||||
waveSurferInstance.destroy(); | |||||
dummyContainer.remove(); | |||||
resolve(metadata); | |||||
}); | |||||
if (typeof window !== 'undefined') { | |||||
const { getMetadataFromUrl: client } = await import('./client'); | |||||
return client(audioUrl); | |||||
} | |||||
if (audioUrl === undefined) { | |||||
resolve({}); | |||||
return; | |||||
} | |||||
// TODO server side | |||||
return {} as Partial<AudioMetadata>; | |||||
}; | |||||
await waveSurferInstance.load(audioUrl); | |||||
} catch (err) { | |||||
reject(err); | |||||
} | |||||
}); | |||||
export * from './common'; |
@@ -1,7 +1,7 @@ | |||||
import ColorThief from 'colorthief'; | import ColorThief from 'colorthief'; | ||||
import {DEFAULT_PALETTE_COLOR_COUNT, GetImageMetadataOptions, ImageMetadata} from './common'; | import {DEFAULT_PALETTE_COLOR_COUNT, GetImageMetadataOptions, ImageMetadata} from './common'; | ||||
const doGetImageMetadata = async (thisImage: HTMLImageElement, options = {} as GetImageMetadataOptions) => { | |||||
const doGetImageMetadata = async (thisImage: HTMLImageElement, options = {} as GetImageMetadataOptions): Promise<Partial<ImageMetadata>> => { | |||||
const { paletteColorCount = DEFAULT_PALETTE_COLOR_COUNT } = options; | const { paletteColorCount = DEFAULT_PALETTE_COLOR_COUNT } = options; | ||||
const colorThief = new ColorThief(); | const colorThief = new ColorThief(); | ||||
const palette = await colorThief.getPalette(thisImage, paletteColorCount); | const palette = await colorThief.getPalette(thisImage, paletteColorCount); | ||||
@@ -12,9 +12,9 @@ const doGetImageMetadata = async (thisImage: HTMLImageElement, options = {} as G | |||||
}; | }; | ||||
}; | }; | ||||
export const getMetadataFromElement = (thisImage: HTMLImageElement, options?: GetImageMetadataOptions): Promise<ImageMetadata> => doGetImageMetadata(thisImage, options); | |||||
export const getMetadataFromElement = (thisImage: HTMLImageElement, options?: GetImageMetadataOptions): Promise<Partial<ImageMetadata>> => doGetImageMetadata(thisImage, options); | |||||
export const getMetadataFromUrl = (imageUrl: string, options = {} as GetImageMetadataOptions): Promise<ImageMetadata> => new Promise((resolve, reject) => { | |||||
export const getMetadataFromUrl = (imageUrl: string, options = {} as GetImageMetadataOptions): Promise<Partial<ImageMetadata>> => new Promise((resolve, reject) => { | |||||
const image = new Image(); | const image = new Image(); | ||||
image.addEventListener('load', async (imageLoadEvent) => { | image.addEventListener('load', async (imageLoadEvent) => { | ||||
@@ -25,14 +25,9 @@ export const getMetadataFromUrl = (imageUrl: string, options = {} as GetImageMet | |||||
}); | }); | ||||
image.addEventListener('error', () => { | image.addEventListener('error', () => { | ||||
reject(new Error('Could not load file as image')); | |||||
reject(new Error('Could not load URL as image')); | |||||
image.remove(); | image.remove(); | ||||
}); | }); | ||||
if (imageUrl === undefined) { | |||||
resolve({}); | |||||
return; | |||||
} | |||||
image.src = imageUrl; | image.src = imageUrl; | ||||
}); | }); |
@@ -1,6 +1,8 @@ | |||||
import {ImageMetadata} from './common'; | |||||
export const getMetadataFromUrl = async (imageUrl?: string) => { | export const getMetadataFromUrl = async (imageUrl?: string) => { | ||||
if (imageUrl === undefined) { | |||||
return {}; | |||||
if (typeof imageUrl === 'undefined') { | |||||
return {} as Partial<ImageMetadata>; | |||||
} | } | ||||
if (typeof window !== 'undefined') { | if (typeof window !== 'undefined') { | ||||
@@ -1,6 +1,6 @@ | |||||
import {DEFAULT_PALETTE_COLOR_COUNT, GetImageMetadataOptions, ImageMetadata} from './common'; | import {DEFAULT_PALETTE_COLOR_COUNT, GetImageMetadataOptions, ImageMetadata} from './common'; | ||||
const doGetImageMetadata = async (input: string | Buffer, options = {} as GetImageMetadataOptions) => { | |||||
const doGetImageMetadata = async (input: string | Buffer, options = {} as GetImageMetadataOptions): Promise<Partial<ImageMetadata>> => { | |||||
const { paletteColorCount = DEFAULT_PALETTE_COLOR_COUNT } = options; | const { paletteColorCount = DEFAULT_PALETTE_COLOR_COUNT } = options; | ||||
const { imageSize, disableFS } = await import('image-size'); | const { imageSize, disableFS } = await import('image-size'); | ||||
disableFS(true); | disableFS(true); | ||||
@@ -14,6 +14,6 @@ const doGetImageMetadata = async (input: string | Buffer, options = {} as GetIma | |||||
}; | }; | ||||
}; | }; | ||||
export const getMetadataFromUrl = (imageUrl: string, options?: GetImageMetadataOptions): Promise<ImageMetadata> => doGetImageMetadata(imageUrl, options); | |||||
export const getMetadataFromUrl = (imageUrl: string, options?: GetImageMetadataOptions): Promise<Partial<ImageMetadata>> => doGetImageMetadata(imageUrl, options); | |||||
export const getMetadataFromBuffer = (buffer: Buffer, options?: GetImageMetadataOptions): Promise<ImageMetadata> => doGetImageMetadata(buffer, options); | |||||
export const getMetadataFromBuffer = (buffer: Buffer, options?: GetImageMetadataOptions): Promise<Partial<ImageMetadata>> => doGetImageMetadata(buffer, options); |
@@ -50,7 +50,7 @@ const countLinesOfCode = (lines: string[], scheme: string): number => { | |||||
//return lines.filter((line) => !line.trim().startsWith('//')).length; | //return lines.filter((line) => !line.trim().startsWith('//')).length; | ||||
return lines.filter((line) => line.trim().length > 0).length; | return lines.filter((line) => line.trim().length > 0).length; | ||||
} | |||||
}; | |||||
export const getTextMetadata = (contents: string, filename?: string): Promise<TextMetadata> => { | export const getTextMetadata = (contents: string, filename?: string): Promise<TextMetadata> => { | ||||
const lineNormalizedContents = contents.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); | const lineNormalizedContents = contents.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); | ||||
@@ -65,7 +65,7 @@ export const getTextMetadata = (contents: string, filename?: string): Promise<Te | |||||
scheme: value.aliasOf, | scheme: value.aliasOf, | ||||
schemeTitle: value.title, | schemeTitle: value.title, | ||||
linesOfCode: countLinesOfCode(lines, value.aliasOf), | linesOfCode: countLinesOfCode(lines, value.aliasOf), | ||||
} | |||||
}; | |||||
} | } | ||||
return { | return { | ||||
@@ -81,7 +81,7 @@ export const getTextMetadata = (contents: string, filename?: string): Promise<Te | |||||
{ | { | ||||
contents, | contents, | ||||
lineCount, | lineCount, | ||||
} as TextMetadata | |||||
} as TextMetadata, | |||||
); | ); | ||||
if (typeof metadata.scheme !== 'string') { | if (typeof metadata.scheme !== 'string') { | ||||
@@ -90,4 +90,4 @@ export const getTextMetadata = (contents: string, filename?: string): Promise<Te | |||||
} | } | ||||
return Promise.resolve(metadata); | return Promise.resolve(metadata); | ||||
} | |||||
}; |
@@ -0,0 +1,25 @@ | |||||
import {VideoMetadata} from './common'; | |||||
export const getMetadataFromUrl = async (videoUrl: string) => new Promise<Partial<VideoMetadata>>((resolve, reject) => { | |||||
const video = window.document.createElement('video'); | |||||
const source = window.document.createElement('source'); | |||||
video.addEventListener('loadedmetadata', (videoLoadEvent) => { | |||||
const thisVideo = videoLoadEvent.currentTarget as HTMLVideoElement; | |||||
const metadata = { | |||||
width: thisVideo.videoWidth, | |||||
height: thisVideo.videoHeight, | |||||
duration: thisVideo.duration, | |||||
}; | |||||
video.remove(); | |||||
resolve(metadata); | |||||
}); | |||||
video.addEventListener('error', () => { | |||||
reject(new Error('Could not load file as video')); | |||||
video.remove(); | |||||
}); | |||||
source.src = videoUrl; | |||||
video.appendChild(source); | |||||
}); |
@@ -0,0 +1,5 @@ | |||||
export interface VideoMetadata { | |||||
width?: number; | |||||
height?: number; | |||||
duration?: number; | |||||
} |
@@ -1,35 +1,15 @@ | |||||
export interface VideoFileMetadata { | |||||
width?: number; | |||||
height?: number; | |||||
duration?: number; | |||||
} | |||||
export const getVideoMetadata = (videoUrl?: string) => new Promise<VideoFileMetadata>((resolve, reject) => { | |||||
// TODO server side | |||||
const video = window.document.createElement('video'); | |||||
const source = window.document.createElement('source'); | |||||
video.addEventListener('loadedmetadata', (videoLoadEvent) => { | |||||
const thisVideo = videoLoadEvent.currentTarget as HTMLVideoElement; | |||||
const metadata = { | |||||
width: thisVideo.videoWidth, | |||||
height: thisVideo.videoHeight, | |||||
duration: thisVideo.duration, | |||||
}; | |||||
video.remove(); | |||||
resolve(metadata); | |||||
}); | |||||
video.addEventListener('error', () => { | |||||
reject(new Error('Could not load file as video')); | |||||
video.remove(); | |||||
}); | |||||
export const getMetadataFromUrl = async (videoUrl?: string) => { | |||||
if (typeof videoUrl === 'undefined') { | if (typeof videoUrl === 'undefined') { | ||||
resolve({}); | |||||
return; | |||||
return {}; | |||||
} | } | ||||
source.src = videoUrl; | |||||
video.appendChild(source); | |||||
}); | |||||
if (typeof window !== 'undefined') { | |||||
const { getMetadataFromUrl: client } = await import('./client'); | |||||
return client(videoUrl); | |||||
} | |||||
// TODO server side | |||||
return {}; | |||||
}; | |||||
export * from './common'; |
@@ -16,6 +16,7 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedElemen | |||||
className, | className, | ||||
children, | children, | ||||
controls, | controls, | ||||
// TODO organize props for color | |||||
waveColor, | waveColor, | ||||
progressColor, | progressColor, | ||||
cursorColor, | cursorColor, | ||||
@@ -1,8 +1,8 @@ | |||||
import * as mimeTypes from 'mime-types'; | import * as mimeTypes from 'mime-types'; | ||||
import {getTextMetadata, TextMetadata} from '@modal-soft/text-utils'; | import {getTextMetadata, TextMetadata} from '@modal-soft/text-utils'; | ||||
import {getMetadataFromUrl, ImageMetadata} from '@modal-soft/image-utils'; | |||||
import {getAudioMetadata, AudioMetadata} from '@modal-soft/audio-utils'; | |||||
import {getVideoMetadata, VideoFileMetadata} from '@modal-soft/video-utils'; | |||||
import {getMetadataFromUrl as getImageMetadataFromUrl, ImageMetadata} from '@modal-soft/image-utils'; | |||||
import {getMetadataFromUrl as getAudioMetadataFromUrl, AudioMetadata} from '@modal-soft/audio-utils'; | |||||
import {getMetadataFromUrl as getVideoMetadataFromUrl, VideoMetadata} from '@modal-soft/video-utils'; | |||||
const MIME_TYPE_DESCRIPTIONS = { | const MIME_TYPE_DESCRIPTIONS = { | ||||
'image/gif': 'GIF Image', | 'image/gif': 'GIF Image', | ||||
@@ -164,7 +164,7 @@ export interface ImageFile extends FileWithResolvedContentType { | |||||
export const augmentImageFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<ImageFile> => { | export const augmentImageFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<ImageFile> => { | ||||
const fileMutable = file as unknown as Record<string, ImageMetadata>; | const fileMutable = file as unknown as Record<string, ImageMetadata>; | ||||
fileMutable.metadata = await getMetadataFromUrl(file.url); | |||||
fileMutable.metadata = await getImageMetadataFromUrl(file.url); | |||||
return fileMutable as unknown as ImageFile; | return fileMutable as unknown as ImageFile; | ||||
}; | }; | ||||
@@ -175,7 +175,11 @@ export interface AudioFile extends FileWithResolvedContentType { | |||||
export const augmentAudioFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<AudioFile> => { | export const augmentAudioFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<AudioFile> => { | ||||
const fileMutable = file as unknown as Record<string, AudioMetadata>; | const fileMutable = file as unknown as Record<string, AudioMetadata>; | ||||
fileMutable.metadata = await getAudioMetadata(file.url, file.type); | |||||
if (file.type === 'audio/mid') { | |||||
fileMutable.metadata = {}; | |||||
} else { | |||||
fileMutable.metadata = await getAudioMetadataFromUrl(file.url); | |||||
} | |||||
return fileMutable as unknown as AudioFile; | return fileMutable as unknown as AudioFile; | ||||
}; | }; | ||||
@@ -200,11 +204,11 @@ export const augmentBinaryFile = async <T extends Partial<FileWithDataUrl>>(file | |||||
export interface VideoFile extends FileWithResolvedContentType { | export interface VideoFile extends FileWithResolvedContentType { | ||||
resolvedType: ContentType.VIDEO; | resolvedType: ContentType.VIDEO; | ||||
metadata?: VideoFileMetadata; | |||||
metadata?: VideoMetadata; | |||||
} | } | ||||
export const augmentVideoFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<VideoFile> => { | export const augmentVideoFile = async <T extends Partial<FileWithDataUrl>>(file: T): Promise<VideoFile> => { | ||||
const fileMutable = file as unknown as Record<string, AudioMetadata>; | const fileMutable = file as unknown as Record<string, AudioMetadata>; | ||||
fileMutable.metadata = await getVideoMetadata(file.url); | |||||
fileMutable.metadata = await getVideoMetadataFromUrl(file.url); | |||||
return fileMutable as unknown as VideoFile; | return fileMutable as unknown as VideoFile; | ||||
}; | }; |