@obsidian_north/nexa-metadata
v1.0.0
Published
Universal metadata extraction, normalization and editing layer for React Native / Expo — Audio, Video, Images & Documents.
Downloads
182
Maintainers
Readme
@obsidian_north/nexa-metadata
Universal metadata extraction, normalization and editing layer for React Native / Expo.
Sits between MediaStore / NexaCore and your player apps:
MediaStore --URI--> Nexa Metadata --normalized--> Lumora / Moon Player / GalleryDesign: don't expose format chaos to the app.
One API:Metadata.read(uri)→ one normalized structure, regardless of MP3/FLAC/M4A/OGG/MP4/JPEG/PDF.
Install
npm i @obsidian_north/nexa-metadata
# or
npx expo install @obsidian_north/nexa-metadataRequires Expo dev-client for native parsing (npx expo prebuild + npx expo run:android/ios).
In Expo Go / Web the JS fallback still works (extension + magic-byte detection, no heavy parsing).
Quick start
import { Metadata } from "@obsidian_north/nexa-metadata";
// Auto-detects type/format via magic bytes + extension
const result = await Metadata.read("file:///storage/emulated/0/Music/Legends.mp3");
if (result.type === "audio") {
console.log(result.common.title, result.common.artist);
console.log(result.audio.duration, result.audio.codec);
console.log(result.artwork[0]?.uri);
}
// Detection only (cheap)
const det = await Metadata.detect(uri);
// { type: "audio", format: "flac", mimeType: "audio/flac", confidence: 1, method: "magic" }Common vs Raw
common— normalized fields apps actually use (title,artist,album,genre,year,track,disc,comment,composer, ...)raw— format-specific dump (TIT2/TPE1for ID3,©nam/©ARTfor MP4, Vorbis comments, etc.)
API
Metadata.detect(uri): Promise<DetectionResult>
Metadata.read(uri, options?): Promise<MetadataResult>
Metadata.write(uri, patch, config?): Promise<void>
Metadata.remove(uri, fields): Promise<void>
Metadata.batch(uris, mapper, config?): Promise<void>
Metadata.getArtwork(uri, options?): Promise<Artwork[]>
Metadata.artwork(uri, options?): Promise<Artwork[]> // alias
Metadata.extractArtwork(uri, options?): Promise<string> // file:// URI
Metadata.writeArtwork(uri, artworkUri): Promise<void>
// Typed shortcuts
Metadata.audio(uri): Promise<AudioMetadata>
Metadata.video(uri): Promise<VideoMetadata>
Metadata.image(uri): Promise<ImageMetadata>
Metadata.document(uri): Promise<DocumentMetadata>Read options
await Metadata.read(uri, {
includeRaw: true, // default true
includeArtwork: true, // default true
maxArtwork: 1,
});Write
await Metadata.write(uri, {
title: "Legends",
artist: "Juice WRLD",
album: "Goodbye & Good Riddance",
track: 3,
year: 2024,
});
await Metadata.writeArtwork(uri, "file:///cache/cover.jpg");
await Metadata.remove(uri, ["comment", "lyrics"]);
// Batch with concurrency + progress
await Metadata.batch(files, () => ({ genre: "Hip-Hop" }), {
concurrency: 4,
onProgress: (done, total) => console.log(`${done}/${total}`),
});Artwork pipeline
Embedded artwork -> Decode -> Normalize -> Resize -> Cache -> file:// URIconst arts = await Metadata.getArtwork(uri, { type: "front", maxSize: 512 });
const fileUri = await Metadata.extractArtwork(uri, { maxSize: 1000, format: "jpeg", quality: 90 });
// fileUri is file://... — perfect for <Image source={{ uri: fileUri }} />Normalized result shapes
Audio
{
type: "audio",
file: { uri, name, size, extension, mimeType },
common: { title, artist, album, albumArtist, genre, year, track, disc, composer, ... },
audio: { duration, bitrate, sampleRate, channels, codec, lossless, bitsPerSample },
artwork: [{ type: "front", mimeType, uri, width, height }],
raw: { ... }
}Video
{
type: "video",
file: { ... },
video: { duration, width, height, frameRate, bitrate, videoCodec, audioCodec, container },
streams: { video: [...], audio: [...], subtitle: [...] },
chapters: [{ title, startTime, endTime }],
raw: { ... }
}Image
{
type: "image",
file: { ... },
image: { width, height, orientation, mimeType },
exif: { make, model, lens, dateTaken, fNumber, iso, focalLength, location, ... },
iptc: { ... },
xmp: { ... },
raw: { ... }
}Document
{
type: "document",
file: { ... },
document: { title, author, creator, subject, keywords, pages, created, modified },
raw: { ... }
}Detection
{
type: "audio" | "video" | "image" | "document" | "unknown",
format: "mp3" | "flac" | "m4a" | "mp4" | "jpeg" | ... | "unknown",
mimeType: "audio/mpeg",
confidence: 1, // 1 = magic-byte verified, 0.6 = extension guess
method: "magic" | "extension" | "mime" | "unknown",
extension: "mp3"
}URI support
Metadata.read(uri) // accepts:
// file://
// content:// (Android MediaStore)
// ph:// (iOS Photos)
// asset://
// https:// (remote — fetches header for magic detection where allowed)Native architecture — 1.0.0
React Native — 1.0.0 (66 files, 22 tests, TSC 0)
|
Nexa Metadata JS API (src/core/Metadata.ts — Identify/Extract/Normalize/Analyze/Manage/Write)
|
+-- Android (Kotlin) — MediaExtractor per-stream HDR10/HDR10+/HLG/Dolby Vision + PdfRenderer + jaudiotagger + CryptoKit hash
| +-- NexaMetadataDetector.kt (magic + extension + ContentResolver)
| +-- NexaMetadataParser.kt (jaudiotagger + MediaExtractor + ExifInterface + PdfRenderer + chapters API28)
| +-- NexaMetadataModule.kt (Expo Module — transactional write/sanitize/hash/artwork resize)
|
+-- iOS (Swift) — AVAssetTrack demux + CGPDFDocument + ImageIO XMP/IPTC/GPS
+-- AVFoundation (audio/video duration, artwork, chapters, HDR exhaustive)
+-- ImageIO (EXIF / IPTC / XMP, dimensions)
+-- CryptoKit (SHA-256/512/MD5)
+-- UniformTypeIdentifiers (MIME/type)Perf (benchmark/scan.bench.ts): cache 1k <50ms, 10k derived <100ms, 10k magic <100ms, 10k cold-scan fast <200ms JS (native 10k MediaStore scan <2s on Pixel 7 — MediaExtractor header-only, no full file load, file:// + content:// + ph://).
Providers (src/providers/): musicbrainz/coverArt/tmdb real HTTP with offline mock fallback, Metadata.lookup(uri, provider) (§28).
JS layer is the clean API; native does heavy parsing. For formats the platform APIs don't cover (e.g. full ID3v2.4, Vorbis, APE) we add specialized parsers in src/formats/.
No MediaStore dependency — pass any URI in, get normalized metadata out.
Roadmap — 1.0.0 stable ✅
v0.1 architecture, native module foundation, TS models, detection, read API ✅
v0.2 MP3/ID3, FLAC, M4A/MP4, artwork + audio properties (native complete) ✅ MediaExtractor/AVAsset per-stream HDR
v0.3 Video metadata, Image EXIF/IPTC/XMP/GPS, Document metadata ✅ PdfRenderer/CGPDFDocument, EPUB OPF/XMP
v0.4 Metadata writing, artwork writing, batch operations (TagLib/jaudiotagger) ✅ transactional write + sanitize
v1.0 Stable API, Android+iOS, Expo module, tests (22), docs, providers, perf ✅ 10k cold-scan <200ms JS / <2s native (benchmark/scan.bench.ts)Custom parsers
import { MetadataRegistry } from "@obsidian_north/nexa-metadata";
MetadataRegistry.register({
id: "my-parser",
formats: ["myformat"],
types: ["audio"],
canHandle: (d) => d.format === "myformat",
read: async (uri, detection) => ({ ... }),
});Ecosystem
@obsidian_north/nexacore
+-- @obsidian_north/nexaui
+-- @obsidian_north/mediastore
+-- @obsidian_north/nexa-metadata <- you are here
+-- @obsidian_north/obsidian-media-playerDevelopers
License
MIT — Obsidian North
