+22
-14
lexicons/lexicons/teal/feed/defs.json
+22
-14
lexicons/lexicons/teal/feed/defs.json
···
5
5
"defs": {
6
6
"playView": {
7
7
"type": "object",
8
-
"required": ["trackName", "artistNames"],
8
+
"required": ["trackName", "artists"],
9
9
"properties": {
10
10
"trackName": {
11
11
"type": "string",
···
26
26
"type": "integer",
27
27
"description": "The length of the track in seconds"
28
28
},
29
-
"artistNames": {
30
-
"type": "array",
31
-
"items": {
32
-
"type": "string",
33
-
"minLength": 1,
34
-
"maxLength": 256,
35
-
"maxGraphemes": 2560
36
-
},
37
-
"description": "Array of artist names in order of original appearance."
38
-
},
39
-
"artistMbIds": {
29
+
"artists": {
40
30
"type": "array",
41
31
"items": {
42
-
"type": "string"
32
+
"type": "ref",
33
+
"ref": "#artist"
43
34
},
44
-
"description": "Array of Musicbrainz artist IDs"
35
+
"description": "Array of artists in order of original appearance."
45
36
},
46
37
"releaseName": {
47
38
"type": "string",
···
75
66
"type": "string",
76
67
"format": "datetime",
77
68
"description": "The unix timestamp of when the track was played"
69
+
}
70
+
}
71
+
},
72
+
"artist": {
73
+
"type": "object",
74
+
"required": ["artistName"],
75
+
"properties": {
76
+
"artistName": {
77
+
"type": "string",
78
+
"minLength": 1,
79
+
"maxLength": 256,
80
+
"maxGraphemes": 2560,
81
+
"description": "The name of the artist"
82
+
},
83
+
"artistMbId": {
84
+
"type": "string",
85
+
"description": "The Musicbrainz ID of the artist"
78
86
}
79
87
}
80
88
}
+11
-3
lexicons/lexicons/teal/feed/play.json
+11
-3
lexicons/lexicons/teal/feed/play.json
···
8
8
"key": "tid",
9
9
"record": {
10
10
"type": "object",
11
-
"required": ["trackName", "artistNames"],
11
+
"required": ["trackName"],
12
12
"properties": {
13
13
"trackName": {
14
14
"type": "string",
···
38
38
"maxLength": 256,
39
39
"maxGraphemes": 2560
40
40
},
41
-
"description": "Array of artist names in order of original appearance."
41
+
"description": "Array of artist names in order of original appearance. Prefer using 'artists'."
42
42
},
43
43
"artistMbIds": {
44
44
"type": "array",
45
45
"items": {
46
46
"type": "string"
47
47
},
48
-
"description": "Array of Musicbrainz artist IDs"
48
+
"description": "Array of Musicbrainz artist IDs. Prefer using 'artists'."
49
+
},
50
+
"artists": {
51
+
"type": "array",
52
+
"items": {
53
+
"type": "ref",
54
+
"ref": "fm.teal.alpha.feed.defs#artist"
55
+
},
56
+
"description": "Array of artists in order of original appearance."
49
57
},
50
58
"releaseName": {
51
59
"type": "string",
+182
-138
src/components/LookUp.vue
+182
-138
src/components/LookUp.vue
···
1
1
<script setup lang="ts">
2
-
import {ref} from 'vue'
2
+
import { ref } from "vue";
3
3
import {
4
-
CompositeDidDocumentResolver,
5
-
CompositeHandleResolver,
6
-
DohJsonHandleResolver, PlcDidDocumentResolver, WebDidDocumentResolver,
7
-
WellKnownHandleResolver
4
+
CompositeDidDocumentResolver,
5
+
CompositeHandleResolver,
6
+
DohJsonHandleResolver,
7
+
PlcDidDocumentResolver,
8
+
WebDidDocumentResolver,
9
+
WellKnownHandleResolver,
8
10
} from "@atcute/identity-resolver";
9
-
import {AtpAgent} from '@atproto/api'
11
+
import { AtpAgent } from "@atproto/api";
10
12
11
13
// handle resolution
12
14
const handleResolver = new CompositeHandleResolver({
13
-
strategy: 'race',
14
-
methods: {
15
-
dns: new DohJsonHandleResolver({dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query'}),
16
-
http: new WellKnownHandleResolver(),
17
-
},
15
+
strategy: "race",
16
+
methods: {
17
+
dns: new DohJsonHandleResolver({
18
+
dohUrl: "https://mozilla.cloudflare-dns.com/dns-query",
19
+
}),
20
+
http: new WellKnownHandleResolver(),
21
+
},
18
22
});
19
23
20
24
const docResolver = new CompositeDidDocumentResolver({
21
-
methods: {
22
-
plc: new PlcDidDocumentResolver(),
23
-
web: new WebDidDocumentResolver(),
24
-
},
25
+
methods: {
26
+
plc: new PlcDidDocumentResolver(),
27
+
web: new WebDidDocumentResolver(),
28
+
},
25
29
});
26
30
27
-
const userHandle = ref('')
28
-
const loading = ref(false)
29
-
const artists = ref<{ name: string, plays: number }[]>([])
30
-
const tracks = ref<{ name: string, plays: number }[]>([])
31
-
const totalSongs = ref(0)
31
+
const userHandle = ref("");
32
+
const loading = ref(false);
33
+
const artists = ref<{ name: string; plays: number }[]>([]);
34
+
const tracks = ref<{ name: string; plays: number }[]>([]);
35
+
const totalSongs = ref(0);
32
36
33
37
const lookup = async () => {
34
-
loading.value = true
35
-
try {
36
-
const did = await handleResolver.resolve(userHandle.value as `${string}.${string}`)
37
-
38
-
if (did == undefined) {
39
-
throw new Error('expected handle to resolve')
40
-
}
41
-
console.log(did) // did:plc:ewvi7nxzyoun6zhxrhs64oiz
38
+
loading.value = true;
39
+
try {
40
+
const did = await handleResolver.resolve(
41
+
userHandle.value as `${string}.${string}`,
42
+
);
42
43
43
-
const doc = await docResolver.resolve(did);
44
-
console.log(doc)
44
+
if (did == undefined) {
45
+
throw new Error("expected handle to resolve");
46
+
}
47
+
console.log(did); // did:plc:ewvi7nxzyoun6zhxrhs64oiz
45
48
46
-
// const handler = simpleFetchHandler({ service: });
47
-
const agent = new AtpAgent({service: doc.service[0].serviceEndpoint as string})
48
-
let cursor = '';
49
-
let plays = [];
50
-
let response = await agent.com.atproto.repo.listRecords({
51
-
repo: did,
52
-
collection: 'fm.teal.alpha.feed.play',
53
-
limit: 100,
54
-
cursor: cursor
55
-
})
49
+
const doc = await docResolver.resolve(did);
50
+
console.log(doc);
56
51
52
+
// const handler = simpleFetchHandler({ service: });
53
+
const agent = new AtpAgent({
54
+
service: doc.service[0].serviceEndpoint as string,
55
+
});
56
+
let cursor = "";
57
+
let plays = [];
58
+
let response = await agent.com.atproto.repo.listRecords({
59
+
repo: did,
60
+
collection: "fm.teal.alpha.feed.play",
61
+
limit: 100,
62
+
cursor: cursor,
63
+
});
57
64
58
-
do {
59
-
plays.push(...response.data.records)
60
-
cursor = response.data.cursor
65
+
do {
66
+
plays.push(...response.data.records);
67
+
cursor = response.data.cursor;
61
68
62
-
if (cursor) {
63
-
response = await agent.com.atproto.repo.listRecords({
64
-
repo: did,
65
-
collection: 'fm.teal.alpha.feed.play',
66
-
limit: 100,
67
-
cursor: cursor
68
-
})
69
-
}
70
-
} while (cursor)
69
+
if (cursor) {
70
+
response = await agent.com.atproto.repo.listRecords({
71
+
repo: did,
72
+
collection: "fm.teal.alpha.feed.play",
73
+
limit: 100,
74
+
cursor: cursor,
75
+
});
76
+
}
77
+
} while (cursor);
71
78
72
-
let inner_tracks = [];
73
-
let inner_artists = [];
74
-
for (const play of plays) {
75
-
for (const arist of play.value.artistNames) {
76
-
let alreadyPlayed = inner_artists.find(a => a.name === arist)
77
-
if (!alreadyPlayed) {
78
-
inner_artists.push({name: arist, plays: 1})
79
-
}else{
80
-
alreadyPlayed.plays++
81
-
}
82
-
}
79
+
let inner_tracks = [];
80
+
let inner_artists = [];
81
+
for (const play of plays) {
82
+
// new version
83
+
if (play.value?.artists) {
84
+
for (const artist of play.value?.artists) {
85
+
let alreadyPlayed = inner_artists.find(
86
+
(a) => a.name === artist,
87
+
);
88
+
if (!alreadyPlayed) {
89
+
inner_artists.push({ name: artist, plays: 1 });
90
+
} else {
91
+
alreadyPlayed.plays++;
92
+
}
93
+
}
94
+
} else {
95
+
// old version
96
+
for (const arist of play.value?.artistNames) {
97
+
let alreadyPlayed = inner_artists.find(
98
+
(a) => a.name === arist,
99
+
);
100
+
if (!alreadyPlayed) {
101
+
inner_artists.push({ name: arist, plays: 1 });
102
+
} else {
103
+
alreadyPlayed.plays++;
104
+
}
105
+
}
106
+
}
83
107
84
-
let alreadyPlayed = inner_tracks.find(a => a.name === play.value.trackName)
85
-
if(!alreadyPlayed) {
86
-
inner_tracks.push({name: play.value.trackName, artist: play.value.artistNames[0], plays: 1})
87
-
}else{
88
-
alreadyPlayed.plays++
89
-
}
108
+
let alreadyPlayed = inner_tracks.find(
109
+
(a) => a.name === play.value.trackName,
110
+
);
111
+
if (!alreadyPlayed) {
112
+
inner_tracks.push({
113
+
name: play.value.trackName,
114
+
artist: play.value.artistNames[0],
115
+
plays: 1,
116
+
});
117
+
} else {
118
+
alreadyPlayed.plays++;
119
+
}
120
+
}
90
121
122
+
artists.value = inner_artists
123
+
.sort((a, b) => b.plays - a.plays)
124
+
.slice(0, 10);
125
+
tracks.value = inner_tracks
126
+
.sort((a, b) => b.plays - a.plays)
127
+
.slice(0, 10);
128
+
totalSongs.value = plays.length;
129
+
} finally {
130
+
loading.value = false;
91
131
}
92
-
93
-
artists.value = inner_artists.sort((a, b) => b.plays - a.plays).slice(0, 10)
94
-
tracks.value = inner_tracks.sort((a, b) => b.plays - a.plays).slice(0, 10)
95
-
totalSongs.value = plays.length
96
-
} finally {
97
-
loading.value = false
98
-
}
99
-
}
100
-
101
-
132
+
};
102
133
</script>
103
134
104
135
<template>
105
-
<div class="container mx-auto p-4 text-center">
106
-
<h1 class="text-5xl font-bold mb-2 bg-gradient-to-r from-teal-400 to-teal-600 text-transparent bg-clip-text">
107
-
Teal Wrapped
108
-
</h1>
109
-
<p class="text-sm text-gray-500 mb-8">Mostly not affiliated with teal.fm™</p>
110
-
<div class="join w-full justify-center">
111
-
<input
112
-
v-model="userHandle"
113
-
type="text"
114
-
placeholder="alice.bsky.social"
115
-
class="input input-bordered join-item w-1/2 max-w-xs"
116
-
/>
117
-
<button @click="lookup" class="btn join-item bg-teal-500 hover:bg-teal-600 text-white">That's a wrap</button>
118
-
119
-
</div>
120
-
<div class="w-full justify-center">
121
-
<span v-if="loading" class="loading loading-dots loading-lg mt-8"></span>
122
-
<div v-if="tracks.length > 0" class="mt-8">
123
-
<h2 class="text-2xl font-bold mb-4">Top Songs out of {{totalSongs}}</h2>
124
-
<div class="overflow-x-auto">
125
-
<table class="table w-full">
126
-
<thead>
127
-
<tr>
128
-
<th>Plays</th>
129
-
<th>Song</th>
130
-
</tr>
131
-
</thead>
132
-
<tbody>
133
-
<tr v-for="track in tracks" :key="track.name">
134
-
<td>{{ track.plays }}</td>
135
-
<td>{{ track.name }} by {{track.artist}}</td>
136
-
</tr>
137
-
</tbody>
138
-
</table>
136
+
<div class="container mx-auto p-4 text-center">
137
+
<h1
138
+
class="text-5xl font-bold mb-2 bg-gradient-to-r from-teal-400 to-teal-600 text-transparent bg-clip-text"
139
+
>
140
+
Teal Wrapped
141
+
</h1>
142
+
<p class="text-sm text-gray-500 mb-8">
143
+
Mostly not affiliated with teal.fm™
144
+
</p>
145
+
<div class="join w-full justify-center">
146
+
<input
147
+
v-model="userHandle"
148
+
type="text"
149
+
placeholder="alice.bsky.social"
150
+
class="input input-bordered join-item w-1/2 max-w-xs"
151
+
/>
152
+
<button
153
+
@click="lookup"
154
+
class="btn join-item bg-teal-500 hover:bg-teal-600 text-white"
155
+
>
156
+
That's a wrap
157
+
</button>
139
158
</div>
140
-
</div>
141
-
<div v-if="artists.length > 0" class="mt-8">
142
-
<h2 class="text-2xl font-bold mb-4">Top Artists</h2>
143
-
<div class="overflow-x-auto">
144
-
<table class="table w-full">
145
-
<thead>
146
-
<tr>
147
-
<th>Plays</th>
148
-
<th>Artist</th>
149
-
</tr>
150
-
</thead>
151
-
<tbody>
152
-
<tr v-for="artist in artists" :key="artist.name">
153
-
<td>{{ artist.plays }}</td>
154
-
<td>{{ artist.name }}</td>
155
-
</tr>
156
-
</tbody>
157
-
</table>
159
+
<div class="w-full justify-center">
160
+
<span
161
+
v-if="loading"
162
+
class="loading loading-dots loading-lg mt-8"
163
+
></span>
164
+
<div v-if="tracks.length > 0" class="mt-8">
165
+
<h2 class="text-2xl font-bold mb-4">
166
+
Top Songs out of {{ totalSongs }}
167
+
</h2>
168
+
<div class="overflow-x-auto">
169
+
<table class="table w-full">
170
+
<thead>
171
+
<tr>
172
+
<th>Plays</th>
173
+
<th>Song</th>
174
+
</tr>
175
+
</thead>
176
+
<tbody>
177
+
<tr v-for="track in tracks" :key="track.name">
178
+
<td>{{ track.plays }}</td>
179
+
<td>{{ track.name }} by {{ track.artist }}</td>
180
+
</tr>
181
+
</tbody>
182
+
</table>
183
+
</div>
184
+
</div>
185
+
<div v-if="artists.length > 0" class="mt-8">
186
+
<h2 class="text-2xl font-bold mb-4">Top Artists</h2>
187
+
<div class="overflow-x-auto">
188
+
<table class="table w-full">
189
+
<thead>
190
+
<tr>
191
+
<th>Plays</th>
192
+
<th>Artist</th>
193
+
</tr>
194
+
</thead>
195
+
<tbody>
196
+
<tr v-for="artist in artists" :key="artist.name">
197
+
<td>{{ artist.plays }}</td>
198
+
<td>{{ artist.name }}</td>
199
+
</tr>
200
+
</tbody>
201
+
</table>
202
+
</div>
203
+
</div>
158
204
</div>
159
-
</div>
160
205
</div>
161
-
</div>
162
206
</template>
163
207
164
208
<style scoped>
165
209
.container {
166
-
min-height: 50vh;
167
-
display: flex;
168
-
flex-direction: column;
169
-
justify-content: center;
210
+
min-height: 50vh;
211
+
display: flex;
212
+
flex-direction: column;
213
+
justify-content: center;
170
214
}
171
-
</style>
215
+
</style>