tangled
alpha
login
or
join now
t1c.dev
/
rocksky
forked from
rocksky.app/rocksky
2
fork
atom
A decentralized music tracking and discovery platform built on AT Protocol 🎵
2
fork
atom
overview
issues
pulls
pipelines
[app] implement search
tsiry-sandratraina.com
9 months ago
e3201575
3e1689d6
+362
-85
10 changed files
expand all
collapse all
unified
split
rocksky-app
bun.lock
package.json
src
api
search.ts
hooks
useSearch.tsx
screens
Search
Search.stories.tsx
Search.tsx
SearchInput
SearchInput.tsx
SearchWithData.tsx
index.tsx
types
search.ts
+8
rocksky-app/bun.lock
···
6
6
"dependencies": {
7
7
"@atproto/api": "^0.15.6",
8
8
"@expo/vector-icons": "^14.1.0",
9
9
+
"@hookform/resolvers": "^5.0.1",
9
10
"@ipld/dag-cbor": "^9.2.2",
10
11
"@react-native-async-storage/async-storage": "^2.1.2",
11
12
"@react-native-menu/menu": "^1.2.3",
···
41
42
"react": "19.0.0",
42
43
"react-content-loader": "^7.0.2",
43
44
"react-dom": "19.0.0",
45
45
+
"react-hook-form": "^7.56.4",
44
46
"react-i18next": "^15.5.1",
45
47
"react-native": "0.79.2",
46
48
"react-native-gesture-handler": "~2.24.0",
···
410
412
411
413
"@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="],
412
414
415
415
+
"@hookform/resolvers": ["@hookform/resolvers@5.0.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA=="],
416
416
+
413
417
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
414
418
415
419
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
···
623
627
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
624
628
625
629
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
630
630
+
631
631
+
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
626
632
627
633
"@storybook/addon-actions": ["@storybook/addon-actions@8.6.12", "", { "dependencies": { "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", "dequal": "^2.0.2", "polished": "^4.2.2", "uuid": "^9.0.0" }, "peerDependencies": { "storybook": "^8.6.12" } }, "sha512-B5kfiRvi35oJ0NIo53CGH66H471A3XTzrfaa6SxXEJsgxxSeKScG5YeXcCvLiZfvANRQ7QDsmzPUgg0o3hdMXw=="],
628
634
···
1973
1979
"react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
1974
1980
1975
1981
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
1982
1982
+
1983
1983
+
"react-hook-form": ["react-hook-form@7.56.4", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw=="],
1976
1984
1977
1985
"react-i18next": ["react-i18next@15.5.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA=="],
1978
1986
+2
rocksky-app/package.json
···
21
21
"dependencies": {
22
22
"@atproto/api": "^0.15.6",
23
23
"@expo/vector-icons": "^14.1.0",
24
24
+
"@hookform/resolvers": "^5.0.1",
24
25
"@ipld/dag-cbor": "^9.2.2",
25
26
"@react-native-async-storage/async-storage": "^2.1.2",
26
27
"@react-native-menu/menu": "^1.2.3",
···
56
57
"react": "19.0.0",
57
58
"react-content-loader": "^7.0.2",
58
59
"react-dom": "19.0.0",
60
60
+
"react-hook-form": "^7.56.4",
59
61
"react-i18next": "^15.5.1",
60
62
"react-native": "0.79.2",
61
63
"react-native-gesture-handler": "~2.24.0",
+10
rocksky-app/src/api/search.ts
···
1
1
+
import axios from "axios";
2
2
+
import { API_URL } from "../consts";
3
3
+
import { SearchResponse } from "../types/search";
4
4
+
5
5
+
export const search = async (query: string) => {
6
6
+
const response = await axios.get<SearchResponse>(
7
7
+
`${API_URL}/search?q=${query}&size=100`
8
8
+
);
9
9
+
return response.data;
10
10
+
};
+8
rocksky-app/src/hooks/useSearch.tsx
···
1
1
+
import { useMutation } from "@tanstack/react-query";
2
2
+
import { search } from "../api/search";
3
3
+
import { SearchResponse } from "../types/search";
4
4
+
5
5
+
export const useSearchMutation = () =>
6
6
+
useMutation<SearchResponse, Error, string>({
7
7
+
mutationFn: (query: string) => search(query),
8
8
+
});
+8
-1
rocksky-app/src/screens/Search/Search.stories.tsx
···
21
21
type Story = StoryObj<typeof meta>;
22
22
23
23
export const Basic: Story = {
24
24
-
args: {},
24
24
+
args: {
25
25
+
onSubmit: (query: string) => {},
26
26
+
onPressAlbum: (uri: string) => {},
27
27
+
onPressArtist: (uri: string) => {},
28
28
+
onPressTrack: (uri: string) => {},
29
29
+
onPressUser: (handle: string) => {},
30
30
+
results: [],
31
31
+
},
25
32
};
+146
-65
rocksky-app/src/screens/Search/Search.tsx
···
1
1
+
import Album from "@/src/components/Album";
2
2
+
import Artist from "@/src/components/Artist";
3
3
+
import ScrollToTopButton from "@/src/components/ScrollToTopButton";
1
4
import Song from "@/src/components/Song";
2
5
import StickyPlayer from "@/src/components/StickyPlayer";
3
3
-
import { ScrollView, View } from "react-native";
6
6
+
import useScrollToTop from "@/src/hooks/useScrollToTop";
7
7
+
import { useNowPlayingContext } from "@/src/providers/NowPlayingProvider";
8
8
+
import { FC } from "react";
9
9
+
import {
10
10
+
ActivityIndicator,
11
11
+
Keyboard,
12
12
+
KeyboardAvoidingView,
13
13
+
Platform,
14
14
+
ScrollView,
15
15
+
TouchableWithoutFeedback,
16
16
+
View,
17
17
+
} from "react-native";
4
18
import SearchInput from "./SearchInput";
5
19
6
6
-
const tracks = [
7
7
-
{
8
8
-
title: "Work from Home (feat. Ty Dolla $ign)",
9
9
-
artist: "Fith Harmony",
10
10
-
image:
11
11
-
"https://cdn.rocksky.app/covers/cbed73745681d6a170b694ee11bb527c.jpg",
12
12
-
},
13
13
-
{
14
14
-
title: "BED",
15
15
-
artist: "Joel Corry",
16
16
-
image: "https://i.scdn.co/image/ab67616d0000b273b06c09b9f72389ee7f1cbd6b",
17
17
-
},
18
18
-
{
19
19
-
title: "Friday (feat. Mufasa & Hypeman) - Dopamine Re-Edit",
20
20
-
artist: "Riton",
21
21
-
image:
22
22
-
"https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreihvqfivhiaxj4cxkwfptzdxwnrwkqsasuirxi5ub3mgyxzgi4yc7i@jpeg",
23
23
-
},
24
24
-
{
25
25
-
title: "Hear Me Say",
26
26
-
artist: "Jonas Blue",
27
27
-
image:
28
28
-
"https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreigjwz3opdzwdeispeckym2gpiy4kixp4skz2gdelckujkbnkz3edm@jpeg",
29
29
-
},
30
30
-
{
31
31
-
title: "Flowers (feat. Jaykae and MALIKA)",
32
32
-
artist: "Nathan Dawe",
33
33
-
image:
34
34
-
"https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreigqdrg5pmv7ji7f72ricgqjyneno5j5qzeppsqmoy3yek3wqv3fca@jpeg",
35
35
-
},
36
36
-
{
37
37
-
title: "Sorry",
38
38
-
artist: "Joel Corry",
39
39
-
image:
40
40
-
"https://cdn.rocksky.app/covers/ec9bbc208b04182f315f8137cfb2125b.jpg",
41
41
-
},
42
42
-
{
43
43
-
title: "3 Libras",
44
44
-
artist: "A Perfect Circle",
45
45
-
image: "https://i.scdn.co/image/ab67616d0000b2732d73b494efcb99356f8c7b28",
46
46
-
},
47
47
-
];
20
20
+
export type SearchProps = {
21
21
+
onSubmit: (query: string) => void;
22
22
+
results: {
23
23
+
record: any;
24
24
+
table: string;
25
25
+
}[];
26
26
+
isLoading?: boolean;
27
27
+
onPressAlbum: (uri: string) => void;
28
28
+
onPressArtist: (uri: string) => void;
29
29
+
onPressTrack: (uri: string) => void;
30
30
+
onPressUser: (handle: string) => void;
31
31
+
};
32
32
+
33
33
+
const Search: FC<SearchProps> = (props) => {
34
34
+
const {
35
35
+
results,
36
36
+
onSubmit,
37
37
+
isLoading,
38
38
+
onPressAlbum,
39
39
+
onPressArtist,
40
40
+
onPressTrack,
41
41
+
onPressUser,
42
42
+
} = props;
43
43
+
const { scrollToTop, isVisible, fadeAnim, handleScroll, scrollViewRef } =
44
44
+
useScrollToTop();
45
45
+
const nowPlaying = useNowPlayingContext();
46
46
+
const bottomButtonPosition = nowPlaying ? 80 : 20;
48
47
49
49
-
const Search = () => {
50
48
return (
51
51
-
<View className="w-full h-full bg-black">
52
52
-
<SearchInput className="mt-[50px] ml-[15px] mr-[15px]" />
53
53
-
<ScrollView className="h-[99%] w-full mt-[10px] pl-[15px] pr-[15px]">
54
54
-
{tracks.map((song, index) => (
55
55
-
<Song
56
56
-
key={index}
57
57
-
image={song.image}
58
58
-
title={song.title}
59
59
-
artist={song.artist}
60
60
-
size={60}
61
61
-
className="mt-[10px]"
62
62
-
onPress={() => {}}
63
63
-
onPressAlbum={() => {}}
64
64
-
did=""
65
65
-
albumUri=""
49
49
+
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
50
50
+
<KeyboardAvoidingView
51
51
+
className="flex-1"
52
52
+
behavior={Platform.OS === "ios" ? "padding" : undefined}
53
53
+
>
54
54
+
<View className="w-full h-full bg-black">
55
55
+
<SearchInput
56
56
+
className="mt-[50px] ml-[15px] mr-[15px]"
57
57
+
onSubmit={onSubmit}
66
58
/>
67
67
-
))}
68
68
-
</ScrollView>
69
69
-
<View className="w-full absolute bottom-0 bg-black">
70
70
-
<StickyPlayer />
71
71
-
</View>
72
72
-
</View>
59
59
+
{isLoading && (
60
60
+
<View className="w-fullitems-center">
61
61
+
<ActivityIndicator
62
62
+
size="large"
63
63
+
color="#bdbaba"
64
64
+
className="mt-[50px]"
65
65
+
/>
66
66
+
</View>
67
67
+
)}
68
68
+
{!isLoading && (
69
69
+
<ScrollView
70
70
+
ref={scrollViewRef}
71
71
+
onScroll={handleScroll}
72
72
+
className="w-full mt-[10px] pl-[15px] pr-[15px]"
73
73
+
showsVerticalScrollIndicator={false}
74
74
+
removeClippedSubviews={true}
75
75
+
keyboardShouldPersistTaps="handled"
76
76
+
>
77
77
+
{results.map(({ record, table }, index) => {
78
78
+
switch (table) {
79
79
+
case "tracks":
80
80
+
return (
81
81
+
<Song
82
82
+
key={index}
83
83
+
image={record.album_art}
84
84
+
title={record.title}
85
85
+
artist={record.artist}
86
86
+
size={60}
87
87
+
className="mt-[10px]"
88
88
+
onPress={() => onPressTrack(record.uri)}
89
89
+
onPressAlbum={() => onPressTrack(record.uri)}
90
90
+
did=""
91
91
+
albumUri={record.album_uri}
92
92
+
/>
93
93
+
);
94
94
+
case "albums":
95
95
+
return (
96
96
+
<Album
97
97
+
key={index}
98
98
+
image={record.album_art}
99
99
+
title={record.title}
100
100
+
artist={record.artist}
101
101
+
size={60}
102
102
+
className="mt-[10px]"
103
103
+
onPress={() => onPressAlbum(record.uri)}
104
104
+
did=""
105
105
+
row
106
106
+
/>
107
107
+
);
108
108
+
case "artists":
109
109
+
return (
110
110
+
<Artist
111
111
+
key={index}
112
112
+
image={record.picture}
113
113
+
name={record.name}
114
114
+
size={60}
115
115
+
className="mt-[10px]"
116
116
+
onPress={() => onPressArtist(record.uri)}
117
117
+
did=""
118
118
+
row
119
119
+
/>
120
120
+
);
121
121
+
case "users":
122
122
+
return (
123
123
+
<Artist
124
124
+
key={index}
125
125
+
image={record.avatar}
126
126
+
name={record.display_name}
127
127
+
size={60}
128
128
+
className="mt-[10px]"
129
129
+
onPress={() => onPressUser(record.handle)}
130
130
+
did={record.did}
131
131
+
row
132
132
+
/>
133
133
+
);
134
134
+
default:
135
135
+
break;
136
136
+
}
137
137
+
})}
138
138
+
</ScrollView>
139
139
+
)}
140
140
+
{isVisible && (
141
141
+
<ScrollToTopButton
142
142
+
fadeAnim={fadeAnim}
143
143
+
bottom={bottomButtonPosition}
144
144
+
onPress={scrollToTop}
145
145
+
/>
146
146
+
)}
147
147
+
148
148
+
<View className="w-full absolute bottom-0 bg-black">
149
149
+
<StickyPlayer />
150
150
+
</View>
151
151
+
</View>
152
152
+
</KeyboardAvoidingView>
153
153
+
</TouchableWithoutFeedback>
73
154
);
74
155
};
75
156
+49
-18
rocksky-app/src/screens/Search/SearchInput/SearchInput.tsx
···
1
1
import Feather from "@expo/vector-icons/Feather";
2
2
import Ionicons from "@expo/vector-icons/Ionicons";
3
3
-
import { FC } from "react";
3
3
+
import { zodResolver } from "@hookform/resolvers/zod";
4
4
+
import { FC, useEffect } from "react";
5
5
+
import { useForm } from "react-hook-form";
4
6
import { TextInput, View } from "react-native";
7
7
+
import { z } from "zod";
5
8
6
9
export type SearchInputProps = {
7
10
className?: string;
11
11
+
onSubmit?: (value: string) => void;
8
12
};
9
13
14
14
+
const schema = z.object({
15
15
+
search: z.string().min(1, "Please enter a search term"),
16
16
+
});
17
17
+
18
18
+
type FormData = z.infer<typeof schema>;
19
19
+
10
20
const SearchInput: FC<SearchInputProps> = (props) => {
11
11
-
const { className } = props;
21
21
+
const { className, onSubmit } = props;
22
22
+
const { register, setValue, handleSubmit, watch } = useForm<FormData>({
23
23
+
resolver: zodResolver(schema),
24
24
+
defaultValues: { search: "" },
25
25
+
});
26
26
+
27
27
+
// Register input on mount
28
28
+
useEffect(() => {
29
29
+
register("search");
30
30
+
}, [register]);
31
31
+
12
32
return (
13
13
-
<>
14
14
-
<View className={`relative ${className}`}>
15
15
-
<Feather
16
16
-
className="absolute z-10 top-[23px] left-[10px]"
17
17
-
name="search"
18
18
-
size={24}
19
19
-
color="#313131"
20
20
-
/>
21
21
-
<TextInput
22
22
-
className="font-rockford-medium bg-[#fff] text-[#000] rounded-[2px] p-[10px] mt-[10px] h-[49px] text-[18px] pl-[46px] pr-[46px]"
23
23
-
placeholder="Search"
24
24
-
placeholderTextColor="#A0A0A0"
25
25
-
cursorColor="#000"
26
26
-
/>
33
33
+
<View className={`relative ${className}`}>
34
34
+
<Feather
35
35
+
className="absolute z-10 top-[23px] left-[10px]"
36
36
+
name="search"
37
37
+
size={24}
38
38
+
color="#313131"
39
39
+
/>
40
40
+
<TextInput
41
41
+
className="font-rockford-medium bg-[#fff] text-[#000] rounded-[2px] p-[10px] mt-[10px] h-[49px] text-[18px] pl-[46px] pr-[46px]"
42
42
+
placeholder="Search"
43
43
+
placeholderTextColor="#A0A0A0"
44
44
+
cursorColor="#000"
45
45
+
onChangeText={(text) => {
46
46
+
setValue("search", text, { shouldValidate: true });
47
47
+
handleSubmit((data) => onSubmit?.(data.search))();
48
48
+
}}
49
49
+
onSubmitEditing={handleSubmit((data) => onSubmit?.(data.search.trim()))}
50
50
+
value={watch("search")}
51
51
+
returnKeyType="search"
52
52
+
/>
53
53
+
{watch("search")?.length > 0 && (
27
54
<Ionicons
28
55
className="absolute z-10 top-[23px] right-[10px]"
29
56
name="close"
30
57
size={24}
31
58
color="#313131"
59
59
+
onPress={() => {
60
60
+
setValue("search", "");
61
61
+
onSubmit?.("");
62
62
+
}}
32
63
/>
33
33
-
</View>
34
34
-
</>
64
64
+
)}
65
65
+
</View>
35
66
);
36
67
};
37
68
+63
rocksky-app/src/screens/Search/SearchWithData.tsx
···
1
1
+
import { useSearchMutation } from "@/src/hooks/useSearch";
2
2
+
import { RootStackParamList } from "@/src/Navigation";
3
3
+
import { useNavigation } from "@react-navigation/native";
4
4
+
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
5
5
+
import _ from "lodash";
6
6
+
import * as R from "ramda";
7
7
+
import { useEffect, useState } from "react";
8
8
+
import Search from "./Search";
9
9
+
10
10
+
const SearchWithData = () => {
11
11
+
const navigation =
12
12
+
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
13
13
+
const [results, setResults] = useState<
14
14
+
{
15
15
+
record: any;
16
16
+
table: string;
17
17
+
}[]
18
18
+
>([]);
19
19
+
const { mutate, data, isPending } = useSearchMutation();
20
20
+
21
21
+
const handleSearch = (query: string) => {
22
22
+
if (query.length < 3) {
23
23
+
setResults([]);
24
24
+
return;
25
25
+
}
26
26
+
27
27
+
if (query.length < 3) {
28
28
+
return;
29
29
+
}
30
30
+
31
31
+
_.debounce(() => {
32
32
+
mutate(query);
33
33
+
}, 500)();
34
34
+
};
35
35
+
36
36
+
useEffect(() => {
37
37
+
if (!data) return;
38
38
+
39
39
+
setResults(
40
40
+
R.pipe(
41
41
+
R.prop("records"),
42
42
+
R.sort(R.descend((item) => R.path(["record", "xata_score"], item) ?? 0))
43
43
+
)(data) as {
44
44
+
record: any;
45
45
+
table: string;
46
46
+
}[]
47
47
+
);
48
48
+
}, [data]);
49
49
+
50
50
+
return (
51
51
+
<Search
52
52
+
onSubmit={handleSearch}
53
53
+
results={results}
54
54
+
isLoading={isPending}
55
55
+
onPressAlbum={(uri) => navigation.navigate("AlbumDetails", { uri })}
56
56
+
onPressArtist={(uri) => navigation.navigate("ArtistDetails", { uri })}
57
57
+
onPressTrack={(uri) => navigation.navigate("SongDetails", { uri })}
58
58
+
onPressUser={(handle) => navigation.navigate("UserProfile", { handle })}
59
59
+
/>
60
60
+
);
61
61
+
};
62
62
+
63
63
+
export default SearchWithData;
+1
-1
rocksky-app/src/screens/Search/index.tsx
···
1
1
-
import Search from "./Search";
1
1
+
import Search from "./SearchWithData";
2
2
3
3
export default Search;
+67
rocksky-app/src/types/search.ts
···
1
1
+
export interface XataHighlight {
2
2
+
name?: string[];
3
3
+
title?: string[];
4
4
+
}
5
5
+
6
6
+
export interface BaseRecord {
7
7
+
sha256: string;
8
8
+
uri: string;
9
9
+
xata_createdat: string;
10
10
+
xata_highlight: XataHighlight;
11
11
+
xata_id: string;
12
12
+
xata_score: number;
13
13
+
xata_table: string;
14
14
+
xata_updatedat: string;
15
15
+
xata_version: number;
16
16
+
apple_music_link: string | null;
17
17
+
spotify_link: string | null;
18
18
+
tidal_link: string | null;
19
19
+
youtube_link: string | null;
20
20
+
}
21
21
+
22
22
+
export interface ArtistRecord extends BaseRecord {
23
23
+
name: string;
24
24
+
picture: string;
25
25
+
biography: string | null;
26
26
+
born: string | null;
27
27
+
born_in: string | null;
28
28
+
died: string | null;
29
29
+
}
30
30
+
31
31
+
export interface TrackRecord extends BaseRecord {
32
32
+
album: string;
33
33
+
album_art: string;
34
34
+
album_artist: string;
35
35
+
album_uri: string;
36
36
+
artist: string;
37
37
+
artist_uri: string;
38
38
+
disc_number: number;
39
39
+
duration: number;
40
40
+
title: string;
41
41
+
track_number: number;
42
42
+
composer: string | null;
43
43
+
copyright_message: string | null;
44
44
+
genre: string | null;
45
45
+
label: string | null;
46
46
+
lyrics: string | null;
47
47
+
mb_id: string | null;
48
48
+
}
49
49
+
50
50
+
export interface AlbumRecord extends BaseRecord {
51
51
+
title: string;
52
52
+
album_art: string;
53
53
+
artist: string;
54
54
+
artist_uri: string;
55
55
+
release_date: string;
56
56
+
year: number;
57
57
+
}
58
58
+
59
59
+
export type RecordItem =
60
60
+
| { table: "artists"; record: ArtistRecord }
61
61
+
| { table: "tracks"; record: TrackRecord }
62
62
+
| { table: "albums"; record: AlbumRecord };
63
63
+
64
64
+
export interface SearchResponse {
65
65
+
totalCount: number;
66
66
+
records: RecordItem[];
67
67
+
}