feat: data storage

This commit is contained in:
Cory Dransfeldt 2024-05-07 17:09:47 -07:00
parent bcc6ea0987
commit d8137dca96
No known key found for this signature in database
29 changed files with 428 additions and 14449 deletions

91
src/_data/artists.js Normal file
View file

@ -0,0 +1,91 @@
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, allTime = false) => {
let query = supabase.from(table).select(fields).order('listened_at', { ascending: false })
if (!allTime) query = query.gte('listened_at', startPeriod.toUTC().toSeconds())
const { data, error } = await query
if (error) {
console.error('Error fetching data:', error)
return []
}
return data
}
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 || '',
type: groupByType
}
} 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
}
}
}
aggregation[key].plays++
})
return Object.values(aggregation).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
year: DateTime.now().minus({ years: 1 }).startOf('day'), // Last 365 days
allTime: null // Null indicates no start period constraint
}
const results = {}
const selectFields = `
track_name,
artist_name,
album_name,
album_key,
artists (mbid, image),
albums (mbid, image)
`
for (const [period, startPeriod] of Object.entries(periods)) {
const isAllTime = period === 'allTime'
const periodData = await fetchDataForPeriod(startPeriod, selectFields, 'listens', isAllTime)
results[period] = {
artists: aggregateData(periodData, 'artist_name', 'artists'),
albums: aggregateData(periodData, 'album_name', 'albums'),
tracks: aggregateData(periodData, 'track_name', 'track')
}
}
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')
}
return results
}

View file

