170 lines
No EOL
5.3 KiB
JavaScript
170 lines
No EOL
5.3 KiB
JavaScript
import { createClient } from '@supabase/supabase-js'
|
|
import { DateTime } from 'luxon'
|
|
|
|
const SUPABASE_URL = process.env.SUPABASE_URL
|
|
const SUPABASE_KEY = process.env.SUPABASE_KEY
|
|
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
|
|
|
const fetchDataForPeriod = async (startPeriod, fields, table) => {
|
|
const PAGE_SIZE = 1000
|
|
let rows = []
|
|
let rangeStart = 0
|
|
|
|
while (true) {
|
|
const { data, error } = await supabase
|
|
.from(table)
|
|
.select(fields)
|
|
.order('listened_at', { ascending: false })
|
|
.gte('listened_at', startPeriod.toSeconds())
|
|
.range(rangeStart, rangeStart + PAGE_SIZE - 1)
|
|
|
|
if (error) {
|
|
console.error(error)
|
|
break
|
|
}
|
|
|
|
rows = rows.concat(data)
|
|
|
|
if (data.length < PAGE_SIZE) break
|
|
rangeStart += PAGE_SIZE
|
|
}
|
|
|
|
return rows
|
|
}
|
|
|
|
const fetchAllTimeData = async (fields, table) => {
|
|
const PAGE_SIZE = 1000
|
|
let rows = []
|
|
let rangeStart = 0
|
|
|
|
while (true) {
|
|
const { data, error } = await supabase
|
|
.from(table)
|
|
.select(fields)
|
|
.order('listened_at', { ascending: false })
|
|
.range(rangeStart, rangeStart + PAGE_SIZE - 1)
|
|
|
|
if (error) {
|
|
console.error(error)
|
|
break
|
|
}
|
|
|
|
rows = rows.concat(data)
|
|
|
|
if (data.length < PAGE_SIZE) break
|
|
rangeStart += PAGE_SIZE
|
|
}
|
|
|
|
return rows
|
|
}
|
|
|
|
const aggregateData = (data, groupByField, groupByType) => {
|
|
const aggregation = {}
|
|
|
|
data.forEach(item => {
|
|
const key = item[groupByField]
|
|
if (!aggregation[key]) {
|
|
if (groupByType === 'track') {
|
|
aggregation[key] = {
|
|
title: item[groupByField],
|
|
plays: 0,
|
|
mbid: item['albums']?.mbid || '',
|
|
url: item['albums']?.mbid ? `https://musicbrainz.org/release/${item['albums'].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item['album_name'])}&type=release`,
|
|
image: item['albums']?.image || '',
|
|
timestamp: item['listened_at'],
|
|
type: groupByType,
|
|
genre: item['artists']?.genre || 'Unknown'
|
|
}
|
|
} else {
|
|
aggregation[key] = {
|
|
title: item[groupByField],
|
|
plays: 0,
|
|
mbid: item[groupByType]?.mbid || '',
|
|
url: item[groupByType]?.mbid ? `https://musicbrainz.org/${groupByType === 'albums' ? 'release' : 'artist'}/${item[groupByType].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item[groupByField])}&type=${groupByType === 'albums' ? 'release' : 'artist'}`,
|
|
image: item[groupByType]?.image || '',
|
|
type: groupByType,
|
|
genre: item['artists']?.genre || 'Unknown'
|
|
}
|
|
}
|
|
if (
|
|
groupByType === 'track' ||
|
|
groupByType === 'albums'
|
|
) aggregation[key]['artist'] = item['artist_name']
|
|
}
|
|
aggregation[key].plays++
|
|
})
|
|
|
|
const aggregatedData = Object.values(aggregation).sort((a, b) => b.plays - a.plays)
|
|
|
|
aggregatedData.forEach((item, index) => {
|
|
item.rank = index + 1
|
|
})
|
|
|
|
return aggregatedData.filter(item => item.plays > 0)
|
|
}
|
|
|
|
const aggregateGenres = (data) => {
|
|
const genreAggregation = {}
|
|
data.forEach(item => {
|
|
const genre = item.artists.genre
|
|
if (!genreAggregation[genre]) {
|
|
genreAggregation[genre] = { genre, plays: 0 }
|
|
}
|
|
genreAggregation[genre].plays++
|
|
})
|
|
return Object.values(genreAggregation).sort((a, b) => b.plays - a.plays)
|
|
}
|
|
|
|
export default async function() {
|
|
const periods = {
|
|
week: DateTime.now().minus({ days: 7 }).startOf('day'), // last week
|
|
month: DateTime.now().minus({ days: 30 }).startOf('day'), // last 30 days
|
|
threeMonth: DateTime.now().minus({ months: 3 }).startOf('day'), // last three months
|
|
}
|
|
|
|
const results = {}
|
|
const selectFields = `
|
|
track_name,
|
|
artist_name,
|
|
album_name,
|
|
album_key,
|
|
listened_at,
|
|
artists (mbid, image, genre),
|
|
albums (mbid, image)
|
|
`
|
|
|
|
for (const [period, startPeriod] of Object.entries(periods)) {
|
|
const periodData = await fetchDataForPeriod(startPeriod, selectFields, 'listens')
|
|
results[period] = {
|
|
artists: aggregateData(periodData, 'artist_name', 'artists'),
|
|
albums: aggregateData(periodData, 'album_name', 'albums'),
|
|
tracks: aggregateData(periodData, 'track_name', 'track'),
|
|
genres: aggregateGenres(periodData),
|
|
totalTracks: periodData?.length?.toLocaleString('en-US')
|
|
}
|
|
}
|
|
|
|
// Fetch and aggregate all-time data
|
|
const allTimeData = await fetchAllTimeData(selectFields, 'listens')
|
|
results['allTime'] = {
|
|
artists: aggregateData(allTimeData, 'artist_name', 'artists'),
|
|
albums: aggregateData(allTimeData, 'album_name', 'albums'),
|
|
tracks: aggregateData(allTimeData, 'track_name', 'track'),
|
|
genres: aggregateGenres(allTimeData),
|
|
totalTracks: allTimeData?.length?.toLocaleString('en-US')
|
|
}
|
|
|
|
const recentData = await fetchDataForPeriod(DateTime.now().minus({ days: 7 }), selectFields, 'listens')
|
|
|
|
results['recent'] = {
|
|
artists: aggregateData(recentData, 'artist_name', 'artists'),
|
|
albums: aggregateData(recentData, 'album_name', 'albums'),
|
|
tracks: aggregateData(recentData, 'track_name', 'track'),
|
|
tracksChronological: aggregateData(recentData, 'track_name', 'track').sort((a, b) => b.timestamp - a.timestamp),
|
|
genres: aggregateGenres(recentData),
|
|
totalTracks: recentData?.length?.toLocaleString('en-US')
|
|
}
|
|
results['nowPlaying'] = results['recent']['tracksChronological'][0]
|
|
|
|
return results
|
|
} |