import AsyncStorage from "@react-native-async-storage/async-storage"; import { browsePlyr, PLYR_ID, plyrSource } from "./plyr"; import { remoteLibraryRequest } from "../../modules/rocksky-engine"; export type LibraryKind = | "navidrome" | "jellyfin" | "upnp" | "kodi" | "plex" | "plyr"; export type LibrarySource = { id: string; kind: LibraryKind; name: string; baseUrl: string; username: string; }; export type LibraryConnection = Omit & { id?: string; password?: string; token?: string; }; export type LibraryEntry = { artistHandle?: string; id: string; title: string; kind: string; artist: string; album: string; durationMs: number; art: string | null; }; export type RemoteTrackMetadata = { title: string; artist: string; album: string; albumArtist?: string; durationMs: number; albumArt: string | null; artistPicture?: string | null; artistLookupAt?: number; artworkLookupAt?: number; genre?: string; year?: number; trackNumber?: number; discNumber?: number; [key: string]: unknown; }; export type LibraryPage = { entries: LibraryEntry[]; nextOffset: number | null; }; export const libraryProviders = [ { kind: "plyr", title: "plyr.fm", hint: "Discover independent music and radio", placeholder: "https://api.plyr.fm", }, { kind: "navidrome", title: "Navidrome / Subsonic", hint: "Your personal music server", placeholder: "https://music.example.com", }, { kind: "jellyfin", title: "Jellyfin", hint: "Your Jellyfin music collection", placeholder: "http://192.168.1.10:8096", }, { kind: "upnp", title: "UPnP / DLNA", hint: "Find media servers on your Wi-Fi", placeholder: "http://192.168.1.10:8200/rootDesc.xml", }, { kind: "kodi", title: "Kodi", hint: "Connect through Kodi’s web server", placeholder: "http://192.168.1.10:8080", }, { kind: "plex", title: "Plex", hint: "Your Plex music libraries", placeholder: "http://192.168.1.10:32400", }, ] as const; export type PlaylistItem = { id: string; entryId: string; removable?: boolean; title: string; artist: string; position: number; }; export type RemoteSearchEntry = LibraryEntry & { sourceId: string; sourceName: string; sourceKind: LibraryKind; }; export type RemoteIndexStatus = { sourceId: string; name: string; state: "idle" | "indexing" | "ready" | "error" | "removed"; updated: number; completed: number; count: number; artworkActive?: boolean; artworkRevision?: number; }; export const remoteLibraries = { search: (query: string, offset = 0, kind = "") => remoteLibraryRequest<{ entries: RemoteSearchEntry[]; nextOffset: number | null; }>({ cmd: "searchIndex", query, offset, kind }), indexStatus: () => remoteLibraryRequest<{ sources: RemoteIndexStatus[] }>({ cmd: "indexStatus", }), reindex: (sourceId: string) => remoteLibraryRequest({ cmd: "indexStart", sourceId, force: true }), playlistCapabilities: (sourceId: string) => remoteLibraryRequest<{ create: boolean; rename: boolean; delete: boolean; items: boolean; move: boolean; add: boolean; }>({ cmd: "playlistOperation", sourceId, operation: "capabilities" }), writablePlaylists: (sourceId: string, id = "", offset = 0) => remoteLibraryRequest({ cmd: "writablePlaylists", sourceId, id, offset, }), addToPlaylist: (sourceId: string, playlistId: string, id: string) => remoteLibraryRequest({ cmd: "addToPlaylist", sourceId, playlistId, id }), playlistOperation: ( sourceId: string, operation: string, params: Record = {}, ) => remoteLibraryRequest<{ playlistId?: string; entries: PlaylistItem[]; nextOffset: number | null; }>({ cmd: "playlistOperation", sourceId, operation, ...params }), metadata: async ( sourceId: string, id: string, seed: Record, ) => ( await remoteLibraryRequest<{ metadata: RemoteTrackMetadata }>({ cmd: "metadata", sourceId, id, seed, }) ).metadata, cacheArtwork: async ( sourceId: string, id: string, artist: string, artistUrl = "", albumUrl = "", ) => ( await remoteLibraryRequest<{ metadata: RemoteTrackMetadata }>({ cmd: "cacheArtwork", sourceId, id, artist, artistUrl, albumUrl, }) ).metadata, list: async () => { const result = await remoteLibraryRequest<{ sources: LibrarySource[] }>({ cmd: "list", }); if (await AsyncStorage.getItem("library:plyr.fm")) result.sources.push(plyrSource); return result; }, connectPlyr: () => AsyncStorage.setItem("library:plyr.fm", "true"), save: (config: LibraryConnection) => remoteLibraryRequest<{ source: LibrarySource }>({ cmd: "save", config }), remove: (sourceId: string) => sourceId === PLYR_ID ? AsyncStorage.removeItem("library:plyr.fm") : remoteLibraryRequest({ cmd: "remove", sourceId }), discover: (kind: "upnp" | "kodi") => remoteLibraryRequest<{ devices: LibrarySource[] }>({ cmd: "discover", kind, }), browse: (sourceId: string, id: string, query: string, offset: number) => sourceId === PLYR_ID ? browsePlyr(id, query, offset) : remoteLibraryRequest({ cmd: "browse", sourceId, id, query, offset, limit: 100, }), artworkFailed: (sourceId: string, entry: LibraryEntry) => sourceId === PLYR_ID ? Promise.resolve({}) : remoteLibraryRequest({ cmd: "artworkFailed", sourceId, entry }), refreshArtwork: (sourceId: string, page: LibraryPage, resume = true) => remoteLibraryRequest({ cmd: "artworkMerge", sourceId, page, resume, }), stream: async (sourceId: string, id: string) => ( await remoteLibraryRequest<{ url: string }>({ cmd: "stream", sourceId, id, }) ).url, };