@ -1,91 +0,0 @@
const sanitizeMediaString = (string) => string.normalize('NFD').replace(/[\u0300-\u036f\u2010—\.\?\(\)\[\]\{\}]/g, '').replace(/\.{3}/g, '')
const artistSanitizedKey = (artist) => `${sanitizeMediaString(artist).replace(/\s+/g, '-').toLowerCase()}`
const albumSanitizedKey = (artist, album) => `${sanitizeMediaString(artist).replace(/\s+/g, '-').toLowerCase()}-${sanitizeMediaString(album.replace(/[:\/\\,'']+/g
, '').replace(/\s+/g, '-').toLowerCase())}`
export const buildChart = (tracks, artists, albums, nowPlaying = {}) => {
const artistsData = {}
const albumsData = {}
const tracksData = {}
const objectToArraySorted = (inputObject) => Object.values(inputObject).sort((a, b) => b.plays - a.plays)
tracks.forEach(track => {
if (!tracksData[track['track']]) {
const artistKey = artistSanitizedKey(track['artist'])
tracksData[track['track']] = {
artist: track['artist'],
title: track['track'],
plays: 1,
type: 'track',
url: (artists[artistKey]?.['mbid'] && artists[artistKey]?.['mbid'] !== '') ? `http://musicbrainz.org/artist/${artists[artistKey]?.['mbid']}` : `https://musicbrainz.org/search?query=${track['artist'].replace(
/\s+/g,
'+'
)}&type=artist`,
}
} else {
tracksData[track['track']]['plays']++
}
if (!artistsData[track['artist']]) {
const artistKey = artistSanitizedKey(track['artist'])
artistsData[track['artist']] = {
title: track['artist'],
plays: 1,
mbid: artists[artistKey]?.['mbid'] || '',
url: (artists[artistKey]?.['mbid'] && artists[artistKey]?.['mbid'] !== '') ? `http://musicbrainz.org/artist/${artists[artistKey]?.['mbid']}` : `https://musicbrainz.org/search?query=${track['artist'].replace(
/\s+/g,
'+'
)}&type=artist`,
image: artists[artistSanitizedKey(track['artist'])]?.['image'] || `https://coryd.dev/media/artists/${sanitizeMediaString(track['artist']).replace(/\s+/g, '-').toLowerCase()}.jpg`,
type: 'artist'
}
} else {
artistsData[track['artist']]['plays']++
}
if (!albumsData[track['album']]) {
const albumKey = albumSanitizedKey(track['artist'], track['album'])
albumsData[track['album']] = {
title: track['album'],
artist: track['artist'],
plays: 1,
mbid: albums[albumKey]?.['mbid'] || '',
url: (albums[albumKey]?.['mbid'] && albums[albumSanitizedKey(track['artist'], track['artist'], track['album'])]?.['mbid'] !== '') ? `https://musicbrainz.org/release/${albums[albumKey]?.['mbid']}` : `https://musicbrainz.org/taglookup/index?tag-lookup.artist=${track['artist'].replace(/\s+/g, '+')}&tag-lookup.release=${track['album'].replace(/\s+/g, '+')}`,
image: albums[albumKey]?.['image'] || `https://coryd.dev/media/albums/${sanitizeMediaString(track['artist']).replace(/\s+/g, '-').toLowerCase()}-${sanitizeMediaString(track['album'].replace(/[:\/\\,'']+/g
, '').replace(/\s+/g, '-').toLowerCase())}.jpg`,
type: 'album'
}
} else {
albumsData[track['album']]['plays']++
}
})
const topTracks = objectToArraySorted(tracksData).splice(0, 10)
const topTracksData = {
data: topTracks,
mostPlayed: Math.max(...topTracks.map(track => track.plays))
}
return {
artists: objectToArraySorted(artistsData),
albums: objectToArraySorted(albumsData),
tracks: objectToArraySorted(tracksData),
topTracks: topTracksData,
nowPlaying
}
}
export const buildTracksWithArt = (tracks, artists, albums) => {
tracks.forEach(track => {
track['image'] = albums[albumSanitizedKey(track['artist'], track['album'])]?.['image'] || `https://coryd.dev/media/albums/${sanitizeMediaString(track['artist']).replace(/\s+/g, '-').toLowerCase()}-${sanitizeMediaString(track['album'].replace(/[:\/\\,'']+/g
, '').replace(/\s+/g, '-').toLowerCase())}.jpg`
track['url'] = (artists[artistSanitizedKey(track['artist'])]?.['mbid'] && artists[artistSanitizedKey(track['artist'])]?.['mbid'] !== '') ? `http://musicbrainz.org/artist/${artists[artistSanitizedKey(track['artist'])]?.['mbid']}` : `https://musicbrainz.org/search?query=${track['artist'].replace(
/\s+/g,
'+'
)}&type=artist`
})
return tracks
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
{"track":"Superior","album":"Lifeless Birth","artist":"Necrot","trackNumber":3,"timestamp":"2024-04-12T17:25:38.120+00:00","genre":"death metal","url":"http://musicbrainz.org/artist/0556f527-d02e-440c-b0bb-3e1aa402cf19"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,11 +1,110 @@
import { readFile } from 'fs/promises'
import { buildChart } from './helpers/music.js'
import { createClient } from '@supabase/supabase-js'
import { DateTime } from 'luxon'
export default async function () {
const window = JSON.parse(await readFile('./src/_data/json/scrobbles-window.json', 'utf8'));
const artists = JSON.parse(await readFile('./src/_data/json/artists-map.json', 'utf8'));
const albums = JSON.parse(await readFile('./src/_data/json/albums-map.json', 'utf8'));
const nowPlaying = JSON.parse(await readFile('./src/_data/json/now-playing.json', 'utf8'));
const SUPABASE_URL = process.env.SUPABASE_URL
const SUPABASE_KEY = process.env.SUPABASE_KEY
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
return buildChart(window['data'], artists, albums, nowPlaying)
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.toUTC().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 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
}
} 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
}
}
if (
groupByType === 'track' ||
groupByType === 'albums'
) aggregation[key]['artist'] = item['artist_name']
}
aggregation[key].plays++
})
return Object.values(aggregation).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
year: DateTime.now().minus({ years: 1 }).startOf('day'), // Last 365 days
}
const results = {}
const selectFields = `
track_name,
artist_name,
album_name,
album_key,
listened_at,
artists (mbid, image),
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')
}
}
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')
}
results.nowPlaying = results.recent.tracks[0]
return results
}

View file

@ -1,18 +0,0 @@
import { readFile } from 'fs/promises'
import { buildChart, buildTracksWithArt } from './helpers/music.js'
export default async function () {
const monthChart = JSON.parse(await readFile('./src/_data/json/scrobbles-month-chart.json', 'utf8'));
const threeMonthChart = JSON.parse(await readFile('./src/_data/json/scrobbles-three-month-chart.json', 'utf8'));
const yearChart = JSON.parse(await readFile('./src/_data/json/scrobbles-year-chart.json', 'utf8'));
const artists = JSON.parse(await readFile('./src/_data/json/artists-map.json', 'utf8'));
const albums = JSON.parse(await readFile('./src/_data/json/albums-map.json', 'utf8'));
const recent = JSON.parse(await readFile('./src/_data/json/scrobbles-window.json', 'utf8'))['data'].reverse().splice(0,10)
return {
recent: buildTracksWithArt(recent, artists, albums),
month: buildChart(monthChart['data'], artists, albums),
threeMonth: buildChart(threeMonthChart['data'], artists, albums),
year: buildChart(yearChart['data'], artists, albums),
}
}

View file

@ -1,25 +0,0 @@
import { readFile } from 'fs/promises'
import { buildChart } from './helpers/music.js'
import { DateTime } from 'luxon'
export default async function () {
const currentDate = DateTime.now()
const lastWeek = currentDate.minus({ weeks: 1 })
const artists = JSON.parse(await readFile('./src/_data/json/artists-map.json', 'utf8'));
const albums = JSON.parse(await readFile('./src/_data/json/albums-map.json', 'utf8'));
const chartData = JSON.parse(await readFile('./src/_data/json/weekly-top-artists-chart.json', 'utf8'))
const artistChart = buildChart(chartData['data'], artists, albums)['artists'].splice(0, 8)
let content = 'My top artists for the week: '
artistChart.forEach((artist, index) => {
content += `${artist['title']} @ ${artist['plays']} play${parseInt(artist['plays']) > 1 ? 's' : ''}`
if (index !== artistChart.length - 1) content += ', '
})
content += ' #Music'
return [{
title: content,
url: `https://coryd.dev/now?ts=${lastWeek.year}-${lastWeek.weekNumber}#artists`,
date: DateTime.fromMillis(parseInt(chartData['timestamp'])).toISO(),
description: `My top artists for the last week.<br/><br/>`
}]
}

