+7
-6
README.md
+7
-6
README.md
···
26
26
27
27
Open http://localhost:3000/ with your browser to see the home page. You will need to login with Bluesky to test the posting functionality of the app. Note: if the redirect back to the app after you login isn't working correctly, you may need to replace the `127.0.0.1` with `localhost`.
28
28
29
-
30
29
### Running the full stack in docker for development
30
+
31
31
_Still a work in progress and may have some hiccups_
32
32
33
-
#### 1. Set up publicly accessible endpoints.
33
+
#### 1. Set up publicly accessible endpoints.
34
+
34
35
It is recommended if you are running aqua to make a publicly accessible endpoint for the app to post to. You can do that a couple of ways:
35
36
36
-
* Set up the traditional port forward on your router
37
-
* Use a tool like [ngrok](https://ngrok.com/) with the command `ngrok http 8080` or [Cloudflare tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/) (follow the 2a. portion of the guide when you get to that point).
37
+
- Set up the traditional port forward on your router
38
+
- Use a tool like [ngrok](https://ngrok.com/) with the command `ngrok http 8080` or [Cloudflare tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/) (follow the 2a. portion of the guide when you get to that point).
38
39
39
40
If you do the cloudflare tunnels for amethyst as well,
40
41
you will also need
···
42
43
Although you can just run it locally and it will work.
43
44
44
45
#### 2. Set up .envs
46
+
45
47
1. copy [.env.template](.env.template) and name it [.env](.env). The docker build will pull everything from this `.env`. There are notes in the [.env.template](.env.template) on what some of the values should be.
46
48
2. Follow the instructions in [piper](https://github.com/teal-fm/piper) to set up environment variables for the music scraper. But name it `.env.air`
47
49
48
50
#### 3. Run docker
51
+
49
52
1. Make sure docker and docker compose is installed
50
53
2. It is recommended to run 'docker compose -f compose.dev.yml pull' to pull the latest remote images before running the docker compose command.
51
54
3. Run `docker compose -f compose.dev.yml up -d`
52
55
53
-
54
56
And that's it! You should have the full teal.fm stack running locally. Now if you are working on aqua you can do `docker container stop aqua-app` and run that locally during development while everything else is running in docker.
55
-
+3
-3
apps/amethyst/app/(tabs)/(stamp)/stamp/index.tsx
+3
-3
apps/amethyst/app/(tabs)/(stamp)/stamp/index.tsx
···
86
86
}}
87
87
/>
88
88
{/* Search Form */}
89
-
<View className="flex gap-2 max-w-2xl w-screen px-4">
90
-
<Text className="font-bold text-lg">Search for a track</Text>
89
+
<View className="flex w-screen max-w-2xl gap-2 px-4">
90
+
<Text className="text-lg font-bold">Search for a track</Text>
91
91
<Input
92
92
placeholder="Track name..."
93
93
value={searchFields.track}
···
124
124
}
125
125
}}
126
126
/>
127
-
<View className="flex-row gap-2 mt-2">
127
+
<View className="mt-2 flex-row gap-2">
128
128
<Button
129
129
className="flex-1"
130
130
onPress={handleSearch}
+11
-10
apps/amethyst/app/(tabs)/(stamp)/stamp/submit.tsx
+11
-10
apps/amethyst/app/(tabs)/(stamp)/stamp/submit.tsx
···
1
+
import { useContext, useEffect, useState } from "react";
2
+
import { Image, Switch, View } from "react-native";
3
+
import { Redirect, Stack, useRouter } from "expo-router";
1
4
import { ExternalLink } from "@/components/ExternalLink";
2
5
import VerticalPlayView from "@/components/play/verticalPlayView";
3
6
import { Button } from "@/components/ui/button";
4
7
import { Text } from "@/components/ui/text";
8
+
import { Textarea } from "@/components/ui/textarea";
5
9
import { MusicBrainzRecording, PlaySubmittedData } from "@/lib/oldStamp";
10
+
import { cn } from "@/lib/utils";
6
11
import { useStore } from "@/stores/mainStore";
7
12
import {
8
13
Agent,
···
10
15
ComAtprotoRepoCreateRecord,
11
16
RichText,
12
17
} from "@atproto/api";
13
-
import { Redirect, Stack, useRouter } from "expo-router";
14
-
import { useContext, useEffect, useState } from "react";
15
-
import { Image, Switch, View } from "react-native";
16
18
17
-
import { Textarea } from "@/components/ui/textarea";
18
-
import { cn } from "@/lib/utils";
19
19
import { Artist } from "@teal/lexicons/src/types/fm/teal/alpha/feed/defs";
20
20
import {
21
21
Record as PlayRecord,
22
22
validateRecord,
23
23
} from "@teal/lexicons/src/types/fm/teal/alpha/feed/play";
24
+
24
25
import { StampContext, StampContextValue, StampStep } from "./_layout";
25
26
26
27
type CardyBResponse = {
···
341
342
</Text>
342
343
</View>
343
344
344
-
<View className="flex-col gap-4 items-center w-full">
345
+
<View className="w-full flex-col items-center gap-4">
345
346
{blueskyEmbedCard && shareWithBluesky ? (
346
347
<View className="w-full gap-2">
347
348
<Text className="text-center text-sm text-muted-foreground">
···
372
373
)
373
374
)}
374
375
{shareWithBluesky && (
375
-
<View className="text-sm text-muted-foreground w-full items-end">
376
+
<View className="w-full items-end text-sm text-muted-foreground">
376
377
<Textarea
377
-
className="w-full p-2 pb-4 border border-border rounded-md text-card-foreground bg-card min-h-[100px] max-h-[200px]"
378
+
className="max-h-[200px] min-h-[100px] w-full rounded-md border border-border bg-card p-2 pb-4 text-card-foreground"
378
379
multiline
379
380
value={blueskyPostText}
380
381
onChangeText={setBlueskyPostText}
···
382
383
/>
383
384
<Text
384
385
className={cn(
385
-
"text-sm text-muted-foreground text-center absolute bottom-1 right-2",
386
+
"absolute bottom-1 right-2 text-center text-sm text-muted-foreground",
386
387
blueskyPostText.length > 150
387
388
? "text-gray-600 dark:text-gray-300"
388
389
: "",
···
393
394
</Text>
394
395
</View>
395
396
)}
396
-
<View className="flex-row gap-2 items-center">
397
+
<View className="flex-row items-center gap-2">
397
398
<Switch
398
399
value={shareWithBluesky}
399
400
onValueChange={setShareWithBluesky}
+6
-6
apps/amethyst/app/(tabs)/_layout.tsx
+6
-6
apps/amethyst/app/(tabs)/_layout.tsx
···
1
1
import React from "react";
2
+
import { Pressable } from "react-native";
3
+
import { Link, Tabs } from "expo-router";
4
+
import useIsMobile from "@/hooks/useIsMobile";
5
+
//import useIsMobile from "@/hooks/useIsMobile";
6
+
import { useStore } from "@/stores/mainStore";
2
7
import {
3
8
FilePen,
4
9
Home,
···
7
12
Settings,
8
13
type LucideIcon,
9
14
} from "lucide-react-native";
10
-
import { Link, Tabs } from "expo-router";
11
-
import { Pressable } from "react-native";
15
+
import { useColorScheme } from "nativewind";
12
16
13
17
import Colors from "../../constants/Colors";
14
18
import { Icon, iconWithClassName } from "../../lib/icons/iconWithClassName";
15
-
//import useIsMobile from "@/hooks/useIsMobile";
16
-
import { useStore } from "@/stores/mainStore";
17
-
import { useColorScheme } from "nativewind";
18
19
import AuthOptions from "../auth/options";
19
-
import useIsMobile from "@/hooks/useIsMobile";
20
20
21
21
function TabBarIcon(props: { name: LucideIcon; color: string }) {
22
22
const Name = props.name;
+3
-3
apps/amethyst/app/(tabs)/profile/[handle].tsx
+3
-3
apps/amethyst/app/(tabs)/profile/[handle].tsx
···
1
+
import { useEffect, useState } from "react";
2
+
import { ActivityIndicator, ScrollView, View } from "react-native";
3
+
import { Stack, useLocalSearchParams } from "expo-router";
1
4
import ActorView from "@/components/actor/actorView";
2
5
import { Text } from "@/components/ui/text";
3
6
import { resolveHandle } from "@/lib/atp/pid";
4
7
import { useStore } from "@/stores/mainStore";
5
-
import { Stack, useLocalSearchParams } from "expo-router";
6
-
import { useEffect, useState } from "react";
7
-
import { ActivityIndicator, ScrollView, View } from "react-native";
8
8
9
9
export default function Handle() {
10
10
let { handle } = useLocalSearchParams();
+7
-8
apps/amethyst/app/(tabs)/settings/index.tsx
+7
-8
apps/amethyst/app/(tabs)/settings/index.tsx
···
1
+
import React, { useState } from "react";
2
+
import { ScrollView, Switch, View } from "react-native";
3
+
import { Link, Stack } from "expo-router";
1
4
import { Button } from "@/components/ui/button";
5
+
import { Input } from "@/components/ui/input";
2
6
import { Text } from "@/components/ui/text";
3
7
import { useColorScheme } from "@/lib/useColorScheme";
4
8
import { cn } from "@/lib/utils";
5
-
import { Link, Stack } from "expo-router";
6
-
import React, { useState } from "react";
7
-
import { ScrollView, Switch, View } from "react-native";
8
-
9
-
import { Input } from "@/components/ui/input";
10
9
import pkg from "@/package.json";
11
10
import { useStore } from "@/stores/mainStore";
12
11
···
102
101
return (
103
102
<View className="items-start gap-2 pt-2">
104
103
<Text className="text-base font-semibold">{text}</Text>
105
-
<View className="flex-row items-center justify-around gap-1 w-full bg-muted h-10 px-1 rounded-xl">
104
+
<View className="h-10 w-full flex-row items-center justify-around gap-1 rounded-xl bg-muted px-1">
106
105
{values.map(({ label, value }) => (
107
106
<Button
108
107
key={value}
···
146
145
return (
147
146
<View className="items-start gap-2 pt-2">
148
147
<Text className="text-base font-semibold">{labelText}</Text>
149
-
<View className="flex-row gap-2 w-full items-center">
148
+
<View className="w-full flex-row items-center gap-2">
150
149
<Input
151
-
className="border border-muted-foreground/50 bg-transparent text-foreground h-10 w-full rounded-md px-3 py-2 text-base"
150
+
className="h-10 w-full rounded-md border border-muted-foreground/50 bg-transparent px-3 py-2 text-base text-foreground"
152
151
value={inputValue}
153
152
onChangeText={setInputValue} // Update internal state on change
154
153
placeholder={placeholder}
+2
-2
apps/amethyst/app/+not-found.tsx
+2
-2
apps/amethyst/app/+not-found.tsx
···
1
-
import { Link, Stack } from "expo-router";
1
+
import React from "react";
2
2
import { StyleSheet, View } from "react-native";
3
+
import { Link, Stack } from "expo-router";
3
4
4
-
import React from "react";
5
5
import { Text } from "../components/ui/text";
6
6
7
7
export default function NotFoundScreen() {
+3
-3
apps/amethyst/app/_layout.tsx
+3
-3
apps/amethyst/app/_layout.tsx
···
20
20
21
21
import "../global.css";
22
22
23
-
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
24
23
import { SafeAreaView, View } from "react-native";
24
+
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
25
25
26
26
let defaultFamily = (weight: string) => {
27
27
return {
···
85
85
}
86
86
87
87
return (
88
-
<SafeAreaView className="flex-1 flex flex-row min-h-screen justify-center bg-background">
89
-
<View className="max-w-2xl flex flex-1 border-x bg-background border-muted-foreground/20">
88
+
<SafeAreaView className="flex min-h-screen flex-1 flex-row justify-center bg-background">
89
+
<View className="flex max-w-2xl flex-1 border-x border-muted-foreground/20 bg-background">
90
90
<RootLayoutNav />
91
91
</View>
92
92
</SafeAreaView>
+18
-20
apps/amethyst/app/auth/login.tsx
+18
-20
apps/amethyst/app/auth/login.tsx
···
1
-
import { Button } from "@/components/ui/button";
2
-
import { Input } from "@/components/ui/input";
3
-
import { Text } from "@/components/ui/text";
4
-
import { Icon } from "@/lib/icons/iconWithClassName";
5
-
import { capFirstLetter, cn } from "@/lib/utils";
6
-
import { Link, router, Stack } from "expo-router";
7
-
import { openAuthSessionAsync } from "expo-web-browser";
8
-
import { AlertCircle, AtSign, Check, ChevronRight } from "lucide-react-native";
9
1
import React, { useCallback, useEffect, useRef, useState } from "react"; // Added useCallback, useRef
10
2
import { Platform, TextInput, View } from "react-native";
11
-
import { SafeAreaView } from "react-native-safe-area-context";
12
-
13
-
import { resolveFromIdentity } from "@/lib/atp/pid";
14
-
import { useStore } from "@/stores/mainStore";
15
-
16
-
import { FontAwesome6, MaterialCommunityIcons } from "@expo/vector-icons";
17
3
import Animated, {
18
4
interpolate,
19
5
useAnimatedStyle,
20
6
useSharedValue,
21
7
withTiming,
22
8
} from "react-native-reanimated";
9
+
import { SafeAreaView } from "react-native-safe-area-context";
10
+
import { Link, router, Stack } from "expo-router";
11
+
import { openAuthSessionAsync } from "expo-web-browser";
12
+
import { Button } from "@/components/ui/button";
13
+
import { Input } from "@/components/ui/input";
14
+
import { Text } from "@/components/ui/text";
15
+
import { resolveFromIdentity } from "@/lib/atp/pid";
16
+
import { Icon } from "@/lib/icons/iconWithClassName";
17
+
import { capFirstLetter, cn } from "@/lib/utils";
18
+
import { useStore } from "@/stores/mainStore";
19
+
import { FontAwesome6, MaterialCommunityIcons } from "@expo/vector-icons";
20
+
import { AlertCircle, AtSign, Check, ChevronRight } from "lucide-react-native";
23
21
24
22
type Url = URL;
25
23
···
218
216
headerShown: false,
219
217
}}
220
218
/>
221
-
<View className="justify-center align-center p-8 gap-4 pb-32 max-w-lg w-screen">
219
+
<View className="align-center w-screen max-w-lg justify-center gap-4 p-8 pb-32">
222
220
<View className="flex items-center">
223
221
<Icon icon={AtSign} className="color-bsky" name="at" size={64} />
224
222
</View>
···
250
248
<Animated.View style={messageContainerAnimatedStyle}>
251
249
<View
252
250
className={cn(
253
-
"p-2 -mt-7 rounded-xl border border-border transition-all duration-300",
251
+
"-mt-7 rounded-xl border border-border p-2 transition-all duration-300",
254
252
isSelected ? "pt-9" : "pt-8",
255
253
pdsUrl !== null
256
254
? pdsUrl.hostname.includes("bsky.network")
···
263
261
<Text>
264
262
PDS:{" "}
265
263
{pdsUrl.hostname.includes("bsky.network") && (
266
-
<View className="gap-0.5 pr-0.5 flex-row">
264
+
<View className="flex-row gap-0.5 pr-0.5">
267
265
<Icon
268
266
icon={FontAwesome6}
269
267
className="color-bsky"
···
286
284
<Text className="justify-baseline px-1">
287
285
<Icon
288
286
icon={AlertCircle}
289
-
className="mr-1 inline -mt-0.5 text-xs"
287
+
className="-mt-0.5 mr-1 inline text-xs"
290
288
size={24}
291
289
/>
292
290
{pdsResolutionError}
293
291
</Text>
294
292
) : (
295
-
<Text className="text-muted-foreground px-1">
293
+
<Text className="px-1 text-muted-foreground">
296
294
Resolving PDS...
297
295
</Text>
298
296
)}
299
297
</View>
300
298
</Animated.View>
301
299
</View>
302
-
<View className="flex flex-row justify-between items-center">
300
+
<View className="flex flex-row items-center justify-between">
303
301
<Link href="https://bsky.app/signup" asChild>
304
302
<Button variant="link" className="p-0">
305
303
<Text className="text-md text-secondary">
+4
-4
apps/amethyst/app/offline.tsx
+4
-4
apps/amethyst/app/offline.tsx
···
1
1
import { View } from "react-native";
2
+
import { Link, Stack } from "expo-router";
2
3
import { Text } from "@/components/ui/text";
3
4
import { Icon } from "@/lib/icons/iconWithClassName";
4
5
import { CloudOff } from "lucide-react-native";
5
-
import { Link, Stack } from "expo-router";
6
6
7
7
export default function Offline() {
8
8
return (
9
-
<View className="flex-1 justify-center items-center gap-2">
9
+
<View className="flex-1 items-center justify-center gap-2">
10
10
<Stack.Screen
11
11
options={{
12
12
title: "Can't Connect",
13
13
headerBackButtonDisplayMode: "minimal",
14
14
}}
15
15
/>
16
-
<View className="max-w-md justify-center items-center">
16
+
<View className="max-w-md items-center justify-center">
17
17
<Icon icon={CloudOff} size={64} />
18
18
<Text className="text-center">Oups! Can’t connect to teal.fm.</Text>
19
19
<Text className="text-center">
20
20
Try again in a few seconds, or you can change the AppView{" "}
21
21
<Link href="/settings">
22
-
<Text className="text-blue-600 dark:text-blue-400 border-b border-border">
22
+
<Text className="border-b border-border text-blue-600 dark:text-blue-400">
23
23
in Settings
24
24
</Text>
25
25
</Link>
+5
-3
apps/amethyst/components/play/actorPlaysView.tsx
+5
-3
apps/amethyst/components/play/actorPlaysView.tsx
···
1
-
import { useStore } from "@/stores/mainStore";
2
-
import { OutputSchema as ActorFeedResponse } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
3
1
import { useEffect, useState } from "react";
4
2
import { ScrollView } from "react-native";
5
3
import { Text } from "@/components/ui/text";
6
-
import PlayView from "./playView";
4
+
import { useStore } from "@/stores/mainStore";
7
5
import { Agent } from "@atproto/api";
6
+
7
+
import { OutputSchema as ActorFeedResponse } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
8
+
9
+
import PlayView from "./playView";
8
10
9
11
interface ActorPlaysViewProps {
10
12
repo: string | undefined;
+2
-2
apps/amethyst/components/play/playView.tsx
+2
-2
apps/amethyst/components/play/playView.tsx
···
28
28
{trackTitle}
29
29
</Text>
30
30
{artistName && (
31
-
<Text className="text-sm text-left text-foreground line-clamp-1 overflow-ellipsis">
31
+
<Text className="line-clamp-1 overflow-ellipsis text-left text-sm text-foreground">
32
32
{artistName}
33
33
</Text>
34
34
)}
35
35
{dateListened && (
36
-
<Text className="text-sm text-left text-muted-foreground line-clamp-1 overflow-ellipsis">
36
+
<Text className="line-clamp-1 overflow-ellipsis text-left text-sm text-muted-foreground">
37
37
played {timeAgo(dateListened)}
38
38
</Text>
39
39
)}
+2
-1
apps/amethyst/components/ui/textarea.tsx
+2
-1
apps/amethyst/components/ui/textarea.tsx
···
1
1
import * as React from "react";
2
2
import { TextInput, type TextInputProps } from "react-native";
3
+
3
4
import { cn } from "~/lib/utils";
4
5
5
6
const Textarea = React.forwardRef<
···
20
21
<TextInput
21
22
ref={ref}
22
23
className={cn(
23
-
"font-sans web:flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base lg:text-sm native:text-lg native:leading-[1.25] text-foreground web:ring-offset-background placeholder:text-muted-foreground web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
24
+
"native:text-lg native:leading-[1.25] min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 font-sans text-base text-foreground placeholder:text-muted-foreground web:flex web:ring-offset-background web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2 lg:text-sm",
24
25
props.editable === false && "opacity-50 web:cursor-not-allowed",
25
26
className,
26
27
)}
+1
-1
apps/amethyst/readme.md
+1
-1
apps/amethyst/readme.md
···
15
15
16
16
set `EXPO_PUBLIC_DID_WEB` to the domain aqua is running on.
17
17
For example if it's running at `aqua.teal.fm` it would be
18
-
`EXPO_PUBLIC_DID_WEB=did:web:aqua.teal.fm`. This is how amethyst sends requests to the appview.
18
+
`EXPO_PUBLIC_DID_WEB=did:web:aqua.teal.fm`. This is how amethyst sends requests to the appview.
19
19
20
20
TODO
+1
-1
apps/amethyst/stores/preferenceSlice.tsx
+1
-1
apps/amethyst/stores/preferenceSlice.tsx
···
11
11
return {
12
12
colorTheme: "system",
13
13
setColorTheme: (theme) => set({ colorTheme: theme }),
14
-
tealDid: process.env.EXPO_PUBLIC_DID_WEB ?? 'did:web:rina.z.teal.fm',
14
+
tealDid: process.env.EXPO_PUBLIC_DID_WEB ?? "did:web:rina.z.teal.fm",
15
15
setTealDid: (url) => set({ tealDid: url }),
16
16
};
17
17
};
+1
-1
apps/aqua/src/index.ts
+1
-1
apps/aqua/src/index.ts
+4
-4
apps/aqua/src/lib/env.ts
+4
-4
apps/aqua/src/lib/env.ts
···
1
-
import dotenv from 'dotenv';
1
+
import process from "node:process";
2
+
import dotenv from "dotenv";
2
3
import dotenvExpand from "dotenv-expand";
3
-
import { cleanEnv, host, port, str, testOnly } from 'envalid';
4
-
import process from 'node:process';
4
+
import { cleanEnv, host, port, str, testOnly } from "envalid";
5
5
6
6
dotenvExpand.expand(dotenv.config());
7
7
// in case our .env file is in the root folder
8
-
dotenvExpand.expand(dotenv.config({ path: './../../.env' }));
8
+
dotenvExpand.expand(dotenv.config({ path: "./../../.env" }));
9
9
10
10
export const env = cleanEnv(process.env, {
11
11
NODE_ENV: str({
+2
-1
apps/aqua/src/xrpc/feed/getActorFeed.ts
+2
-1
apps/aqua/src/xrpc/feed/getActorFeed.ts
···
1
1
import { TealContext } from "@/ctx";
2
+
import { and, desc, eq, lt, sql } from "drizzle-orm";
3
+
2
4
import { artists, db, plays, playToArtists } from "@teal/db";
3
-
import { eq, and, lt, desc, sql } from "drizzle-orm";
4
5
import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
5
6
6
7
export default async function getActorFeed(c: TealContext) {
+3
-2
apps/aqua/src/xrpc/route.ts
+3
-2
apps/aqua/src/xrpc/route.ts
···
1
1
import { EnvWithCtx } from "@/ctx";
2
2
import { Hono } from "hono";
3
3
import { logger } from "hono/logger";
4
-
import getPlay from "./feed/getPlay";
5
-
import getActorFeed from "./feed/getActorFeed";
4
+
6
5
import getProfile from "./actor/getProfile";
7
6
import searchActors from "./actor/searchActors";
7
+
import getActorFeed from "./feed/getActorFeed";
8
+
import getPlay from "./feed/getPlay";
8
9
9
10
// mount this on /xrpc
10
11
const app = new Hono<EnvWithCtx>();
+2
-2
package.json
+2
-2
package.json
···
10
10
"fix": "biome lint --apply . && biome format --write . && biome check . --apply",
11
11
"nuke": "rimraf node_modules */*/node_modules",
12
12
"lex:gen-server": "turbo lex:gen-server",
13
-
"format": "prettier --write ."
14
-
"db:migrate": "cd ./packages/db && drizzle-kit migrate"=
13
+
"format": "prettier --write .",
14
+
"db:migrate": "cd ./packages/db && drizzle-kit migrate"
15
15
},
16
16
"dependencies": {
17
17
"@atproto/oauth-client": "^0.3.8",
+10
-29
packages/db/.drizzle/meta/0006_snapshot.json
+10
-29
packages/db/.drizzle/meta/0006_snapshot.json
···
65
65
"name": "play_to_artists_artist_mbid_artists_mbid_fk",
66
66
"tableFrom": "play_to_artists",
67
67
"tableTo": "artists",
68
-
"columnsFrom": [
69
-
"artist_mbid"
70
-
],
71
-
"columnsTo": [
72
-
"mbid"
73
-
],
68
+
"columnsFrom": ["artist_mbid"],
69
+
"columnsTo": ["mbid"],
74
70
"onDelete": "no action",
75
71
"onUpdate": "no action"
76
72
},
···
78
74
"name": "play_to_artists_play_uri_plays_uri_fk",
79
75
"tableFrom": "play_to_artists",
80
76
"tableTo": "plays",
81
-
"columnsFrom": [
82
-
"play_uri"
83
-
],
84
-
"columnsTo": [
85
-
"uri"
86
-
],
77
+
"columnsFrom": ["play_uri"],
78
+
"columnsTo": ["uri"],
87
79
"onDelete": "no action",
88
80
"onUpdate": "no action"
89
81
}
···
91
83
"compositePrimaryKeys": {
92
84
"play_to_artists_play_uri_artist_mbid_pk": {
93
85
"name": "play_to_artists_play_uri_artist_mbid_pk",
94
-
"columns": [
95
-
"play_uri",
96
-
"artist_mbid"
97
-
]
86
+
"columns": ["play_uri", "artist_mbid"]
98
87
}
99
88
},
100
89
"uniqueConstraints": {},
···
204
193
"name": "plays_recording_mbid_recordings_mbid_fk",
205
194
"tableFrom": "plays",
206
195
"tableTo": "recordings",
207
-
"columnsFrom": [
208
-
"recording_mbid"
209
-
],
210
-
"columnsTo": [
211
-
"mbid"
212
-
],
196
+
"columnsFrom": ["recording_mbid"],
197
+
"columnsTo": ["mbid"],
213
198
"onDelete": "no action",
214
199
"onUpdate": "no action"
215
200
},
···
217
202
"name": "plays_release_mbid_releases_mbid_fk",
218
203
"tableFrom": "plays",
219
204
"tableTo": "releases",
220
-
"columnsFrom": [
221
-
"release_mbid"
222
-
],
223
-
"columnsTo": [
224
-
"mbid"
225
-
],
205
+
"columnsFrom": ["release_mbid"],
206
+
"columnsTo": ["mbid"],
226
207
"onDelete": "no action",
227
208
"onUpdate": "no action"
228
209
}
···
599
580
"schemas": {},
600
581
"tables": {}
601
582
}
602
-
}
583
+
}
+2
-4
packages/db/drizzle.config.ts
+2
-4
packages/db/drizzle.config.ts
···
1
-
import { defineConfig } from "drizzle-kit";
2
1
import dotenv from "dotenv";
3
2
import dotenvExpand from "dotenv-expand";
3
+
import { defineConfig } from "drizzle-kit";
4
4
5
5
//Loads from root .env
6
-
dotenvExpand.expand(dotenv.config({ path: '../../.env' }));
6
+
dotenvExpand.expand(dotenv.config({ path: "../../.env" }));
7
7
//Or can be overridden by .env in the current folder
8
8
dotenvExpand.expand(dotenv.config());
9
-
10
-
11
9
12
10
export default defineConfig({
13
11
dialect: "postgresql",
+21
-21
packages/lexicons/src/lexicons.ts
+21
-21
packages/lexicons/src/lexicons.ts
···
432
432
description: "The unix timestamp of when the item was recorded",
433
433
},
434
434
expiry: {
435
-
type: 'string',
436
-
format: 'datetime',
435
+
type: "string",
436
+
format: "datetime",
437
437
description:
438
-
'The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time.',
438
+
"The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time.",
439
439
},
440
440
item: {
441
441
type: "ref",
···
453
453
"This lexicon is in a not officially released state. It is subject to change. | Misc. items related to feeds.",
454
454
defs: {
455
455
playView: {
456
-
type: 'object',
457
-
required: ['trackName', 'artists'],
456
+
type: "object",
457
+
required: ["trackName", "artists"],
458
458
properties: {
459
459
trackName: {
460
460
type: "string",
···
476
476
description: "The length of the track in seconds",
477
477
},
478
478
artists: {
479
-
type: 'array',
479
+
type: "array",
480
480
items: {
481
-
type: 'ref',
482
-
ref: 'lex:fm.teal.alpha.feed.defs#artist',
481
+
type: "ref",
482
+
ref: "lex:fm.teal.alpha.feed.defs#artist",
483
483
},
484
-
description: 'Array of artists in order of original appearance.',
484
+
description: "Array of artists in order of original appearance.",
485
485
},
486
486
releaseName: {
487
487
type: "string",
···
521
521
},
522
522
},
523
523
artist: {
524
-
type: 'object',
525
-
required: ['artistName'],
524
+
type: "object",
525
+
required: ["artistName"],
526
526
properties: {
527
527
artistName: {
528
-
type: 'string',
528
+
type: "string",
529
529
minLength: 1,
530
530
maxLength: 256,
531
531
maxGraphemes: 2560,
532
-
description: 'The name of the artist',
532
+
description: "The name of the artist",
533
533
},
534
534
artistMbId: {
535
-
type: 'string',
536
-
description: 'The Musicbrainz ID of the artist',
535
+
type: "string",
536
+
description: "The Musicbrainz ID of the artist",
537
537
},
538
538
},
539
539
},
···
635
635
type: "record",
636
636
key: "tid",
637
637
record: {
638
-
type: 'object',
639
-
required: ['trackName'],
638
+
type: "object",
639
+
required: ["trackName"],
640
640
properties: {
641
641
trackName: {
642
642
type: "string",
···
677
677
"Array of Musicbrainz artist IDs. Prefer using 'artists'.",
678
678
},
679
679
artists: {
680
-
type: 'array',
680
+
type: "array",
681
681
items: {
682
-
type: 'ref',
683
-
ref: 'lex:fm.teal.alpha.feed.defs#artist',
682
+
type: "ref",
683
+
ref: "lex:fm.teal.alpha.feed.defs#artist",
684
684
},
685
-
description: 'Array of artists in order of original appearance.',
685
+
description: "Array of artists in order of original appearance.",
686
686
},
687
687
releaseName: {
688
688
type: "string",
+4
-4
packages/lexicons/src/types/fm/teal/alpha/actor/status.ts
+4
-4
packages/lexicons/src/types/fm/teal/alpha/actor/status.ts
···
10
10
11
11
export interface Record {
12
12
/** The unix timestamp of when the item was recorded */
13
-
time: string
13
+
time: string;
14
14
/** The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time. */
15
-
expiry?: string
16
-
item: FmTealAlphaFeedDefs.PlayView
17
-
[k: string]: unknown
15
+
expiry?: string;
16
+
item: FmTealAlphaFeedDefs.PlayView;
17
+
[k: string]: unknown;
18
18
}
19
19
20
20
export function isRecord(v: unknown): v is Record {
+9
-9
packages/lexicons/src/types/fm/teal/alpha/feed/defs.ts
+9
-9
packages/lexicons/src/types/fm/teal/alpha/feed/defs.ts
···
15
15
/** The Musicbrainz recording ID of the track */
16
16
recordingMbId?: string;
17
17
/** The length of the track in seconds */
18
-
duration?: number
18
+
duration?: number;
19
19
/** Array of artists in order of original appearance. */
20
-
artists: Artist[]
20
+
artists: Artist[];
21
21
/** The name of the release/album */
22
22
releaseName?: string;
23
23
/** The Musicbrainz release ID */
···
49
49
50
50
export interface Artist {
51
51
/** The name of the artist */
52
-
artistName: string
52
+
artistName: string;
53
53
/** The Musicbrainz ID of the artist */
54
-
artistMbId?: string
55
-
[k: string]: unknown
54
+
artistMbId?: string;
55
+
[k: string]: unknown;
56
56
}
57
57
58
58
export function isArtist(v: unknown): v is Artist {
59
59
return (
60
60
isObj(v) &&
61
-
hasProp(v, '$type') &&
62
-
v.$type === 'fm.teal.alpha.feed.defs#artist'
63
-
)
61
+
hasProp(v, "$type") &&
62
+
v.$type === "fm.teal.alpha.feed.defs#artist"
63
+
);
64
64
}
65
65
66
66
export function validateArtist(v: unknown): ValidationResult {
67
-
return lexicons.validate('fm.teal.alpha.feed.defs#artist', v)
67
+
return lexicons.validate("fm.teal.alpha.feed.defs#artist", v);
68
68
}
+10
-9
packages/lexicons/src/types/fm/teal/alpha/feed/play.ts
+10
-9
packages/lexicons/src/types/fm/teal/alpha/feed/play.ts
···
1
1
/**
2
2
* GENERATED CODE - DO NOT MODIFY
3
3
*/
4
-
import { ValidationResult, BlobRef } from '@atproto/lexicon'
5
-
import { lexicons } from '../../../../../lexicons'
6
-
import { isObj, hasProp } from '../../../../../util'
7
-
import { CID } from 'multiformats/cid'
8
-
import * as FmTealAlphaFeedDefs from './defs'
4
+
import { BlobRef, ValidationResult } from "@atproto/lexicon";
5
+
import { CID } from "multiformats/cid";
6
+
7
+
import { lexicons } from "../../../../../lexicons";
8
+
import { hasProp, isObj } from "../../../../../util";
9
+
import * as FmTealAlphaFeedDefs from "./defs";
9
10
10
11
export interface Record {
11
12
/** The name of the track */
···
15
16
/** The Musicbrainz recording ID of the track */
16
17
recordingMbId?: string;
17
18
/** The length of the track in seconds */
18
-
duration?: number
19
+
duration?: number;
19
20
/** Array of artist names in order of original appearance. Prefer using 'artists'. */
20
-
artistNames?: string[]
21
+
artistNames?: string[];
21
22
/** Array of Musicbrainz artist IDs. Prefer using 'artists'. */
22
-
artistMbIds?: string[]
23
+
artistMbIds?: string[];
23
24
/** Array of artists in order of original appearance. */
24
-
artists?: FmTealAlphaFeedDefs.Artist[]
25
+
artists?: FmTealAlphaFeedDefs.Artist[];
25
26
/** The name of the release/album */
26
27
releaseName?: string;
27
28
/** The Musicbrainz release ID */