-
-
Notifications
You must be signed in to change notification settings - Fork 91
/
core.ts
114 lines (98 loc) · 4.15 KB
/
core.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import * as strtok3 from 'strtok3';
import { ParserFactory } from './ParserFactory.js';
import { RandomUint8ArrayReader } from './common/RandomUint8ArrayReader.js';
import { APEv2Parser } from './apev2/APEv2Parser.js';
import { hasID3v1Header } from './id3v1/ID3v1Parser.js';
import { getLyricsHeaderLength } from './lyrics3/Lyrics3.js';
import type { IAudioMetadata, INativeTagDict, IOptions, IPicture, IPrivateOptions, IRandomReader, ITag } from './type.js';
import type { ReadableStream as NodeReadableStream } from 'node:stream/web';
export { IFileInfo } from 'strtok3';
export type AnyWebStream<G> = NodeReadableStream<G> | ReadableStream<G>;
/**
* Parse Web API File
* Requires Blob to be able to stream using a ReadableStreamBYOBReader, only available since Node.js ≥ 20
* @param blob - Blob to parse
* @param options - Parsing options
* @returns Metadata
*/
export async function parseBlob(blob: Blob, options: IOptions = {}): Promise<IAudioMetadata> {
const fileInfo: strtok3.IFileInfo = {mimeType: blob.type, size: blob.size};
if (blob instanceof File) {
fileInfo.path = (blob as File).name;
}
return parseWebStream(blob.stream() as any, fileInfo, options);
}
/**
* Parse audio from Web Stream.Readable
* @param webStream - WebStream to read the audio track from
* @param options - Parsing options
* @param fileInfo - File information object or MIME-type string
* @returns Metadata
*/
export function parseWebStream(webStream: AnyWebStream<Uint8Array>, fileInfo?: strtok3.IFileInfo | string, options: IOptions = {}): Promise<IAudioMetadata> {
return parseFromTokenizer(strtok3.fromWebStream(webStream as any, typeof fileInfo === 'string' ? {mimeType: fileInfo} : fileInfo), options);
}
/**
* Parse audio from Node Buffer
* @param uint8Array - Uint8Array holding audio data
* @param fileInfo - File information object or MIME-type string
* @param options - Parsing options
* @returns Metadata
* Ref: https://github.com/Borewit/strtok3/blob/e6938c81ff685074d5eb3064a11c0b03ca934c1d/src/index.ts#L15
*/
export async function parseBuffer(uint8Array: Uint8Array, fileInfo?: strtok3.IFileInfo | string, options: IOptions = {}): Promise<IAudioMetadata> {
const bufferReader = new RandomUint8ArrayReader(uint8Array);
await scanAppendingHeaders(bufferReader, options);
const tokenizer = strtok3.fromBuffer(uint8Array, typeof fileInfo === 'string' ? {mimeType: fileInfo} : fileInfo);
return parseFromTokenizer(tokenizer, options);
}
/**
* Parse audio from ITokenizer source
* @param tokenizer - Audio source implementing the tokenizer interface
* @param options - Parsing options
* @returns Metadata
*/
export function parseFromTokenizer(tokenizer: strtok3.ITokenizer, options?: IOptions): Promise<IAudioMetadata> {
return ParserFactory.parseOnContentType(tokenizer, options);
}
/**
* Create a dictionary ordered by their tag id (key)
* @param nativeTags list of tags
* @returns tags indexed by id
*/
export function orderTags(nativeTags: ITag[]): INativeTagDict {
const tags = {};
for (const tag of nativeTags) {
(tags[tag.id] = (tags[tag.id] || [])).push(tag.value);
}
return tags;
}
/**
* Convert rating to 1-5 star rating
* @param rating: Normalized rating [0..1] (common.rating[n].rating)
* @returns Number of stars: 1, 2, 3, 4 or 5 stars
*/
export function ratingToStars(rating: number): number {
return rating === undefined ? 0 : 1 + Math.round(rating * 4);
}
/**
* Select most likely cover image.
* @param pictures Usually metadata.common.picture
* @return Cover image, if any, otherwise null
*/
export function selectCover(pictures?: IPicture[]): IPicture | null {
return pictures ? pictures.reduce((acc, cur) => {
if (cur.name && cur.name.toLowerCase() in ['front', 'cover', 'cover (front)'])
return cur;
return acc;
}) : null;
}
export async function scanAppendingHeaders(randomReader: IRandomReader, options: IPrivateOptions = {}) {
let apeOffset = randomReader.fileSize;
if (await hasID3v1Header(randomReader)) {
apeOffset -= 128;
const lyricsLen = await getLyricsHeaderLength(randomReader);
apeOffset -= lyricsLen;
}
options.apeHeader = await APEv2Parser.findApeFooterOffset(randomReader, apeOffset);
}