View file

@ -19,16 +19,16 @@ layout: default
</div>
</div>
<div id="artists-window">
{% render "partials/now/media-grid.liquid", data:music.artists, shape: "square", count: 8, loading: "eager" %}
{% render "partials/now/media-grid.liquid", data:music.week.artists, shape: "square", count: 8, loading: "eager" %}
</div>
<div class="hidden" id="artists-month">
{% render "partials/now/media-grid.liquid", data:musicCharts.month.artists, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.month.artists, shape: "square", count: 8 %}
</div>
<div class="hidden" id="artists-three-months">
{% render "partials/now/media-grid.liquid", data:musicCharts.threeMonth.artists, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.threeMonth.artists, shape: "square", count: 8 %}
</div>
<div class="hidden" id="artists-year">
{% render "partials/now/media-grid.liquid", data:musicCharts.year.artists, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.year.artists, shape: "square", count: 8 %}
</div>
<div class="section-header-wrapper">
<h2 id="albums" class="section-header flex-centered">
@ -43,16 +43,16 @@ layout: default
</div>
</div>
<div id="albums-window">
{% render "partials/now/media-grid.liquid", data:music.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.week.albums, shape: "square", count: 8 %}
</div>
<div class="hidden" id="albums-month">
{% render "partials/now/media-grid.liquid", data:musicCharts.month.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.month.albums, shape: "square", count: 8 %}
</div>
<div class="hidden" id="albums-three-months">
{% render "partials/now/media-grid.liquid", data:musicCharts.threeMonth.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.threeMonth.albums, shape: "square", count: 8 %}
</div>
<div class="hidden" id="albums-year">
{% render "partials/now/media-grid.liquid", data:musicCharts.year.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.year.albums, shape: "square", count: 8 %}
</div>
<div class="section-header-wrapper">
<h2 id="tracks" class="section-header flex-centered">
@ -68,19 +68,19 @@ layout: default
</div>
</div>
<div id="tracks-recent">
{% render "partials/now/tracks-recent.liquid", data:musicCharts.recent %}
{% render "partials/now/tracks-recent.liquid", data:music.recent.tracks %}
</div>
<div class="hidden" id="tracks-window">
{% render "partials/now/track-chart.liquid", data:music.topTracks.data, mostPlayed:music.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.week.tracks, mostPlayed:music.week.tracks[0].plays %}
</div>
<div class="hidden" id="tracks-month">
{% render "partials/now/track-chart.liquid", data:musicCharts.month.topTracks.data, mostPlayed:musicCharts.month.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.month.tracks, mostPlayed:music.month.tracks[0].plays %}
</div>
<div class="hidden" id="tracks-three-months">
{% render "partials/now/track-chart.liquid", data:musicCharts.threeMonth.topTracks.data, mostPlayed:musicCharts.threeMonth.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.threeMonth.tracks, mostPlayed:music.threeMonth.tracks[0].plays %}
</div>
<div class="hidden" id="tracks-year">
{% render "partials/now/track-chart.liquid", data:musicCharts.year.topTracks.data, mostPlayed:musicCharts.year.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.year.tracks, mostPlayed:music.year.tracks[0].plays %}
</div>
{% render "partials/now/album-releases.liquid", albumReleases:albumReleases %}
<h2 id="books" class="section-header flex-centered">

