import { requireNativeModule, type NativeModule } from 'expo-modules-core'; /** A single audio file found during the SAF tree walk. */ export interface ScannedFile { /** SAF document URI — playable directly by ExoPlayer and readable by MMR. */ uri: string; name: string; size: number | null; lastModified: number; mimeType: string | null; /** Document URI of the containing directory (key into `ListResult.covers`). */ parentUri: string; } export interface ListResult { files: ScannedFile[]; /** Best external cover-art candidate per directory (cover/folder/front/albumart). */ covers: Record; } export interface ExtractedMetadata { uri: string; ok: boolean; error?: string; title?: string | null; artist?: string | null; /** Ordered repeated ARTIST tag values; empty when the file has no multi-value credit. */ artistNames?: string[]; album?: string | null; albumArtist?: string | null; /** Ordered repeated ALBUMARTIST tag values; empty when not multi-valued. */ albumArtistNames?: string[]; genre?: string | null; /** Container mime type reported by MediaMetadataRetriever. */ mimeType?: string | null; /** Audio track mime type from MediaExtractor (e.g. "audio/flac"). */ codecMime?: string | null; durationMs?: number | null; bitrate?: number | null; trackNumber?: number | null; discNumber?: number | null; year?: number | null; sampleRate?: number | null; channels?: number | null; bitsPerSample?: number | null; /** File name in the artwork cache dir: `md5(bytes) + extension`. */ artworkHash?: string | null; } /** ReplayGain tags read from a file's container (null = tag absent). */ export interface ReplayGainTags { trackGainDb: number | null; albumGainDb: number | null; trackPeak: number | null; albumPeak: number | null; } /** A `.xlrc`/`.lrc` file found next to the track. */ export interface SidecarLyrics { text: string; format: 'xlrc' | 'lrc'; } export interface EmbeddedLyricsSyncText { timestampMs: number; text: string; } /** Embedded lyrics read from container tags without decoding audio. */ export type EmbeddedLyricsReadResult = | { status: 'hit'; text: string | null; syncText: EmbeddedLyricsSyncText[]; } | { status: 'missing' } | { status: 'unavailable' }; export interface ScanProgressEvent { phase: 'discovering' | 'extracting' | 'indexing'; found?: number; processed?: number; total?: number; folderName?: string; } export interface NativeScanResult { added: number; updated: number; removed: number; errors: number; total: number; catalogRevision: string; cancelled: boolean; } /** * Partial waveform emitted while `analyzeTrack` decodes, so the seek bar can fill in * left-to-right. `peaks` holds RAW (un-normalized) RMS for bins `[0, filledBins)` — the * global max isn't known until the decode ends, so callers normalize against the max of * what they've received so far and accept a slight rescale as louder material arrives. */ export interface WaveformProgressEvent { /** Track URI this partial belongs to — callers must filter, decodes overlap. */ uri: string; filledBins: number; totalBins: number; peaks: number[]; } type AstraLibraryScannerEvents = { onScanProgress: (event: ScanProgressEvent) => void; onWaveformProgress: (event: WaveformProgressEvent) => void; }; /** One decode pass: waveform peaks + (optionally) loudness, plus timing. */ export interface TrackAnalysis { /** `bins` RMS peaks normalized to [0,1]; empty on failure. */ peaks: number[]; /** Integrated LUFS; null when unmeasured (withLoudness false) or unmeasurable. */ lufs: number | null; /** Absolute sample peak, linear [0,1]; null when unmeasured. */ peak: number | null; /** True if the decode bailed early — do NOT persist peaks or loudness. */ cancelled: boolean; decodeMs: number | null; durationMs: number | null; /** durationMs / decodeMs — how many times faster than realtime the decode ran. */ realtimeFactor: number | null; decoderName: string | null; mime: string | null; withLoudness: boolean; } declare class AstraLibraryScannerModuleType extends NativeModule { listAudioFiles(treeUri: string, extensions: string[]): Promise; extractMetadata(files: { uri: string; coverUri?: string | null }[]): Promise; scanFolderNative( folderId: number, mode: 'incremental' | 'full', extensions: string[] ): Promise; /** Cooperatively stop every active or queued library scan at its next safe checkpoint. */ cancelScan(): void; /** * ONE whole-file PCM decode producing `bins` RMS waveform peaks and, when * `withLoudness`, gated integrated LUFS + sample peak. Both analyses need every * sample, so they share a pass — ask for loudness here whenever you'd otherwise * measure it separately. Heavy; concurrency is capped natively at 2 and results * should be cached. Emits `onWaveformProgress` as bins finalize. */ analyzeTrack(uri: string, bins: number, withLoudness: boolean): Promise; /** * Stop an in-flight (or still-queued) `analyzeTrack` for this URI so a skipped-past * track stops burning CPU. Safe to call when nothing is running. */ cancelAnalysis(uri: string): Promise; /** * Decode short windows across the file and return approximate RMS peaks for * immediate seek-bar paint. Cheap preview only; callers should not persist it. */ extractWaveformPreview(uri: string, bins: number): Promise; /** * Read ReplayGain track/album gain (dB) + peak (linear) from container tags * (ID3 TXXX / Vorbis comments / MP4 freeform) without decoding audio. All fields * are null when the tag is absent; the whole call is cheap (metadata only). */ readReplayGain(uri: string): Promise; /** * Look for a sibling lyrics file next to the track (`.xlrc` preferred, then * `.lrc`) and return its text + format, or null. Read fresh on demand so * files authored after a scan are picked up. */ readSidecarLyrics(uri: string): Promise; /** * Read embedded lyrics from Vorbis, ID3 (USLT/SYLT/TXXX), and MP4/M4A tags * without decoding audio. `missing` means metadata was read successfully but * no supported tag was present; `unavailable` preserves cache on I/O failure. */ readEmbeddedLyrics(uri: string): Promise; getArtworkDirPath(): string; getArtworkThumbDirPath(): string; ensureArtworkThumbnails(hashes: string[]): Promise; /** Validate and content-addressably cache a JPEG, PNG, or WebP URI plus thumbnail. */ cacheArtworkFromUri(uri: string): Promise; /** * A wait that still elapses while the app is backgrounded, unlike `setTimeout` * — React Native stops firing JS timers once the activity pauses. */ backgroundDelay(milliseconds: number): Promise; getPersistedTreeUris(): string[]; takePersistableUriPermission(uri: string): Promise; releasePersistedUriPermission(uri: string): Promise; /** * Scan keepalive (Android). `startScanService` promotes a `dataSync` foreground * service + partial wakelock so a JS-orchestrated scan keeps running when the app * is backgrounded / the screen sleeps, and shows a progress notification; * `updateScanNotification` refreshes it; `stopScanService` tears it down. The * wakelock/keepalive work even if the notification itself is not permitted. */ startScanService(title: string, text: string): void; updateScanNotification( title: string, text: string, subText: string | null, current: number, total: number, indeterminate: boolean ): void; stopScanService(): void; } export const AstraLibraryScanner = requireNativeModule('AstraLibraryScanner'); export type LibraryStatus = | 'initializing' | 'empty' | 'ready' | 'scanning' | 'rebuilding' | 'degraded' | 'fatalUserData'; export interface LibraryStatusSnapshot { status: LibraryStatus; catalogRevision: string; trackCount: number; message: string | null; recoveryNotice: string | null; } export interface NativePage { items: T[]; nextCursor: string | null; previousCursor: string | null; totalCount: number; catalogRevision: string; error?: 'STALE_REVISION'; } export type LibraryQuery = | { kind: 'library'; sort: 'artist' | 'title' | 'recently_added' | 'duration'; direction: 'asc' | 'desc'; } | { kind: 'album'; albumKey: string } | { kind: 'artist'; artistKey: string; groupingMode: 'astra' | 'fileTags'; section: 'songs' | 'appearances' | 'all'; } | { kind: 'folder'; folderNodeId?: string; folderId?: number } | { kind: 'playlist'; playlistId: number } | { kind: 'favorites' } | { kind: 'recent' } | { kind: 'search'; query: string } | { kind: 'manual'; paths: string[] } | { kind: 'dynamicPlaylist'; playlistId: number }; export interface NativePlaybackWindow { sessionId: string; items: (T & { queuePosition: number; queueEntryId: number })[]; windowStart: number; activePosition: number; totalCount: number; contextJson: string; shuffleSeed: number | null; queueRevision: number; catalogRevision: string; } export interface LibrarySectionAnchor { label: string; cursor: string; } export interface NativeFolderNode { id: string; folderId: number; parentNodeId: string | null; name: string; depth: number; directTrackCount: number; totalTrackCount: number; available: boolean; catalogRevision: string; } export interface NativeTrackLoudness { path: string; loudness_lufs: number | null; sample_peak: number | null; replay_gain_track_db: number | null; replay_gain_album_db: number | null; replay_gain_track_peak: number | null; replay_gain_album_peak: number | null; rg_scanned: number; } export interface NativeLibraryLoudnessStats { lufsCount: number; medianLufs: number | null; rgCount: number; medianRgTrackDb: number | null; } export interface NativeArtistImageLookupTarget { groupingMode: 'astra' | 'fileTags'; artistKey: string; artistName: string; retryCount: number; } export interface NativeArtistImageState { groupingMode: 'astra' | 'fileTags'; artistKey: string; artistName?: string; manualImageHash: string | null; automaticImageHash: string | null; automaticProvider: 'deezer' | null; automaticSourceId: string | null; lookupStatus: 'never' | 'found' | 'not_found' | 'transient_error'; retryCount: number; lastAttemptAt: number | null; nextRetryAt: number | null; updatedAt: number | null; } type AstraLibraryDataEvents = { onLibraryStatus: (event: LibraryStatusSnapshot) => void; onScanProgress: (event: { scanId: string; phase: 'discovering' | 'extracting' | 'publishing'; processed: number; total: number; folderName: string; }) => void; onCatalogChanged: (event: { catalogRevision: string }) => void; onArtistImagesChanged: (event: { artistKey: string; groupingMode: 'astra' | 'fileTags'; }) => void; }; declare class AstraLibraryDataModuleType extends NativeModule { initialize(): Promise; getCurrentStatus(): LibraryStatusSnapshot; getSettings(keys: string[]): Promise>; setSettings(values: Record): Promise; listFolders(): Promise[]>; getFolderNodes(parentNodeId: string | null): Promise; getFolderTracks( nodeId: string, offset: number, limit: number ): Promise<{ items: T[]; nextOffset: number | null; totalCount: number; catalogRevision: string; }>; registerFolder(treeUri: string, displayName: string): Promise>; removeFolder(folderId: number): Promise; getTrackPage( sort: 'artist' | 'title' | 'recently_added' | 'duration', direction: 'asc' | 'desc', cursor: string | null, limit: number ): Promise>; /** * The page immediately above `cursor` — how the lists refill upwards after an A-Z * jump lands mid-catalog. Items come back ascending; `previousCursor` is null once * there is nothing left above. Only the rail's sorts can be walked backwards. */ getTrackPageBefore( sort: 'artist' | 'title', direction: 'asc' | 'desc', cursor: string | null, limit: number ): Promise>; getTrack(path: string): Promise; getTrackLoudness(paths: string[]): Promise; setTrackLoudness(path: string, lufs: number | null, samplePeak: number | null): Promise; setTrackReplayGain( path: string, trackGainDb: number | null, albumGainDb: number | null, trackPeak: number | null, albumPeak: number | null ): Promise; getLibraryLoudnessStats(): Promise; getWaveform(path: string): Promise; putWaveform(path: string, peaks: number[]): Promise; countWaveforms(): Promise; clearWaveforms(): Promise; getLyrics(path: string, metadataSignature: string): Promise; putLyrics(path: string, values: Record): Promise; deleteLyrics(path: string): Promise; countLyrics(): Promise; clearLyrics(): Promise; readMobileSession(): Promise; writeMobileSession(snapshotJson: string): Promise; createPlaybackContext( context: LibraryQuery, anchorPath: string | null, shuffle: boolean, seed: number | null ): Promise>; getPlaybackWindow( sessionId: string, start: number, limit: number ): Promise>; updatePlaybackPosition(sessionId: string, activePosition: number): Promise; restorePlaybackContext(): Promise | null>; mutatePlaybackContext( operation: | 'insertAfterActive' | 'append' | 'insertQueryAfterActive' | 'appendQuery' | 'remove' | 'move' | 'moveManyAfterActive' | 'shuffle', values: Record ): Promise | null>; recordTrackPlayed(path: string): Promise; getListeningHistoryStatus(): Promise; checkpointListeningSession(payload: Record): Promise; getListeningStatsDashboard(query: Record): Promise; clearDetailedListeningHistory(): Promise; getRecentlyPlayed(limit: number): Promise; listRemoteSources(): Promise; getRemoteSource(sourceId: number): Promise; createRemoteSource( type: 'subsonic' | 'jellyfin', name: string, baseUrl: string, username: string, enabled: boolean ): Promise; updateRemoteSource(sourceId: number, fields: Record): Promise; setRemoteSourceStatus(sourceId: number, status: string, error: string | null): Promise; deleteRemoteSource(sourceId: number, purgeCatalog: boolean): Promise; replaceRemoteUserState( sourceId: number, sourceType: 'subsonic' | 'jellyfin', favoritePaths: string[], playlists: Record[] ): Promise; beginRemoteSync(sourceId: number, sourceType: 'subsonic' | 'jellyfin'): Promise; appendRemoteTracks(syncId: string, rows: Record[]): Promise; commitRemoteSync( syncId: string ): Promise<{ tracksScanned: number; removed: number; catalogRevision: string }>; abortRemoteSync(syncId: string): Promise; listPlaylists(): Promise; createPlaylist(name: string, kind: 'normal' | 'dynamic', rulesJson: string | null): Promise; getDynamicPlaylistRules(playlistId: number): Promise; updateDynamicPlaylistRules(playlistId: number, rulesJson: string): Promise; previewDynamicPlaylist(rulesJson: string): Promise; renamePlaylist(playlistId: number, name: string): Promise; deletePlaylist(playlistId: number): Promise; markPlaylistPlayed(playlistId: number): Promise; addPlaylistEntries( playlistId: number, entries: { trackPath: string; fallbackTitle?: string | null; fallbackArtist?: string | null; fallbackAlbum?: string | null; }[] ): Promise; removePlaylistEntry(playlistId: number, path: string): Promise; movePlaylistEntry(playlistId: number, path: string, direction: -1 | 1): Promise; getPlaylistEntries( playlistId: number, offset: number, limit: number ): Promise<{ items: T[]; nextOffset: number | null; totalCount: number }>; getFavoritePaths(): Promise; getFavoriteTracks(limit: number): Promise; setFavorite(path: string, favorite: boolean): Promise; getDesktopSyncState(): Promise; applyDesktopSyncPlan(plan: Record): Promise; resolveDesktopSyncConflict( conflict: Record, resolution: 'desktop' | 'phone' | 'both' | 'merge', mergedPlaylist: Record | null ): Promise; clearDesktopSyncBaselines(): Promise; getAlbumPage( sort: 'artist' | 'name' | 'recently_added' | 'year', direction: 'asc' | 'desc', includeSingles: boolean, cursor: string | null, limit: number ): Promise>; /** Backward twin of `getAlbumPage`; see `getTrackPageBefore`. */ getAlbumPageBefore( sort: 'artist' | 'name', direction: 'asc' | 'desc', includeSingles: boolean, cursor: string | null, limit: number ): Promise>; getArtistPage( sort: 'name' | 'track_count', direction: 'asc' | 'desc', groupingMode: 'astra' | 'fileTags', includeCollaborations: boolean, cursor: string | null, limit: number ): Promise>; /** Backward twin of `getArtistPage`; see `getTrackPageBefore`. */ getArtistPageBefore( sort: 'name', direction: 'asc' | 'desc', groupingMode: 'astra' | 'fileTags', includeCollaborations: boolean, cursor: string | null, limit: number ): Promise>; getAlbumDetail>( albumKey: string, cursor: string | null, limit: number ): Promise & { summary: S | null }>; getArtistDetail>( artistKey: string, groupingMode: 'astra' | 'fileTags', section: 'songs' | 'appearances' | 'all', cursor: string | null, limit: number ): Promise & { summary: S | null }>; getArtistAlbums( artistKey: string, groupingMode: 'astra' | 'fileTags', offset: number, limit: number ): Promise<{ items: T[]; nextOffset: number | null; totalCount: number; catalogRevision: string; }>; getPendingArtistImageLookups( limit: number, now: number ): Promise; /** * Re-queues artists a provider previously had no match for, returning how many * became pending. `not_found` is otherwise terminal. */ clearArtistImageLookupFailures(): Promise; /** * `pending` = distinct artists awaiting a lookup across both grouping modes * (the denominator for sweep progress). `missing` = artists in `groupingMode` * with no portrait at all, including ones already written off as not_found. */ getArtistImageStats( groupingMode: 'astra' | 'fileTags', now: number ): Promise<{ pending: number; missing: number }>; getArtistImageState( artistKey: string, groupingMode: 'astra' | 'fileTags' ): Promise; recordArtistImageLookup( artistKey: string, artistName: string, groupingMode: 'astra' | 'fileTags', values: { status: 'found' | 'not_found' | 'transient_error'; automaticImageHash?: string | null; provider?: 'deezer' | null; sourceId?: string | null; attemptedAt: number; nextRetryAt?: number | null; clearManual?: boolean; } ): Promise; setManualArtistImage( artistKey: string, artistName: string, groupingMode: 'astra' | 'fileTags', artworkHash: string ): Promise; clearManualArtistImage( artistKey: string, artistName: string, groupingMode: 'astra' | 'fileTags' ): Promise; searchTracks(query: string, limit: number): Promise; searchLibrary( query: string, limit: number, includeSingles: boolean, groupingMode: 'astra' | 'fileTags', includeCollaborations: boolean ): Promise<{ tracks: TTrack[]; albums: TAlbum[]; artists: TArtist[] }>; matchSignal( title: string, artist: string, durationSeconds: number | null ): Promise<{ kind: 'match' | 'ambiguous' | 'none'; candidates: { track: T; match: 'exact' | 'normalized'; durationDeltaSec: number | null }[]; }>; getSectionAnchors( kind: 'tracks' | 'albums' | 'artists', sort: 'artist' | 'title' | 'name', direction: 'asc' | 'desc', includeSingles: boolean, groupingMode: 'astra' | 'fileTags', includeCollaborations: boolean ): Promise; flushUserSnapshot(): Promise; } export const AstraLibraryData = requireNativeModule('AstraLibraryData'); export { AstraQueue, AstraQueueView, toNativeQueuePalette, } from './queue'; export type { AstraQueueViewProps, NativeQueuePlaybackRequest, NativeQueuePalette, NativeQueuePresentationOptions, NativeQueueRevisionEvent, } from './queue';