diff --git a/src/file/file-render-type.spec.ts b/src/file/file-render-type.spec.ts new file mode 100644 index 0000000..d1f9ea7 --- /dev/null +++ b/src/file/file-render-type.spec.ts @@ -0,0 +1,113 @@ +import { getFileRenderType, FileRenderType } from './file-render-type'; +import type { IFile } from '@putdotio/api-client'; + +const baseFile: IFile = { + id: 1, + parent_id: 1, + name: 'test', + size: 100, + extension: 'txt', + file_type: 'TEXT', + content_type: 'text/plain', + crc32: '1234567890', + created_at: '2021-01-01T00:00:00Z', +}; + +describe('getFileRenderType', () => { + it('should return "folder" for directories', () => { + const file: IFile = { + ...baseFile, + content_type: 'application/x-directory', + file_type: 'FOLDER', + }; + expect(getFileRenderType(file)).toBe('folder'); + }); + + it('should return "audio" for audio files', () => { + const file: IFile = { + ...baseFile, + content_type: 'audio/mpeg', + file_type: 'AUDIO', + }; + expect(getFileRenderType(file)).toBe('audio'); + }); + + it('should return "video" for video files', () => { + const file: IFile = { + ...baseFile, + content_type: 'video/mp4', + file_type: 'VIDEO', + }; + expect(getFileRenderType(file)).toBe('video'); + }); + + it('should return "text/markdown" for markdown files', () => { + const file: IFile = { + ...baseFile, + content_type: 'text/markdown', + extension: 'md', + file_type: 'TEXT', + }; + expect(getFileRenderType(file)).toBe('text/markdown'); + }); + + it('should return "text" for text files', () => { + const file: IFile = { + ...baseFile, + content_type: 'text/plain', + file_type: 'TEXT', + }; + expect(getFileRenderType(file)).toBe('text'); + }); + + it('should return "image" for image files', () => { + const file: IFile = { + ...baseFile, + content_type: 'image/jpeg', + file_type: 'IMAGE', + }; + expect(getFileRenderType(file)).toBe('image'); + }); + + it('should return "pdf" for PDF files', () => { + const file: IFile = { + ...baseFile, + content_type: 'application/pdf', + file_type: 'PDF', + }; + expect(getFileRenderType(file)).toBe('pdf'); + }); + + it('should return "archive" for archive files', () => { + const file: IFile = { + ...baseFile, + content_type: 'application/zip', + file_type: 'ARCHIVE', + }; + expect(getFileRenderType(file)).toBe('archive'); + }); + + it('should return "epub" for EPUB files', () => { + const file: IFile = { + ...baseFile, + content_type: 'application/epub+zip', + }; + expect(getFileRenderType(file)).toBe('epub'); + }); + + it('should return "other" for unknown file types', () => { + const file: IFile = { + ...baseFile, + content_type: 'application/octet-stream', + }; + expect(getFileRenderType(file)).toBe('other'); + }); + + it('should return "other" if content_type is not a string', () => { + const file: IFile = { + ...baseFile, + content_type: null as any, + }; + expect(getFileRenderType(file)).toBe('other'); + }); +}); diff --git a/src/file/file-render-type.ts b/src/file/file-render-type.ts new file mode 100644 index 0000000..54370c5 --- /dev/null +++ b/src/file/file-render-type.ts @@ -0,0 +1,58 @@ +import type { IFile } from '@putdotio/api-client'; + +export type FileRenderType = + | 'archive' + | 'audio' + | 'epub' + | 'folder' + | 'image' + | 'other' + | 'pdf' + | 'text' + | 'text/markdown' + | 'video'; + +export const getFileRenderType = (file: IFile): FileRenderType => { + const { content_type, extension, file_type } = file; + + if (typeof content_type !== 'string') { + return 'other'; + } + + if (content_type === 'application/x-directory') { + return 'folder'; + } + + if (content_type.startsWith('audio') || file_type === 'AUDIO') { + return 'audio'; + } + + if (content_type.startsWith('video')) { + return 'video'; + } + + if (content_type.startsWith('text')) { + if (content_type.endsWith('/markdown') || extension === 'md') { + return 'text/markdown'; + } + return 'text'; + } + + if (content_type.startsWith('image')) { + return 'image'; + } + + if (content_type === 'application/pdf') { + return 'pdf'; + } + + if (['application/x-rar', 'application/zip'].includes(content_type)) { + return 'archive'; + } + + if (content_type === 'application/epub+zip') { + return 'epub'; + } + + return 'other'; +}; diff --git a/src/file/file.spec.ts b/src/file/file-size.spec.ts similarity index 85% rename from src/file/file.spec.ts rename to src/file/file-size.spec.ts index 14716d3..00f8b36 100644 --- a/src/file/file.spec.ts +++ b/src/file/file-size.spec.ts @@ -1,6 +1,6 @@ -import { toHumanFileSize } from './file'; +import { toHumanFileSize } from './file-size'; -describe('files', () => { +describe('file-size', () => { describe('toHumanFileSize', () => { it('should convert bytes to human readable format', () => { expect(toHumanFileSize(1024)).toBe('1 KB'); diff --git a/src/file/file.ts b/src/file/file-size.ts similarity index 100% rename from src/file/file.ts rename to src/file/file-size.ts diff --git a/src/file/file-url-provider.spec.ts b/src/file/file-url-provider.spec.ts new file mode 100644 index 0000000..dbaf045 --- /dev/null +++ b/src/file/file-url-provider.spec.ts @@ -0,0 +1,203 @@ +import type { IFile } from '@putdotio/api-client'; + +import { FileURLProvider } from './file-url-provider'; +import { getFileRenderType } from './file-render-type'; + +jest.mock('./file-render-type'); + +const apiURL = 'https://api.example.com'; +const token = 'test-token'; +let provider: FileURLProvider; + +const baseFile: IFile = { + id: 1, + parent_id: 1, + name: 'test', + size: 100, + extension: 'txt', + file_type: 'TEXT', + content_type: 'text/plain', + crc32: '1234567890', + created_at: '2021-01-01T00:00:00Z', +}; + +beforeEach(() => { + provider = new FileURLProvider(apiURL, token); +}); + +const mockFileRenderType = (type: string) => { + (getFileRenderType as jest.Mock).mockReturnValue(type); +}; + +describe('FileURLProvider', () => { + describe('getDownloadURL', () => { + it('should return download URL for file ID', () => { + const fileId = 123; + const url = provider.getDownloadURL(fileId); + expect(url).toMatchInlineSnapshot( + `"https://api.example.com/v2/files/123/download?oauth_token=test-token"` + ); + }); + + it('should return null for folder type', () => { + const file: IFile = { + ...baseFile, + file_type: 'FOLDER', + }; + const url = provider.getDownloadURL(file); + expect(url).toBeNull(); + }); + + it('should return download URL for file object', () => { + const file: IFile = { + ...baseFile, + file_type: 'VIDEO', + }; + const url = provider.getDownloadURL(file); + expect(url).toMatchInlineSnapshot( + `"https://api.example.com/v2/files/1/download?oauth_token=test-token"` + ); + }); + }); + + describe('getHLSStreamURL', () => { + it('should return null if file is not a video', () => { + mockFileRenderType('audio'); + const file: IFile = { + ...baseFile, + file_type: 'AUDIO', + }; + const url = provider.getHLSStreamURL(file, {}); + expect(url).toBeNull(); + }); + + it('should return HLS stream URL with parameters', () => { + mockFileRenderType('video'); + const file: IFile = { + ...baseFile, + file_type: 'VIDEO', + }; + const params = { + playOriginal: true, + subtitleLanguages: ['en', 'es'], + maxSubtitleCount: 2, + }; + const url = provider.getHLSStreamURL(file, params); + expect(url).toMatchInlineSnapshot( + `"https://api.example.com/v2/files/1/hls/media.m3u8?oauth_token=test-token&original=1&subtitle_languages=en%2Ces&max_subtitle_count=2"` + ); + }); + }); + + describe('getMP4DownloadURL', () => { + it('should return null if file is not a video', () => { + mockFileRenderType('audio'); + const file: IFile = { + ...baseFile, + file_type: 'AUDIO', + }; + const url = provider.getMP4DownloadURL(file); + expect(url).toBeNull(); + }); + + it('should return null if MP4 is not available', () => { + mockFileRenderType('video'); + const file: IFile = { + ...baseFile, + file_type: 'VIDEO', + is_mp4_available: false, + parent_id: 0, + name: 'test.mp4', + size: 1024, + content_type: 'video/mp4', + }; + const url = provider.getMP4DownloadURL(file); + expect(url).toBeNull(); + }); + + it('should return MP4 download URL if MP4 is available', () => { + mockFileRenderType('video'); + const file: IFile = { + ...baseFile, + file_type: 'VIDEO', + is_mp4_available: true, + }; + const url = provider.getMP4DownloadURL(file); + expect(url).toMatchInlineSnapshot( + `"https://api.example.com/v2/files/1/mp4/download?oauth_token=test-token"` + ); + }); + }); + + describe('getMP4StreamURL', () => { + it('should return null if file is not a video', () => { + mockFileRenderType('audio'); + const file: IFile = { + ...baseFile, + file_type: 'AUDIO', + }; + const url = provider.getMP4StreamURL(file); + expect(url).toBeNull(); + }); + + it('should return null if MP4 is not available', () => { + mockFileRenderType('video'); + const file: IFile = { + ...baseFile, + file_type: 'VIDEO', + is_mp4_available: false, + }; + const url = provider.getMP4StreamURL(file); + expect(url).toBeNull(); + }); + + it('should return MP4 stream URL if MP4 is available', () => { + mockFileRenderType('video'); + const file: IFile = { + ...baseFile, + file_type: 'VIDEO', + is_mp4_available: true, + }; + const url = provider.getMP4StreamURL(file); + expect(url).toMatchInlineSnapshot( + `"https://api.example.com/v2/files/1/mp4/stream?oauth_token=test-token"` + ); + }); + }); + + describe('getStreamURL', () => { + it('should return audio stream URL for audio file', () => { + mockFileRenderType('audio'); + const file: IFile = { + ...baseFile, + file_type: 'AUDIO', + }; + const url = provider.getStreamURL(file); + expect(url).toMatchInlineSnapshot( + `"https://api.example.com/v2/files/1/stream.mp3?oauth_token=test-token"` + ); + }); + + it('should return video stream URL for video file', () => { + mockFileRenderType('video'); + const file: IFile = { + ...baseFile, + file_type: 'VIDEO', + }; + const url = provider.getStreamURL(file); + expect(url).toMatchInlineSnapshot( + `"https://api.example.com/v2/files/1/stream?oauth_token=test-token"` + ); + }); + + it('should return null for unsupported file type', () => { + mockFileRenderType('archive'); + const file: IFile = { + ...baseFile, + file_type: 'ARCHIVE', + }; + const url = provider.getStreamURL(file); + expect(url).toBeNull(); + }); + }); +}); diff --git a/src/file/file-url-provider.ts b/src/file/file-url-provider.ts new file mode 100644 index 0000000..6d71650 --- /dev/null +++ b/src/file/file-url-provider.ts @@ -0,0 +1,98 @@ +import type { IFile } from '@putdotio/api-client'; + +import { getFileRenderType } from './file-render-type'; + +export class FileURLProvider { + apiURL: string; + token: string; + + constructor(apiURL: string, token: string) { + this.apiURL = apiURL.endsWith('/v2') ? apiURL : `${apiURL}/v2`; + this.token = token; + } + + getDownloadURL(fileOrFileId: IFile | IFile['id']) { + if (typeof fileOrFileId === 'number') { + return `${this.apiURL}/files/${fileOrFileId}/download?oauth_token=${this.token}`; + } + + if (fileOrFileId.file_type === 'FOLDER') { + return null; + } + + return `${this.apiURL}/files/${fileOrFileId.id}/download?oauth_token=${this.token}`; + } + + getHLSStreamURL( + file: IFile, + params: { + maxSubtitleCount?: number; + playOriginal?: boolean; + subtitleLanguages?: string[]; + } + ) { + if (getFileRenderType(file) !== 'video') { + return null; + } + + const url = new URL(`${this.apiURL}/files/${file.id}/hls/media.m3u8`); + url.searchParams.set('oauth_token', this.token); + + if (params.playOriginal) { + url.searchParams.set('original', '1'); + } + + if (params.subtitleLanguages) { + url.searchParams.set( + 'subtitle_languages', + params.subtitleLanguages.join(',') + ); + } + + if (params.maxSubtitleCount) { + url.searchParams.set( + 'max_subtitle_count', + params.maxSubtitleCount.toString() + ); + } + + return url.toString(); + } + + getMP4DownloadURL(file: IFile) { + if (getFileRenderType(file) !== 'video') { + return null; + } + + if (!file.is_mp4_available) { + return null; + } + + return `${this.apiURL}/files/${file.id}/mp4/download?oauth_token=${this.token}`; + } + + getMP4StreamURL(file: IFile) { + if (getFileRenderType(file) !== 'video') { + return null; + } + + if (!file.is_mp4_available) { + return null; + } + + return `${this.apiURL}/files/${file.id}/mp4/stream?oauth_token=${this.token}`; + } + + getStreamURL(file: IFile) { + switch (getFileRenderType(file)) { + case 'audio': + return `${this.apiURL}/files/${file.id}/stream.mp3?oauth_token=${this.token}`; + + case 'video': + return `${this.apiURL}/files/${file.id}/stream?oauth_token=${this.token}`; + + default: + return null; + } + } +} diff --git a/src/index.ts b/src/index.ts index 88cbd8e..614c9cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,11 @@ export { default as ExtendableError } from 'es6-error'; + export * from './date/date'; export * from './date/duration'; -export * from './file/file'; -export * from './localized-error/LocalizedError'; -export * from './localized-error/localizeError'; -export * from './string/string'; + +export * from './file/file-size'; + +export * from './localized-error/localized-error'; +export * from './localized-error/localize-error'; + +export * from './string/string-manipulation'; diff --git a/src/localized-error/localizeError.spec.ts b/src/localized-error/localize-error.spec.ts similarity index 98% rename from src/localized-error/localizeError.spec.ts rename to src/localized-error/localize-error.spec.ts index 4b0cadb..b8e228a 100644 --- a/src/localized-error/localizeError.spec.ts +++ b/src/localized-error/localize-error.spec.ts @@ -1,10 +1,10 @@ import { createMockErrorResponse } from '@putdotio/api-client'; -import { LocalizedError } from './LocalizedError'; +import { LocalizedError } from './localized-error'; import { GenericErrorLocalizer, createLocalizeError, isErrorLocalizer, -} from './localizeError'; +} from './localize-error'; const genericErrorLocalizer: GenericErrorLocalizer = { kind: 'generic', diff --git a/src/localized-error/localizeError.ts b/src/localized-error/localize-error.ts similarity index 97% rename from src/localized-error/localizeError.ts rename to src/localized-error/localize-error.ts index ee54c91..0afe979 100644 --- a/src/localized-error/localizeError.ts +++ b/src/localized-error/localize-error.ts @@ -4,7 +4,7 @@ import { isPutioAPIErrorResponse, createMockErrorResponse, } from '@putdotio/api-client'; -import { LocalizedError, type LocalizedErrorParams } from './LocalizedError'; +import { LocalizedError, type LocalizedErrorParams } from './localized-error'; export type LocalizeErrorFn = ( error: E diff --git a/src/localized-error/LocalizedError.ts b/src/localized-error/localized-error.ts similarity index 100% rename from src/localized-error/LocalizedError.ts rename to src/localized-error/localized-error.ts diff --git a/src/string/string.spec.ts b/src/string/string-manipulation.spec.ts similarity index 94% rename from src/string/string.spec.ts rename to src/string/string-manipulation.spec.ts index 6e04489..b8799d2 100644 --- a/src/string/string.spec.ts +++ b/src/string/string-manipulation.spec.ts @@ -1,6 +1,6 @@ -import { dotsToSpaces, truncate, truncateMiddle } from './string'; +import { dotsToSpaces, truncate, truncateMiddle } from './string-manipulation'; -describe('string', () => { +describe('string-manipulation', () => { describe('dotsToSpaces', () => { it('should replace dots with spaces', () => { expect(dotsToSpaces('a.b.c')).toEqual('a b c'); diff --git a/src/string/string.ts b/src/string/string-manipulation.ts similarity index 100% rename from src/string/string.ts rename to src/string/string-manipulation.ts