View file

@ -1,12 +1,12 @@
{% if data.size > 0 %}
<div class="music-chart">
{% for item in data limit: 10 %}
{% capture alt %}{{ item.track | escape }} by {{ item.artist }}{% endcapture %}
{% capture alt %}{{ item.title | escape }} by {{ item.artist }}{% endcapture %}
<div class="item">
<div class="meta">
<img src="https://coryd.dev/.netlify/images/?url={{ item.image }}&fit=cover&w=64&h=64&fm=webp&q=65" class="image-banner" alt="{{ alt }}" loading="lazy" decoding="async" width="64" height="64" />
<div class="meta-text">
<div class="title">{{ item.track }}</div>
<div class="title">{{ item.title }}</div>
<div class="subtext">
<a href="{{ item.url }}">{{ item.artist }}</a>
</div>

View file

@ -1,13 +0,0 @@
---
layout: null
eleventyExcludeFromCollections: true
permalink: /feeds/weekly-artist-chart
---
{% render "partials/feeds/rss.liquid"
permalink:"/feeds/weekly-artist-chart"
title:"Weekly artist chart • Cory Dransfeldt"
description:"The top 8 artists I've listened to this week."
data:weeklyArtistChart
updated:weeklyArtistChart[0].date
site:site
%}

View file

@ -1,7 +1,7 @@
---
layout: default
---
{% render "partials/home/now.liquid" status:status, artists:music.artists, books:books, tv:tv %}
{% render "partials/home/now.liquid" status:status, artists:music.week.artists, books:books, tv:tv %}
{% render "partials/home/posts.liquid" icon: "star", title: "Featured", postData:collections.posts, postType: "featured" %}
{% assign posts = collections.posts | reverse %}
{% render "partials/home/posts.liquid" icon: "clock-2", title: "Recent posts", postData:posts %}

View file

@ -3,7 +3,7 @@ title: About
layout: default
permalink: /about.html
---
{%- assign artist = music.artists | first -%}
{%- assign artist = music.week.artists | first -%}
{%- assign book = books | bookStatus: 'started' | reverse | first -%}
{%- assign show = tv | first -%}
<div class="avatar-wrapper flex-centered">

View file

@ -20,7 +20,7 @@ description: "See what I'm doing now."
</p>
<p>
{% tablericon "headphones" "Listening to" %}
Listening to tracks like <strong class="highlight-text">{{ music.nowPlaying.track }}</strong> by <strong class="highlight-text">{{ music.nowPlaying.artist }}</strong>.
Listening to tracks like <strong class="highlight-text">{{ music.nowPlaying.title }}</strong> by <strong class="highlight-text">{{ music.nowPlaying.artist }}</strong>.
</p>
<p>
{% tablericon "needle" "Getting tattooed" %}