From d1d4deecf39db2947b60272de10415b49c6c4fdb Mon Sep 17 00:00:00 2001 From: ziven Date: Sun, 26 Apr 2026 19:52:55 +0800 Subject: [PATCH 1/3] fix: null artwork error --- example/src/contexts/PlayerContext.tsx | 13 +++++++++++-- example/src/pages/AlbumTrackListScreen.tsx | 6 +++--- example/src/pages/TrackListScreen.tsx | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/example/src/contexts/PlayerContext.tsx b/example/src/contexts/PlayerContext.tsx index d06b9d2..f0d13c9 100644 --- a/example/src/contexts/PlayerContext.tsx +++ b/example/src/contexts/PlayerContext.tsx @@ -3,6 +3,15 @@ import type { Track } from '../../../src/NativeMusicLibrary'; import type { AudioProTrack } from 'react-native-audio-pro'; import { AudioPro } from 'react-native-audio-pro'; +export function toAudioProTrack(track: Track): AudioProTrack { + return { + ...track, + artwork: track.artwork ?? require('../assets/default_artwork.png'), + artist: track.artist ?? '', + album: track.album ?? '', + } as unknown as AudioProTrack; +} + interface PlayerContextType { playlist: Track[]; setPlaylist: (tracks: Track[]) => void; @@ -24,7 +33,7 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) { const nextIndex = (currentIndex + 1) % playlist.length; const nextTrack = playlist[nextIndex]; if (nextTrack) { - AudioPro.play(nextTrack as unknown as AudioProTrack); + AudioPro.play(toAudioProTrack(nextTrack)); } }, [playlist]); @@ -37,7 +46,7 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) { const prevIndex = (currentIndex - 1 + playlist.length) % playlist.length; const prevTrack = playlist[prevIndex]; if (prevTrack) { - AudioPro.play(prevTrack as unknown as AudioProTrack); + AudioPro.play(toAudioProTrack(prevTrack)); } }, [playlist]); diff --git a/example/src/pages/AlbumTrackListScreen.tsx b/example/src/pages/AlbumTrackListScreen.tsx index 5a20e59..40a2863 100644 --- a/example/src/pages/AlbumTrackListScreen.tsx +++ b/example/src/pages/AlbumTrackListScreen.tsx @@ -13,8 +13,8 @@ import { type Track, } from '@nodefinity/react-native-music-library'; import TrackItem from '../components/TrackItem'; -import { usePlayer } from '../contexts/PlayerContext'; -import { AudioPro, type AudioProTrack } from 'react-native-audio-pro'; +import { usePlayer, toAudioProTrack } from '../contexts/PlayerContext'; +import { AudioPro } from 'react-native-audio-pro'; import { useEffect, useState } from 'react'; type Props = NativeStackScreenProps; @@ -40,7 +40,7 @@ export default function AlbumTrackListScreen({ navigation, route }: Props) { const handleTrackPress = (track: Track) => { setPlaylist(albumTracks); - AudioPro.play(track as unknown as AudioProTrack); + AudioPro.play(toAudioProTrack(track)); console.log('Playing track:', track.title); navigation.navigate('Player'); }; diff --git a/example/src/pages/TrackListScreen.tsx b/example/src/pages/TrackListScreen.tsx index 04f0f53..a0dd95c 100644 --- a/example/src/pages/TrackListScreen.tsx +++ b/example/src/pages/TrackListScreen.tsx @@ -10,8 +10,8 @@ import { pickDirectory } from '@react-native-documents/picker'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { usePlayer } from '../contexts/PlayerContext'; -import { AudioPro, type AudioProTrack } from 'react-native-audio-pro'; +import { usePlayer, toAudioProTrack } from '../contexts/PlayerContext'; +import { AudioPro } from 'react-native-audio-pro'; import TrackItem from '../components/TrackItem'; type Props = NativeStackScreenProps; @@ -97,7 +97,7 @@ export default function TrackListScreen({ navigation }: Props) { const handleTrackPress = (track: Track) => { setPlaylist(tracks); - AudioPro.play(track as unknown as AudioProTrack); + AudioPro.play(toAudioProTrack(track)); console.log('Playing track:', track.title); navigation.navigate('Player'); }; From 85dc5a39645d9a1332798555a9924f195656c55f Mon Sep 17 00:00:00 2001 From: ziven Date: Sun, 26 Apr 2026 20:02:25 +0800 Subject: [PATCH 2/3] feat: implement iOS methods --- README.md | 242 +++++++++++++++++++++-- README_ZH.md | 247 +++++++++++++++++++++--- ios/MusicLibraryImpl.swift | 38 ++-- ios/albums/GetAlbumsByArtistQuery.swift | 26 +++ ios/albums/GetAlbumsQuery.swift | 87 +++++++++ ios/artists/GetArtistsQuery.swift | 85 ++++++++ ios/tracks/GetTracks.swift | 12 ++ ios/tracks/GetTracksByArtistQuery.swift | 104 ++++++++++ src/NativeMusicLibrary.ts | 6 +- src/NativeMusicLibrary.web.ts | 2 +- 10 files changed, 776 insertions(+), 73 deletions(-) create mode 100644 ios/albums/GetAlbumsByArtistQuery.swift create mode 100644 ios/albums/GetAlbumsQuery.swift create mode 100644 ios/artists/GetArtistsQuery.swift create mode 100644 ios/tracks/GetTracksByArtistQuery.swift diff --git a/README.md b/README.md index 909cd8e..72a6954 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ A powerful React Native library for accessing local music files and getting full metadata. Built with React Native's New Architecture (TurboModules) for optimal performance. -
Home Screen Track List @@ -18,15 +17,15 @@ A powerful React Native library for accessing local music files and getting full ## ✨ Features -- 🎵 **Rich Metadata** - Access local music with full metadata including lyrics +- 🎵 **Rich Metadata** - Access local music with full metadata including lyrics, bitrate, sample rate, and more - 🚀 **TurboModules** - Built with React Native's New Architecture for maximum performance -- 📄 **Pagination** - Efficient handling of large music collections +- 📄 **Pagination** - Cursor-based pagination for efficiently handling large music collections - 🔍 **Flexible Sorting** - Multiple sorting options for tracks, albums, and artists -- 📁 **Directory Filtering** - Filter music by specific directories +- 📁 **Directory Filtering** - Filter tracks by specific directories (Android) - 🔄 **TypeScript** - Full type definitions and type safety - 🎨 **Album Artwork** - Support for album artwork and cover images -- 🤖 **Android Support** - Full native Android implementation -- 📱 **iOS Support** - Coming soon +- 🤖 **Android** - Full native Android implementation +- 🍎 **iOS** - Full native iOS implementation via MediaPlayer framework ## 🚀 Quick Start @@ -38,31 +37,234 @@ npm install @nodefinity/react-native-music-library yarn add @nodefinity/react-native-music-library ``` +### Permissions + +**Android** — add to `android/app/src/main/AndroidManifest.xml`: + +```xml + + +``` + +**iOS** — add to `Info.plist`: + +```xml +NSAppleMusicUsageDescription +This app needs access to your music library. +``` + ### Basic Usage ```js -import { getTracksAsync, getAlbumsAsync, getArtistsAsync } from '@nodefinity/react-native-music-library'; +import { + getTracksAsync, + getAlbumsAsync, + getArtistsAsync, +} from '@nodefinity/react-native-music-library'; -// Get tracks -const tracks = await getTracksAsync(); +// Get first 20 tracks sorted by title +const result = await getTracksAsync({ sortBy: ['title', true] }); +console.log(result.items); // Track[] +console.log(result.hasNextPage); // boolean +console.log(result.endCursor); // string | undefined -// Get albums with sorting -const albums = await getAlbumsAsync({ - sortBy: ['title', true], // Sort by title ascending - first: 50 +// Get next page +const nextPage = await getTracksAsync({ + sortBy: ['title', true], + first: 20, + after: result.endCursor, }); +``` + +## 📖 API -// Get artists -const artists = await getArtistsAsync(); +### `getTracksAsync(options?)` + +Returns a paginated list of tracks from the music library. + +```ts +getTracksAsync(options?: TrackOptions): Promise> ``` -### Android Permissions +**TrackOptions** -Add to `android/app/src/main/AndroidManifest.xml`: +| Property | Type | Default | Description | +| ----------- | -------------------------------------------------------- | ----------- | --------------------------------------- | +| `first` | `number` | `20` | Max number of items to return | +| `after` | `string` | — | Cursor from previous page's `endCursor` | +| `sortBy` | `TrackSortByKey \| [TrackSortByKey, boolean] \| (...)[]` | `'default'` | Sort key, or tuple `[key, ascending]` | +| `directory` | `string` | — | Filter by directory path (Android only) | -```xml - - +**TrackSortByKey**: `'default' \| 'title' \| 'artist' \| 'album' \| 'duration' \| 'createdAt' \| 'modifiedAt' \| 'fileSize'` + +--- + +### `getTrackMetadataAsync(trackId)` + +Returns detailed audio metadata for a single track. + +```ts +getTrackMetadataAsync(trackId: string): Promise +``` + +--- + +### `getTracksByAlbumAsync(albumId)` + +Returns all tracks in an album (no pagination). + +```ts +getTracksByAlbumAsync(albumId: string): Promise +``` + +--- + +### `getTracksByArtistAsync(artistId, options?)` + +Returns a paginated list of tracks by an artist. + +```ts +getTracksByArtistAsync(artistId: string, options?: TrackOptions): Promise> +``` + +--- + +### `getAlbumsAsync(options?)` + +Returns a paginated list of albums. + +```ts +getAlbumsAsync(options?: AlbumOptions): Promise> +``` + +**AlbumOptions** + +| Property | Type | Default | Description | +| -------- | -------------------------------------------------------- | ----------- | --------------------------------------- | +| `first` | `number` | `20` | Max number of items to return | +| `after` | `string` | — | Cursor from previous page's `endCursor` | +| `sortBy` | `AlbumSortByKey \| [AlbumSortByKey, boolean] \| (...)[]` | `'default'` | Sort key, or tuple `[key, ascending]` | + +**AlbumSortByKey**: `'default' \| 'title' \| 'artist' \| 'trackCount' \| 'year'` + +--- + +### `getAlbumsByArtistAsync(artistId)` + +Returns all albums by an artist (no pagination). + +```ts +getAlbumsByArtistAsync(artistId: string): Promise +``` + +--- + +### `getArtistsAsync(options?)` + +Returns a paginated list of artists. + +```ts +getArtistsAsync(options?: ArtistOptions): Promise> +``` + +**ArtistOptions** + +| Property | Type | Default | Description | +| -------- | ---------------------------------------------------------- | ----------- | --------------------------------------- | +| `first` | `number` | `20` | Max number of items to return | +| `after` | `string` | — | Cursor from previous page's `endCursor` | +| `sortBy` | `ArtistSortByKey \| [ArtistSortByKey, boolean] \| (...)[]` | `'default'` | Sort key, or tuple `[key, ascending]` | + +**ArtistSortByKey**: `'default' \| 'title' \| 'trackCount' \| 'albumCount'` + +--- + +## 📦 Types + +### `Track` + +| Field | Type | Description | +| ------------ | --------- | ----------------------------------- | +| `id` | `string` | Unique identifier | +| `title` | `string` | Track title | +| `artist` | `string` | Artist name | +| `artwork` | `string?` | Artwork file URI (may be undefined) | +| `album` | `string` | Album name | +| `duration` | `number` | Duration in seconds | +| `url` | `string` | File URI | +| `createdAt` | `number` | Unix timestamp (seconds) | +| `modifiedAt` | `number` | Unix timestamp (seconds) | +| `fileSize` | `number` | File size in bytes | + +### `TrackMetadata` + +| Field | Type | Description | +| ------------- | -------- | ------------------------------------ | +| `id` | `string` | Track ID | +| `duration` | `number` | Duration in seconds | +| `bitrate` | `number` | Bitrate in kbps | +| `sampleRate` | `number` | Sample rate in Hz | +| `channels` | `string` | Number of audio channels | +| `format` | `string` | Audio format (e.g. `"AAC"`, `"MP3"`) | +| `title` | `string` | Title tag | +| `artist` | `string` | Artist tag | +| `album` | `string` | Album tag | +| `year` | `number` | Release year | +| `genre` | `string` | Genre tag | +| `track` | `number` | Track number | +| `disc` | `number` | Disc number | +| `composer` | `string` | Composer tag | +| `lyricist` | `string` | Lyricist tag | +| `lyrics` | `string` | Embedded lyrics | +| `albumArtist` | `string` | Album artist tag | +| `comment` | `string` | Comment tag | + +### `Album` + +| Field | Type | Description | +| ------------ | --------- | ------------------------------ | +| `id` | `string` | Unique identifier | +| `title` | `string` | Album name | +| `artist` | `string` | Primary artist | +| `artwork` | `string?` | Artwork URI (may be undefined) | +| `trackCount` | `number` | Number of tracks | +| `year` | `number?` | Release year | + +### `Artist` + +| Field | Type | Description | +| ------------ | -------- | ---------------------- | +| `id` | `string` | Unique identifier | +| `title` | `string` | Artist name | +| `albumCount` | `number` | Number of albums | +| `trackCount` | `number` | Total number of tracks | + +### `PaginatedResult` + +| Field | Type | Description | +| ------------- | --------- | ----------------------------------------- | +| `items` | `T[]` | Array of results | +| `hasNextPage` | `boolean` | Whether more items are available | +| `endCursor` | `string?` | Pass to `after` to fetch next page | +| `totalCount` | `number?` | Total count (may be expensive to compute) | + +--- + +## 🔄 Pagination Example + +```ts +async function fetchAllTracks() { + const allTracks = []; + let cursor: string | undefined; + + do { + const result = await getTracksAsync({ first: 50, after: cursor }); + allTracks.push(...result.items); + cursor = result.hasNextPage ? result.endCursor : undefined; + } while (cursor); + + return allTracks; +} ``` ## 🤝 Contributing diff --git a/README_ZH.md b/README_ZH.md index 8df83d1..cb40b4d 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -5,7 +5,7 @@ [English](./README.md) -基于 React Native 新架构(TurboModules)构建的 React Native 库,用于访问本地音乐文件并获取完整的元数据信息。 +基于 React Native 新架构(TurboModules)构建的本地音乐库访问库,支持完整元数据读取。
Home Screen @@ -17,15 +17,15 @@ ## ✨ 特性 -- 🎵 **丰富元数据** - 访问本地音乐并获取完整元数据,包括歌词 +- 🎵 **丰富元数据** - 读取完整音频元数据,包括歌词、比特率、采样率等 - 🚀 **TurboModules** - 基于 React Native 新架构构建,性能卓越 -- 📄 **分页支持** - 高效处理大型音乐集合 +- 📄 **游标分页** - 高效处理大型音乐集合 - 🔍 **灵活排序** - 支持曲目、专辑和艺术家的多种排序选项 -- 📁 **目录过滤** - 按特定目录过滤音乐 -- 🔄 **TypeScript** - 完整的类型定义和类型安全 -- 🎨 **专辑封面** - 支持专辑封面和图片 -- 🤖 **Android 支持** - 完整的原生 Android 实现 -- 📱 **iOS 支持** - 即将推出 +- 📁 **目录过滤** - 按目录路径过滤曲目(Android) +- 🔄 **TypeScript** - 完整类型定义 +- 🎨 **专辑封面** - 支持专辑封面图片 +- 🤖 **Android** - 完整原生 Android 实现 +- 🍎 **iOS** - 基于 MediaPlayer 框架的完整原生 iOS 实现 ## 🚀 快速开始 @@ -37,31 +37,234 @@ npm install @nodefinity/react-native-music-library yarn add @nodefinity/react-native-music-library ``` +### 权限配置 + +**Android** — 在 `android/app/src/main/AndroidManifest.xml` 中添加: + +```xml + + +``` + +**iOS** — 在 `Info.plist` 中添加: + +```xml +NSAppleMusicUsageDescription +此应用需要访问您的音乐库。 +``` + ### 基本用法 ```js -import { getTracksAsync, getAlbumsAsync, getArtistsAsync } from '@nodefinity/react-native-music-library'; +import { + getTracksAsync, + getAlbumsAsync, + getArtistsAsync, +} from '@nodefinity/react-native-music-library'; -// 获取曲目 -const tracks = await getTracksAsync(); +// 获取前 20 首曲目,按标题升序排序 +const result = await getTracksAsync({ sortBy: ['title', true] }); +console.log(result.items); // Track[] +console.log(result.hasNextPage); // boolean +console.log(result.endCursor); // string | undefined -// 获取专辑并排序 -const albums = await getAlbumsAsync({ - sortBy: ['title', true], // 按标题升序排序 - first: 50 +// 获取下一页 +const nextPage = await getTracksAsync({ + sortBy: ['title', true], + first: 20, + after: result.endCursor, }); +``` + +## 📖 API + +### `getTracksAsync(options?)` -// 获取艺术家 -const artists = await getArtistsAsync(); +返回音乐库中的分页曲目列表。 + +```ts +getTracksAsync(options?: TrackOptions): Promise> ``` -### Android 权限 +**TrackOptions** -在 `android/app/src/main/AndroidManifest.xml` 中添加: +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `first` | `number` | `20` | 最多返回的条数 | +| `after` | `string` | — | 上一页的 `endCursor` | +| `sortBy` | `TrackSortByKey \| [TrackSortByKey, boolean] \| (...)[]` | `'default'` | 排序字段,或 `[字段, 是否升序]` 元组 | +| `directory` | `string` | — | 按目录路径过滤(仅 Android) | -```xml - - +**TrackSortByKey**:`'default' \| 'title' \| 'artist' \| 'album' \| 'duration' \| 'createdAt' \| 'modifiedAt' \| 'fileSize'` + +--- + +### `getTrackMetadataAsync(trackId)` + +返回单首曲目的详细音频元数据。 + +```ts +getTrackMetadataAsync(trackId: string): Promise +``` + +--- + +### `getTracksByAlbumAsync(albumId)` + +返回专辑内的所有曲目(不分页)。 + +```ts +getTracksByAlbumAsync(albumId: string): Promise +``` + +--- + +### `getTracksByArtistAsync(artistId, options?)` + +返回某位艺术家的分页曲目列表。 + +```ts +getTracksByArtistAsync(artistId: string, options?: TrackOptions): Promise> +``` + +--- + +### `getAlbumsAsync(options?)` + +返回分页专辑列表。 + +```ts +getAlbumsAsync(options?: AlbumOptions): Promise> +``` + +**AlbumOptions** + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `first` | `number` | `20` | 最多返回的条数 | +| `after` | `string` | — | 上一页的 `endCursor` | +| `sortBy` | `AlbumSortByKey \| [AlbumSortByKey, boolean] \| (...)[]` | `'default'` | 排序字段,或 `[字段, 是否升序]` 元组 | + +**AlbumSortByKey**:`'default' \| 'title' \| 'artist' \| 'trackCount' \| 'year'` + +--- + +### `getAlbumsByArtistAsync(artistId)` + +返回某位艺术家的所有专辑(不分页)。 + +```ts +getAlbumsByArtistAsync(artistId: string): Promise +``` + +--- + +### `getArtistsAsync(options?)` + +返回分页艺术家列表。 + +```ts +getArtistsAsync(options?: ArtistOptions): Promise> +``` + +**ArtistOptions** + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `first` | `number` | `20` | 最多返回的条数 | +| `after` | `string` | — | 上一页的 `endCursor` | +| `sortBy` | `ArtistSortByKey \| [ArtistSortByKey, boolean] \| (...)[]` | `'default'` | 排序字段,或 `[字段, 是否升序]` 元组 | + +**ArtistSortByKey**:`'default' \| 'title' \| 'trackCount' \| 'albumCount'` + +--- + +## 📦 类型定义 + +### `Track` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | `string` | 唯一标识符 | +| `title` | `string` | 曲目标题 | +| `artist` | `string` | 艺术家名称 | +| `artwork` | `string?` | 封面图片 URI(可能为空) | +| `album` | `string` | 专辑名称 | +| `duration` | `number` | 时长(秒) | +| `url` | `string` | 文件 URI | +| `createdAt` | `number` | 添加时间(Unix 时间戳,秒) | +| `modifiedAt` | `number` | 修改时间(Unix 时间戳,秒) | +| `fileSize` | `number` | 文件大小(字节) | + +### `TrackMetadata` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | `string` | 曲目 ID | +| `duration` | `number` | 时长(秒) | +| `bitrate` | `number` | 比特率(kbps) | +| `sampleRate` | `number` | 采样率(Hz) | +| `channels` | `string` | 声道数 | +| `format` | `string` | 音频格式(如 `"AAC"`、`"MP3"`) | +| `title` | `string` | 标题标签 | +| `artist` | `string` | 艺术家标签 | +| `album` | `string` | 专辑标签 | +| `year` | `number` | 发行年份 | +| `genre` | `string` | 流派标签 | +| `track` | `number` | 音轨号 | +| `disc` | `number` | 碟号 | +| `composer` | `string` | 作曲家标签 | +| `lyricist` | `string` | 作词人标签 | +| `lyrics` | `string` | 内嵌歌词 | +| `albumArtist` | `string` | 专辑艺术家标签 | +| `comment` | `string` | 备注标签 | + +### `Album` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | `string` | 唯一标识符 | +| `title` | `string` | 专辑名称 | +| `artist` | `string` | 主要艺术家 | +| `artwork` | `string?` | 封面图片 URI(可能为空) | +| `trackCount` | `number` | 曲目数量 | +| `year` | `number?` | 发行年份 | + +### `Artist` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | `string` | 唯一标识符 | +| `title` | `string` | 艺术家名称 | +| `albumCount` | `number` | 专辑数量 | +| `trackCount` | `number` | 曲目总数 | + +### `PaginatedResult` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `items` | `T[]` | 结果数组 | +| `hasNextPage` | `boolean` | 是否还有更多数据 | +| `endCursor` | `string?` | 传入 `after` 以获取下一页 | +| `totalCount` | `number?` | 总数量(计算可能较慢) | + +--- + +## 🔄 分页示例 + +```ts +async function fetchAllTracks() { + const allTracks = []; + let cursor: string | undefined; + + do { + const result = await getTracksAsync({ first: 50, after: cursor }); + allTracks.push(...result.items); + cursor = result.hasNextPage ? result.endCursor : undefined; + } while (cursor); + + return allTracks; +} ``` ## 🤝 贡献 diff --git a/ios/MusicLibraryImpl.swift b/ios/MusicLibraryImpl.swift index e43d96b..897ff51 100644 --- a/ios/MusicLibraryImpl.swift +++ b/ios/MusicLibraryImpl.swift @@ -54,41 +54,25 @@ public class MusicLibraryImpl: NSObject { } @objc public func getTracksByArtistAsync(_ artistId: String, first: Int, after: String?, sortBy: [String], directory: String?) -> [String: Any] { - NSLog("🎵 [MusicLibrary] getTracksByArtistAsync called with artistId: %@, first: %d, after: %@, sortBy: %@, directory: %@", artistId, first, after ?? "nil", sortBy, directory ?? "nil") - - let result = PaginatedResult(items: [], hasNextPage: false) - let resultDict = result.toDictionary() - - NSLog("🎵 [MusicLibrary] getTracksByArtistAsync returning: %@", resultDict) - return resultDict + let options = TrackOptions(after: after, first: first, sortBy: sortBy, directory: directory) + let result = GetTracksByArtistQuery.getTracksByArtist(artistId: artistId, options: options) + return result.toDictionary() } @objc public func getAlbumsAsync(first: Int, after: String?, sortBy: [String]) -> [String: Any] { - NSLog("🎵 [MusicLibrary] getAlbumsAsync called with first: %d, after: %@, sortBy: %@", first, after ?? "nil", sortBy) - - let result = PaginatedResult(items: [], hasNextPage: false) - let resultDict = result.toDictionary() - - NSLog("🎵 [MusicLibrary] getAlbumsAsync returning: %@", resultDict) - return resultDict + let options = AlbumOptions(after: after, first: first, sortBy: sortBy) + let result = GetAlbumsQuery.getAlbums(options: options) + return result.toDictionary() } @objc public func getAlbumsByArtistAsync(_ artistId: String) -> [[String: Any]] { - NSLog("🎵 [MusicLibrary] getAlbumsByArtistAsync called with artistId: %@", artistId) - - let result: [[String: Any]] = [] - - NSLog("🎵 [MusicLibrary] getAlbumsByArtistAsync returning: %@", result) - return result + let albums = GetAlbumsByArtistQuery.getAlbumsByArtist(artistId: artistId) + return albums.map { $0.toDictionary() } } @objc public func getArtistsAsync(first: Int, after: String?, sortBy: [String]) -> [String: Any] { - NSLog("🎵 [MusicLibrary] getArtistsAsync called with first: %d, after: %@, sortBy: %@", first, after ?? "nil", sortBy) - - let result = PaginatedResult(items: [], hasNextPage: false) - let resultDict = result.toDictionary() - - NSLog("🎵 [MusicLibrary] getArtistsAsync returning: %@", resultDict) - return resultDict + let options = ArtistOptions(after: after, first: first, sortBy: sortBy) + let result = GetArtistsQuery.getArtists(options: options) + return result.toDictionary() } } diff --git a/ios/albums/GetAlbumsByArtistQuery.swift b/ios/albums/GetAlbumsByArtistQuery.swift new file mode 100644 index 0000000..c7145b1 --- /dev/null +++ b/ios/albums/GetAlbumsByArtistQuery.swift @@ -0,0 +1,26 @@ +import Foundation +import MediaPlayer + +internal class GetAlbumsByArtistQuery { + + static func getAlbumsByArtist(artistId: String) -> [Album] { + guard MPMediaLibrary.authorizationStatus() == .authorized else { + return [] + } + + let query = MPMediaQuery.albums() + let artistPredicate = MPMediaPropertyPredicate( + value: NSNumber(value: UInt64(artistId) ?? 0), + forProperty: MPMediaItemPropertyArtistPersistentID + ) + query.filterPredicates = Set([artistPredicate]) + + guard let collections = query.collections else { + return [] + } + + return collections + .compactMap { GetAlbumsQuery.buildAlbum($0) } + .sorted { $0.title < $1.title } + } +} diff --git a/ios/albums/GetAlbumsQuery.swift b/ios/albums/GetAlbumsQuery.swift new file mode 100644 index 0000000..02857e6 --- /dev/null +++ b/ios/albums/GetAlbumsQuery.swift @@ -0,0 +1,87 @@ +import Foundation +import MediaPlayer + +internal class GetAlbumsQuery { + + static func getAlbums(options: AlbumOptions) -> PaginatedResult { + guard MPMediaLibrary.authorizationStatus() == .authorized else { + return PaginatedResult(items: [], hasNextPage: false, totalCount: 0) + } + + let query = MPMediaQuery.albums() + guard let collections = query.collections else { + return PaginatedResult(items: [], hasNextPage: false, totalCount: 0) + } + + var albums = collections.compactMap { buildAlbum($0) } + + applySortBy(options.sortBy, to: &albums) + + let totalCount = albums.count + var startIndex = 0 + if let after = options.after { + if let idx = albums.firstIndex(where: { $0.id == after }) { + startIndex = idx + 1 + } + } + + let endIndex = min(startIndex + options.first, albums.count) + let page = startIndex < albums.count ? Array(albums[startIndex..( + items: page, + hasNextPage: hasNextPage, + endCursor: endCursor, + totalCount: totalCount + ) + } + + // MARK: - Helpers + + static func buildAlbum(_ collection: MPMediaItemCollection) -> Album? { + guard let representative = collection.representativeItem else { return nil } + let albumId = representative.albumPersistentID + let title = representative.albumTitle ?? "" + guard !title.isEmpty else { return nil } + + let artist = representative.albumArtist ?? representative.artist ?? "" + let trackCount = collection.count + let year = representative.releaseDate.map { Calendar.current.component(.year, from: $0) } + let artwork: String? = representative.artwork != nil ? "artwork://album/\(albumId)" : nil + + return Album( + id: "\(albumId)", + title: title, + artist: artist, + artwork: artwork, + trackCount: trackCount, + year: year + ) + } + + private static func applySortBy(_ sortBy: [String], to albums: inout [Album]) { + for sortString in sortBy { + let parts = sortString.components(separatedBy: " ") + guard parts.count == 2 else { continue } + let ascending = parts[1].uppercased() == "ASC" + switch parts[0].lowercased() { + case "default", "title": + albums.sort { ascending ? $0.title < $1.title : $0.title > $1.title } + case "artist": + albums.sort { ascending ? $0.artist < $1.artist : $0.artist > $1.artist } + case "trackcount": + albums.sort { ascending ? $0.trackCount < $1.trackCount : $0.trackCount > $1.trackCount } + case "year": + albums.sort { + let y0 = $0.year ?? 0 + let y1 = $1.year ?? 0 + return ascending ? y0 < y1 : y0 > y1 + } + default: break + } + } + } +} diff --git a/ios/artists/GetArtistsQuery.swift b/ios/artists/GetArtistsQuery.swift new file mode 100644 index 0000000..8508ed7 --- /dev/null +++ b/ios/artists/GetArtistsQuery.swift @@ -0,0 +1,85 @@ +import Foundation +import MediaPlayer + +internal class GetArtistsQuery { + + static func getArtists(options: ArtistOptions) -> PaginatedResult { + guard MPMediaLibrary.authorizationStatus() == .authorized else { + return PaginatedResult(items: [], hasNextPage: false, totalCount: 0) + } + + let artistQuery = MPMediaQuery.artists() + guard let artistCollections = artistQuery.collections else { + return PaginatedResult(items: [], hasNextPage: false, totalCount: 0) + } + + var artists = artistCollections.compactMap { buildArtist($0) } + + applySortBy(options.sortBy, to: &artists) + + let totalCount = artists.count + var startIndex = 0 + if let after = options.after { + if let idx = artists.firstIndex(where: { $0.id == after }) { + startIndex = idx + 1 + } + } + + let endIndex = min(startIndex + options.first, artists.count) + let page = startIndex < artists.count ? Array(artists[startIndex..( + items: page, + hasNextPage: hasNextPage, + endCursor: endCursor, + totalCount: totalCount + ) + } + + // MARK: - Helpers + + private static func buildArtist(_ collection: MPMediaItemCollection) -> Artist? { + guard let representative = collection.representativeItem else { return nil } + let artistName = representative.artist ?? "" + guard !artistName.isEmpty else { return nil } + + let artistId = representative.artistPersistentID + let trackCount = collection.count + + // Count albums for this artist + let albumQuery = MPMediaQuery.albums() + let artistPredicate = MPMediaPropertyPredicate( + value: NSNumber(value: artistId), + forProperty: MPMediaItemPropertyArtistPersistentID + ) + albumQuery.filterPredicates = Set([artistPredicate]) + let albumCount = albumQuery.collections?.count ?? 0 + + return Artist( + id: "\(artistId)", + title: artistName, + albumCount: albumCount, + trackCount: trackCount + ) + } + + private static func applySortBy(_ sortBy: [String], to artists: inout [Artist]) { + for sortString in sortBy { + let parts = sortString.components(separatedBy: " ") + guard parts.count == 2 else { continue } + let ascending = parts[1].uppercased() == "ASC" + switch parts[0].lowercased() { + case "default", "title": + artists.sort { ascending ? $0.title < $1.title : $0.title > $1.title } + case "trackcount": + artists.sort { ascending ? $0.trackCount < $1.trackCount : $0.trackCount > $1.trackCount } + case "albumcount": + artists.sort { ascending ? $0.albumCount < $1.albumCount : $0.albumCount > $1.albumCount } + default: break + } + } + } +} diff --git a/ios/tracks/GetTracks.swift b/ios/tracks/GetTracks.swift index 8d060bb..2988e1c 100644 --- a/ios/tracks/GetTracks.swift +++ b/ios/tracks/GetTracks.swift @@ -79,6 +79,18 @@ internal class GetTracks { let date2 = item2.lastPlayedDate ?? Date(timeIntervalSince1970: 0) return ascending ? date1 < date2 : date1 > date2 } + case "album": + items.sort { item1, item2 in + let album1 = item1.albumTitle ?? "" + let album2 = item2.albumTitle ?? "" + return ascending ? album1 < album2 : album1 > album2 + } + case "filesize": + items.sort { item1, item2 in + let size1 = getFileSize(for: item1) + let size2 = getFileSize(for: item2) + return ascending ? size1 < size2 : size1 > size2 + } default: NSLog("🎵 [MusicLibrary] Unknown sort key: %@", key) } diff --git a/ios/tracks/GetTracksByArtistQuery.swift b/ios/tracks/GetTracksByArtistQuery.swift new file mode 100644 index 0000000..16a67c1 --- /dev/null +++ b/ios/tracks/GetTracksByArtistQuery.swift @@ -0,0 +1,104 @@ +import Foundation +import MediaPlayer + +internal class GetTracksByArtistQuery { + + static func getTracksByArtist(artistId: String, options: TrackOptions) -> PaginatedResult { + guard MPMediaLibrary.authorizationStatus() == .authorized else { + return PaginatedResult(items: [], hasNextPage: false, totalCount: 0) + } + + let query = MPMediaQuery.songs() + let artistPredicate = MPMediaPropertyPredicate( + value: NSNumber(value: UInt64(artistId) ?? 0), + forProperty: MPMediaItemPropertyArtistPersistentID + ) + query.filterPredicates = Set([artistPredicate]) + + guard var items = query.items else { + return PaginatedResult(items: [], hasNextPage: false, totalCount: 0) + } + + applySortBy(options.sortBy, to: &items) + + let totalCount = items.count + var startIndex = 0 + if let after = options.after, let afterId = UInt64(after) { + if let idx = items.firstIndex(where: { $0.persistentID == afterId }) { + startIndex = idx + 1 + } + } + + let endIndex = min(startIndex + options.first, items.count) + let page = startIndex < items.count ? Array(items[startIndex..( + items: tracks, + hasNextPage: hasNextPage, + endCursor: endCursor, + totalCount: totalCount + ) + } + + // MARK: - Helpers + + private static func applySortBy(_ sortBy: [String], to items: inout [MPMediaItem]) { + for sortString in sortBy { + let parts = sortString.components(separatedBy: " ") + guard parts.count == 2 else { continue } + let ascending = parts[1].uppercased() == "ASC" + switch parts[0].lowercased() { + case "default", "title": + items.sort { ($0.title ?? "") < ($1.title ?? "") ? ascending : !ascending } + case "artist": + items.sort { ($0.artist ?? "") < ($1.artist ?? "") ? ascending : !ascending } + case "album": + items.sort { ($0.albumTitle ?? "") < ($1.albumTitle ?? "") ? ascending : !ascending } + case "duration": + items.sort { ascending ? $0.playbackDuration < $1.playbackDuration : $0.playbackDuration > $1.playbackDuration } + case "createdat": + items.sort { ascending ? $0.dateAdded < $1.dateAdded : $0.dateAdded > $1.dateAdded } + case "modifiedat": + items.sort { ascending ? $0.dateAdded < $1.dateAdded : $0.dateAdded > $1.dateAdded } + case "filesize": + items.sort { + let s0 = fileSize(for: $0) + let s1 = fileSize(for: $1) + return ascending ? s0 < s1 : s0 > s1 + } + default: break + } + } + } + + private static func buildTrack(_ item: MPMediaItem) -> Track { + var urlString = "" + if let assetURL = item.assetURL { + urlString = assetURL.absoluteString + } else { + urlString = "ipod-library://item/item.m4a?id=\(item.persistentID)" + } + + return Track( + id: "\(item.persistentID)", + title: item.title ?? "Unknown Title", + artist: item.artist, + artwork: item.artwork != nil ? "artwork://\(item.persistentID)" : nil, + album: item.albumTitle, + duration: item.playbackDuration, + url: urlString, + createdAt: item.dateAdded.timeIntervalSince1970, + modifiedAt: item.dateAdded.timeIntervalSince1970, + fileSize: fileSize(for: item) + ) + } + + private static func fileSize(for item: MPMediaItem) -> Int64 { + guard let url = item.assetURL else { return 0 } + return (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64) ?? 0 + } +} diff --git a/src/NativeMusicLibrary.ts b/src/NativeMusicLibrary.ts index 8292f62..09fccce 100644 --- a/src/NativeMusicLibrary.ts +++ b/src/NativeMusicLibrary.ts @@ -172,8 +172,8 @@ export interface Track { /** Artist name */ artist: string; - /** Track artwork (file URI) */ - artwork: string; + /** Track artwork (file URI, optional) */ + artwork?: string; /** Album name */ album: string; @@ -202,7 +202,7 @@ export interface TrackMetadata { duration: number; // in seconds bitrate: number; // in kbps sampleRate: number; // in Hz - channels: number; + channels: string; format: string; /** Tag info */ diff --git a/src/NativeMusicLibrary.web.ts b/src/NativeMusicLibrary.web.ts index aa03b8f..11aa553 100644 --- a/src/NativeMusicLibrary.web.ts +++ b/src/NativeMusicLibrary.web.ts @@ -36,7 +36,7 @@ const MusicLibrary: Spec = { duration: 0, bitrate: 0, sampleRate: 0, - channels: 0, + channels: '', format: '', title: '', artist: '', From dcbaef7b01f329c815e397c1ecb4a3d1abc1f22d Mon Sep 17 00:00:00 2001 From: ziven Date: Sun, 26 Apr 2026 20:02:39 +0800 Subject: [PATCH 3/3] docs: update docs --- docs/docs/api.md | 104 +++++++++--------- docs/docs/getting-started.md | 58 ++++++---- docs/docs/intro.md | 27 +++-- .../current/api.md | 104 +++++++++--------- .../current/getting-started.md | 26 ++++- .../current/intro.md | 19 +++- 6 files changed, 193 insertions(+), 145 deletions(-) diff --git a/docs/docs/api.md b/docs/docs/api.md index 613b401..571d0d0 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -37,7 +37,7 @@ const result = await getTracksAsync(); const tracks = await getTracksAsync({ first: 50, sortBy: ['artist', true], - directory: '/Music/Favorites' + directory: '/Music/Favorites', }); ``` @@ -69,7 +69,7 @@ const result = await getAlbumsAsync(); // Get albums with sorting const albums = await getAlbumsAsync({ first: 30, - sortBy: ['trackCount', false] // Sort by track count descending + sortBy: ['trackCount', false], // Sort by track count descending }); ``` @@ -101,7 +101,7 @@ const result = await getArtistsAsync(); // Get artists with sorting const artists = await getArtistsAsync({ first: 20, - sortBy: ['trackCount', false] // Sort by track count descending + sortBy: ['trackCount', false], // Sort by track count descending }); ``` @@ -167,7 +167,7 @@ import { getTracksByArtistAsync } from '@nodefinity/react-native-music-library'; const tracks = await getTracksByArtistAsync('artist-id-123', { first: 100, - sortBy: ['album', true] + sortBy: ['album', true], }); ``` @@ -197,10 +197,10 @@ const albums = await getAlbumsByArtistAsync('artist-id-123'); ```typescript interface TrackOptions { - after?: string; // Cursor for pagination - first?: number; // Max items to return (default: 20) + after?: string; // Cursor for pagination + first?: number; // Max items to return (default: 20) sortBy?: SortByValue | SortByValue[]; - directory?: string; // Directory path to search + directory?: string; // Directory path to search } ``` @@ -208,8 +208,8 @@ interface TrackOptions { ```typescript interface AlbumOptions { - after?: string; // Cursor for pagination - first?: number; // Max items to return (default: 20) + after?: string; // Cursor for pagination + first?: number; // Max items to return (default: 20) sortBy?: SortByValue | SortByValue[]; } ``` @@ -218,8 +218,8 @@ interface AlbumOptions { ```typescript interface ArtistOptions { - after?: string; // Cursor for pagination - first?: number; // Max items to return (default: 20) + after?: string; // Cursor for pagination + first?: number; // Max items to return (default: 20) sortBy?: SortByValue | SortByValue[]; } ``` @@ -229,15 +229,15 @@ interface ArtistOptions { ```typescript interface Track { id: string; - title: string; // Track title - artist: string; // Artist name - artwork: string; // Artwork file URI - album: string; // Album name - duration: number; // Duration in seconds - url: string; // File URL or path - createdAt: number; // Date added (Unix timestamp) - modifiedAt: number; // Date modified (Unix timestamp) - fileSize: number; // File size in bytes + title: string; // Track title + artist: string; // Artist name + artwork?: string; // Artwork file URI (may be undefined) + album: string; // Album name + duration: number; // Duration in seconds + url: string; // File URL or path + createdAt: number; // Date added (Unix timestamp) + modifiedAt: number; // Date modified (Unix timestamp) + fileSize: number; // File size in bytes } ``` @@ -246,11 +246,11 @@ interface Track { ```typescript interface Album { id: string; - title: string; // Album name - artist: string; // Primary artist - artwork?: string; // Album artwork URI - trackCount: number; // Number of tracks - year?: number; // Release year + title: string; // Album name + artist: string; // Primary artist + artwork?: string; // Album artwork URI + trackCount: number; // Number of tracks + year?: number; // Release year } ``` @@ -259,9 +259,9 @@ interface Album { ```typescript interface Artist { id: string; - title: string; // Artist name - albumCount: number; // Number of albums - trackCount: number; // Total number of tracks + title: string; // Artist name + albumCount: number; // Number of albums + trackCount: number; // Total number of tracks } ``` @@ -269,28 +269,28 @@ interface Artist { ```typescript interface TrackMetadata { - id: string; // Track ID + id: string; // Track ID // Audio header - duration: number; // Duration in seconds - bitrate: number; // Bitrate in kbps - sampleRate: number; // Sample rate in Hz - channels: number; // Number of channels - format: string; // Audio format + duration: number; // Duration in seconds + bitrate: number; // Bitrate in kbps + sampleRate: number; // Sample rate in Hz + channels: string; // Number of channels (e.g. "2") + format: string; // Audio format // Tag info - title: string; // Track title - artist: string; // Artist name - album: string; // Album name - year: number; // Release year - genre: string; // Music genre - track: number; // Track number - disc: number; // Disc number - composer: string; // Composer - lyricist: string; // Lyricist - lyrics: string; // Lyrics content - albumArtist: string; // Album artist - comment: string; // Comment + title: string; // Track title + artist: string; // Artist name + album: string; // Album name + year: number; // Release year + genre: string; // Music genre + track: number; // Track number + disc: number; // Disc number + composer: string; // Composer + lyricist: string; // Lyricist + lyrics: string; // Lyrics content + albumArtist: string; // Album artist + comment: string; // Comment } ``` @@ -326,16 +326,12 @@ interface TrackMetadata { ```js // Single sort key (descending by default) -sortBy: 'artist' +sortBy: 'artist'; // Single sort key with direction -sortBy: ['artist', true] // ascending -sortBy: ['artist', false] // descending +sortBy: ['artist', true]; // ascending +sortBy: ['artist', false]; // descending // Multiple sort criteria -sortBy: [ - ['artist', true], - ['album', true], - 'duration' -] +sortBy: [['artist', true], ['album', true], 'duration']; ``` diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 2678238..f53d32c 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -27,14 +27,14 @@ yarn add @nodefinity/react-native-music-library ``` -2. **Request runtime permissions** using a permissions library: +1. **Request runtime permissions** using a permissions library: ```js import { request, PERMISSIONS, RESULTS } from 'react-native-permissions'; const requestMusicPermission = async () => { const result = await request(PERMISSIONS.ANDROID.READ_MEDIA_AUDIO); - + if (result === RESULTS.GRANTED) { console.log('Music permission granted'); } else { @@ -43,25 +43,41 @@ const requestMusicPermission = async () => { }; ``` -### iOS (Coming Soon) +### iOS -iOS implementation is not yet available. For future compatibility, add to `Info.plist`: +1. **Add permission** to `Info.plist`: ```xml NSAppleMusicUsageDescription This app needs access to your music library to play songs ``` +1. **Request runtime permission** using a permissions library: + +```js +import { request, PERMISSIONS, RESULTS } from 'react-native-permissions'; + +const requestMusicPermission = async () => { + const result = await request(PERMISSIONS.IOS.MEDIA_LIBRARY); + + if (result === RESULTS.GRANTED) { + console.log('Music permission granted'); + } else { + console.log('Music permission denied'); + } +}; +``` + ## Basic Usage ### Import the library ```js -import { - getTracksAsync, - getAlbumsAsync, +import { + getTracksAsync, + getAlbumsAsync, getArtistsAsync, - getTrackMetadataAsync + getTrackMetadataAsync, } from '@nodefinity/react-native-music-library'; ``` @@ -71,10 +87,12 @@ import { const loadTracks = async () => { try { const result = await getTracksAsync(); - - result.items.forEach(track => { + + result.items.forEach((track) => { console.log(`${track.title} by ${track.artist}`); - console.log(`Duration: ${Math.floor(track.duration / 60)}:${track.duration % 60}`); + console.log( + `Duration: ${Math.floor(track.duration / 60)}:${track.duration % 60}` + ); console.log(`File: ${track.url}`); }); } catch (error) { @@ -90,10 +108,10 @@ const loadAlbums = async () => { try { const result = await getAlbumsAsync({ sortBy: ['title', true], // Sort by title ascending - first: 50 + first: 50, }); - - result.items.forEach(album => { + + result.items.forEach((album) => { console.log(`${album.title} by ${album.artist}`); console.log(`Tracks: ${album.trackCount}`); }); @@ -109,10 +127,10 @@ const loadAlbums = async () => { const loadArtists = async () => { try { const result = await getArtistsAsync({ - sortBy: 'title' // Sort by name + sortBy: 'title', // Sort by name }); - - result.items.forEach(artist => { + + result.items.forEach((artist) => { console.log(`${artist.title}`); console.log(`Albums: ${artist.albumCount}, Tracks: ${artist.trackCount}`); }); @@ -124,6 +142,6 @@ const loadArtists = async () => { ## Next Steps -- [API Reference](./api) - Learn about all available methods and options -- [Examples](./examples) - See practical usage examples -- [Type Definitions](./api#type-definitions) - Understand the data structures +- [API Reference](./api.md) - Learn about all available methods and options +- [Examples](./examples.md) - See practical usage examples +- [Type Definitions](./api.md#type-definitions) - Understand the data structures diff --git a/docs/docs/intro.md b/docs/docs/intro.md index a907b07..9a4209a 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -15,8 +15,8 @@ React Native Music Library is a powerful library for accessing local music files - 📁 **Directory Filtering** - Filter music by specific directories - 🔄 **TypeScript** - Full type definitions and type safety - 🎨 **Album Artwork** - Support for album artwork and cover images -- 🤖 **Android Support** - Full native Android implementation -- 📱 **iOS Support** - Coming soon +- 🤖 **Android** - Full native Android implementation +- 🍎 **iOS** - Full native iOS implementation via MediaPlayer framework ## 🚀 Quick Start @@ -31,7 +31,11 @@ yarn add @nodefinity/react-native-music-library ### Basic Usage ```js -import { getTracksAsync, getAlbumsAsync, getArtistsAsync } from '@nodefinity/react-native-music-library'; +import { + getTracksAsync, + getAlbumsAsync, + getArtistsAsync, +} from '@nodefinity/react-native-music-library'; // Get all tracks const tracks = await getTracksAsync(); @@ -39,7 +43,7 @@ const tracks = await getTracksAsync(); // Get albums with sorting const albums = await getAlbumsAsync({ sortBy: ['title', true], // Sort by title ascending - first: 50 + first: 50, }); // Get artists @@ -55,8 +59,17 @@ Add to `android/app/src/main/AndroidManifest.xml`: ``` +### iOS Permissions + +Add to `Info.plist`: + +```xml +NSAppleMusicUsageDescription +This app needs access to your music library. +``` + ## 📖 What's Next? -- [Getting Started](./getting-started) - Learn how to set up and use the library -- [API Reference](./api) - Complete API documentation -- [Examples](./examples) - Practical usage examples +- [Getting Started](./getting-started.md) - Learn how to set up and use the library +- [API Reference](./api.md) - Complete API documentation +- [Examples](./examples.md) - Practical usage examples diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/api.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/api.md index 9d12f99..542d2d0 100644 --- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/api.md +++ b/docs/i18n/zh/docusaurus-plugin-content-docs/current/api.md @@ -37,7 +37,7 @@ const result = await getTracksAsync(); const tracks = await getTracksAsync({ first: 50, sortBy: ['artist', true], - directory: '/Music/Favorites' + directory: '/Music/Favorites', }); ``` @@ -69,7 +69,7 @@ const result = await getAlbumsAsync(); // 获取专辑并排序 const albums = await getAlbumsAsync({ first: 30, - sortBy: ['trackCount', false] // 按曲目数降序排序 + sortBy: ['trackCount', false], // 按曲目数降序排序 }); ``` @@ -101,7 +101,7 @@ const result = await getArtistsAsync(); // 获取艺术家并排序 const artists = await getArtistsAsync({ first: 20, - sortBy: ['trackCount', false] // 按曲目数降序排序 + sortBy: ['trackCount', false], // 按曲目数降序排序 }); ``` @@ -167,7 +167,7 @@ import { getTracksByArtistAsync } from '@nodefinity/react-native-music-library'; const tracks = await getTracksByArtistAsync('artist-id-123', { first: 100, - sortBy: ['album', true] + sortBy: ['album', true], }); ``` @@ -197,10 +197,10 @@ const albums = await getAlbumsByArtistAsync('artist-id-123'); ```typescript interface TrackOptions { - after?: string; // 分页游标 - first?: number; // 最大返回项目数(默认:20) + after?: string; // 分页游标 + first?: number; // 最大返回项目数(默认:20) sortBy?: SortByValue | SortByValue[]; - directory?: string; // 搜索目录路径 + directory?: string; // 搜索目录路径 } ``` @@ -208,8 +208,8 @@ interface TrackOptions { ```typescript interface AlbumOptions { - after?: string; // 分页游标 - first?: number; // 最大返回项目数(默认:20) + after?: string; // 分页游标 + first?: number; // 最大返回项目数(默认:20) sortBy?: SortByValue | SortByValue[]; } ``` @@ -218,8 +218,8 @@ interface AlbumOptions { ```typescript interface ArtistOptions { - after?: string; // 分页游标 - first?: number; // 最大返回项目数(默认:20) + after?: string; // 分页游标 + first?: number; // 最大返回项目数(默认:20) sortBy?: SortByValue | SortByValue[]; } ``` @@ -229,15 +229,15 @@ interface ArtistOptions { ```typescript interface Track { id: string; - title: string; // 曲目标题 - artist: string; // 艺术家名称 - artwork: string; // 封面文件 URI - album: string; // 专辑名称 - duration: number; // 时长(秒) - url: string; // 文件 URL 或路径 - createdAt: number; // 添加日期(Unix 时间戳) - modifiedAt: number; // 修改日期(Unix 时间戳) - fileSize: number; // 文件大小(字节) + title: string; // 曲目标题 + artist: string; // 艺术家名称 + artwork?: string; // 封面文件 URI(可能为空) + album: string; // 专辑名称 + duration: number; // 时长(秒) + url: string; // 文件 URL 或路径 + createdAt: number; // 添加日期(Unix 时间戳) + modifiedAt: number; // 修改日期(Unix 时间戳) + fileSize: number; // 文件大小(字节) } ``` @@ -246,11 +246,11 @@ interface Track { ```typescript interface Album { id: string; - title: string; // 专辑名称 - artist: string; // 主要艺术家 - artwork?: string; // 专辑封面 URI - trackCount: number; // 曲目数量 - year?: number; // 发行年份 + title: string; // 专辑名称 + artist: string; // 主要艺术家 + artwork?: string; // 专辑封面 URI + trackCount: number; // 曲目数量 + year?: number; // 发行年份 } ``` @@ -259,9 +259,9 @@ interface Album { ```typescript interface Artist { id: string; - title: string; // 艺术家名称 - albumCount: number; // 专辑数量 - trackCount: number; // 总曲目数 + title: string; // 艺术家名称 + albumCount: number; // 专辑数量 + trackCount: number; // 总曲目数 } ``` @@ -269,28 +269,28 @@ interface Artist { ```typescript interface TrackMetadata { - id: string; // 曲目 ID + id: string; // 曲目 ID // 音频头信息 - duration: number; // 时长(秒) - bitrate: number; // 比特率(kbps) - sampleRate: number; // 采样率(Hz) - channels: number; // 声道数 - format: string; // 音频格式 + duration: number; // 时长(秒) + bitrate: number; // 比特率(kbps) + sampleRate: number; // 采样率(Hz) + channels: string; // 声道数(如 "2") + format: string; // 音频格式 // 标签信息 - title: string; // 曲目标题 - artist: string; // 艺术家名称 - album: string; // 专辑名称 - year: number; // 发行年份 - genre: string; // 音乐流派 - track: number; // 曲目编号 - disc: number; // 碟片编号 - composer: string; // 作曲家 - lyricist: string; // 作词家 - lyrics: string; // 歌词内容 - albumArtist: string; // 专辑艺术家 - comment: string; // 注释 + title: string; // 曲目标题 + artist: string; // 艺术家名称 + album: string; // 专辑名称 + year: number; // 发行年份 + genre: string; // 音乐流派 + track: number; // 曲目编号 + disc: number; // 碟片编号 + composer: string; // 作曲家 + lyricist: string; // 作词家 + lyrics: string; // 歌词内容 + albumArtist: string; // 专辑艺术家 + comment: string; // 注释 } ``` @@ -326,16 +326,12 @@ interface TrackMetadata { ```js // 单个排序键(默认降序) -sortBy: 'artist' +sortBy: 'artist'; // 单个排序键带方向 -sortBy: ['artist', true] // 升序 -sortBy: ['artist', false] // 降序 +sortBy: ['artist', true]; // 升序 +sortBy: ['artist', false]; // 降序 // 多个排序条件 -sortBy: [ - ['artist', true], - ['album', true], - 'duration' -] +sortBy: [['artist', true], ['album', true], 'duration']; ``` diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/getting-started.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/getting-started.md index 61a02c5..8997e15 100644 --- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/getting-started.md +++ b/docs/i18n/zh/docusaurus-plugin-content-docs/current/getting-started.md @@ -43,15 +43,31 @@ const requestMusicPermission = async () => { }; ``` -### iOS(即将推出) +### iOS -iOS 实现尚未可用。为了未来兼容性,在 `Info.plist` 中添加: +1. **添加权限**到 `Info.plist`: ```xml NSAppleMusicUsageDescription 此应用需要访问您的音乐库来播放歌曲 ``` +2. **请求运行时权限**使用权限库: + +```js +import { request, PERMISSIONS, RESULTS } from 'react-native-permissions'; + +const requestMusicPermission = async () => { + const result = await request(PERMISSIONS.IOS.MEDIA_LIBRARY); + + if (result === RESULTS.GRANTED) { + console.log('音乐权限已授予'); + } else { + console.log('音乐权限被拒绝'); + } +}; +``` + ## 基本用法 ### 导入库 @@ -124,6 +140,6 @@ const loadArtists = async () => { ## 下一步 -- [API 参考](./api) - 了解所有可用的方法和选项 -- [示例](./examples) - 查看实际使用示例 -- [类型定义](./api#type-definitions) - 理解数据结构 +- [API 参考](./api.md) - 了解所有可用的方法和选项 +- [示例](./examples.md) - 查看实际使用示例 +- [类型定义](./api.md#type-definitions) - 理解数据结构 diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/intro.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/intro.md index b09b635..3aabb2a 100644 --- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/intro.md +++ b/docs/i18n/zh/docusaurus-plugin-content-docs/current/intro.md @@ -15,8 +15,8 @@ React Native Music Library 是一个功能强大的库,用于访问本地音 - 📁 **目录过滤** - 按特定目录过滤音乐 - 🔄 **TypeScript** - 完整的类型定义和类型安全 - 🎨 **专辑封面** - 支持专辑封面和封面图片 -- 🤖 **Android 支持** - 完整的原生 Android 实现 -- 📱 **iOS 支持** - 即将推出 +- 🤖 **Android** - 完整的原生 Android 实现 +- 🍎 **iOS** - 基于 MediaPlayer 框架的完整原生 iOS 实现 ## 🚀 快速开始 @@ -67,8 +67,17 @@ const artists = await getArtistsAsync(); ``` +### iOS 权限 + +在 `Info.plist` 中添加: + +```xml +NSAppleMusicUsageDescription +此应用需要访问您的音乐库。 +``` + ## 📖 下一步 -- [开始使用](./getting-started) - 学习如何设置和使用该库 -- [API 参考](./api) - 完整的 API 文档 -- [示例](./examples) - 实际使用示例 +- [开始使用](./getting-started.md) - 学习如何设置和使用该库 +- [API 参考](./api.md) - 完整的 API 文档 +- [示例](./examples.md) - 实际使用示例