Merge branch 'main' of git@github.com:cdransf/coryd.dev.git
This commit is contained in:
commit
0381408235
38 changed files with 504 additions and 14466 deletions
4
.env
4
.env
|
@ -3,4 +3,6 @@ SITE_KEY_CLICKY=
|
|||
API_KEY_TRAKT=
|
||||
API_KEY_MOVIEDB=
|
||||
SECRET_FEED_ALBUM_RELEASES=
|
||||
ACCOUNT_ID_PLEX=
|
||||
ACCOUNT_ID_PLEX=
|
||||
SUPABASE_URL=
|
||||
SUPABASE_KEY=
|
|
@ -1,4 +1,8 @@
|
|||
import { getStore } from '@netlify/blobs'
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL
|
||||
const SUPABASE_KEY = process.env.SUPABASE_KEY
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
const emojiMap = (genre, artist) => {
|
||||
const DEFAULT = "🎧"
|
||||
|
@ -77,28 +81,43 @@ const emojiMap = (genre, artist) => {
|
|||
}
|
||||
|
||||
export default async () => {
|
||||
const scrobbles = getStore('scrobbles')
|
||||
const { data, error } = await supabase
|
||||
.from('listens')
|
||||
.select(`
|
||||
track_name,
|
||||
artist_name,
|
||||
artists (mbid, genre),
|
||||
`)
|
||||
.order('listened_at', { ascending: false })
|
||||
.limit(1);
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, s-maxage=360, stale-while-revalidate=1080",
|
||||
}
|
||||
const scrobbleData = await scrobbles.get('now-playing', { type: 'json'})
|
||||
};
|
||||
|
||||
if (!scrobbleData) return new Response(JSON.stringify({}, { headers }))
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return new Response(JSON.stringify({ error: "Failed to fetch the latest track" }), { headers });
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return new Response(JSON.stringify({ message: "No recent tracks found" }), { headers });
|
||||
}
|
||||
|
||||
const scrobbleData = data[0]
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
content: `${emojiMap(
|
||||
scrobbleData['genre'],
|
||||
scrobbleData['artist']
|
||||
)} ${scrobbleData['track']} by <a href="${scrobbleData['url']}">${
|
||||
scrobbleData['artist']
|
||||
}</a>`,
|
||||
}),
|
||||
{ headers }
|
||||
)
|
||||
}
|
||||
content: `${emojiMap(
|
||||
scrobbleData.artists.genre,
|
||||
scrobbleData.artist_name
|
||||
)} ${scrobbleData.track_name} by <a href="http://musicbrainz.org/artist/${scrobbleData.artists.mbid}">${
|
||||
scrobbleData.artist_name
|
||||
}</a>`,
|
||||
}), { headers });
|
||||
};
|
||||
|
||||
export const config = {
|
||||
cache: "manual",
|
||||
path: "/api/now-playing"
|
||||
}
|
||||
};
|
126
api/scrobble.js
126
api/scrobble.js
|
@ -1,90 +1,78 @@
|
|||
import { getStore } from '@netlify/blobs'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
const SUPABASE_URL = Netlify.env.get('SUPABASE_URL')
|
||||
const SUPABASE_KEY = Netlify.env.get('SUPABASE_KEY')
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
const sanitizeMediaString = (string) => string.normalize('NFD').replace(/[\u0300-\u036f\u2010—\.\?\(\)\[\]\{\}]/g, '').replace(/\.{3}/g, '')
|
||||
|
||||
const weekKey = () => {
|
||||
const currentDate = DateTime.now();
|
||||
return `${currentDate.year}-${currentDate.weekNumber}`
|
||||
}
|
||||
|
||||
const filterOldScrobbles = (scrobbles) => {
|
||||
const windowEnd = DateTime.now().minus({ days: 7 });
|
||||
return scrobbles.filter(scrobble => {
|
||||
const timestamp = DateTime.fromISO(scrobble.timestamp);
|
||||
return timestamp >= windowEnd;
|
||||
});
|
||||
}
|
||||
|
||||
export default async (request) => {
|
||||
const ACCOUNT_ID_PLEX = Netlify.env.get('ACCOUNT_ID_PLEX');
|
||||
const MUSIC_KEY = Netlify.env.get('API_KEY_LASTFM');
|
||||
const params = new URL(request['url']).searchParams
|
||||
const ACCOUNT_ID_PLEX = process.env.ACCOUNT_ID_PLEX
|
||||
const params = new URL(request.url).searchParams
|
||||
const id = params.get('id')
|
||||
|
||||
if (!id) return new Response(JSON.stringify({
|
||||
status: 'Bad request',
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
)
|
||||
|
||||
if (id !== ACCOUNT_ID_PLEX) return new Response(JSON.stringify({
|
||||
status: 'Forbidden',
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
)
|
||||
if (!id) return new Response(JSON.stringify({ status: 'Bad request' }), { headers: { "Content-Type": "application/json" } })
|
||||
if (id !== ACCOUNT_ID_PLEX) return new Response(JSON.stringify({ status: 'Forbidden' }), { headers: { "Content-Type": "application/json" } })
|
||||
|
||||
const data = await request.formData()
|
||||
const payload = JSON.parse(data.get('payload'))
|
||||
const artists = getStore('artists')
|
||||
const scrobbles = getStore('scrobbles')
|
||||
|
||||
if (payload?.event === 'media.scrobble') {
|
||||
const artist = payload['Metadata']['grandparentTitle']
|
||||
const album = payload['Metadata']['parentTitle']
|
||||
const track = payload['Metadata']['title']
|
||||
const trackNumber = payload['Metadata']['index']
|
||||
const timestamp = DateTime.now()
|
||||
const artistsMap = await artists.get('artists-map', { type: 'json' })
|
||||
const artistSanitizedKey = `${sanitizeMediaString(artist).replace(/\s+/g, '-').toLowerCase()}`
|
||||
const trackScrobbleData = {
|
||||
track,
|
||||
album,
|
||||
artist,
|
||||
trackNumber,
|
||||
timestamp,
|
||||
genre: artistsMap[artistSanitizedKey]?.['genre'] || ''
|
||||
const artist = payload.Metadata.grandparentTitle
|
||||
const album = payload.Metadata.parentTitle
|
||||
const track = payload.Metadata.title
|
||||
const listenedAt = DateTime.now().toSeconds()
|
||||
const artistKey = sanitizeMediaString(artist).replace(/\s+/g, '-').toLowerCase()
|
||||
const albumKey = `${artistKey}-${sanitizeMediaString(album).replace(/\s+/g, '-').toLowerCase()}`
|
||||
|
||||
const { data: albumData, error: albumError } = await supabase
|
||||
.from('albums')
|
||||
.select('*')
|
||||
.eq('key', albumKey)
|
||||
.single()
|
||||
|
||||
if (albumError && albumError.code === 'PGRST116') {
|
||||
const albumImageUrl = `https://coryd.dev/media/albums/${albumKey}.jpg`
|
||||
const albumMBID = null
|
||||
const { error: insertAlbumError } = await supabase.from('albums').insert([
|
||||
{
|
||||
mbid: albumMBID,
|
||||
image: albumImageUrl,
|
||||
key: albumKey,
|
||||
name: album,
|
||||
tentative: true
|
||||
}
|
||||
])
|
||||
|
||||
if (insertAlbumError) {
|
||||
console.error('Error inserting album into Supabase:', insertAlbumError.message)
|
||||
return new Response(JSON.stringify({ status: 'error', message: insertAlbumError.message }), { headers: { "Content-Type": "application/json" } })
|
||||
}
|
||||
} else if (albumError) {
|
||||
console.error('Error querying album from Supabase:', albumError.message)
|
||||
return new Response(JSON.stringify({ status: 'error', message: albumError.message }), { headers: { "Content-Type": "application/json" } })
|
||||
}
|
||||
|
||||
const scrobbleData = await scrobbles.get(`${weekKey()}`, { type: 'json'})
|
||||
const windowData = await scrobbles.get('window', { type: 'json'})
|
||||
const artistUrl = (artistsMap[artistSanitizedKey]?.['mbid'] && artistsMap[artistSanitizedKey]?.['mbid'] !== '') ? `http://musicbrainz.org/artist/${artistsMap[artistSanitizedKey]?.['mbid']}` : `https://musicbrainz.org/search?query=${artist.replace(
|
||||
/\s+/g,
|
||||
'+'
|
||||
)}&type=artist`
|
||||
const { error: listenError } = await supabase.from('listens').insert([
|
||||
{
|
||||
artist_name: artist,
|
||||
album_name: album,
|
||||
track_name: track,
|
||||
listened_at: listenedAt,
|
||||
album_key: albumKey
|
||||
}
|
||||
])
|
||||
|
||||
await scrobbles.setJSON('now-playing', {...trackScrobbleData, ...{ url: artistUrl }})
|
||||
|
||||
let scrobbleUpdate = scrobbleData
|
||||
let windowUpdate = windowData;
|
||||
|
||||
if (scrobbleUpdate?.['data']) scrobbleUpdate['data'].push(trackScrobbleData)
|
||||
if (!scrobbleUpdate?.['data']) scrobbleUpdate = { data: [trackScrobbleData] }
|
||||
if (windowData?.['data']) windowUpdate['data'].push(trackScrobbleData)
|
||||
if (!windowData?.['data']) windowUpdate = { data: [trackScrobbleData] }
|
||||
windowUpdate = { data: filterOldScrobbles(windowUpdate.data) }
|
||||
|
||||
await scrobbles.setJSON(`${weekKey()}`, scrobbleUpdate)
|
||||
await scrobbles.setJSON('window', windowUpdate)
|
||||
if (listenError) {
|
||||
console.error('Error inserting data into Supabase:', listenError.message)
|
||||
return new Response(JSON.stringify({ status: 'error', message: listenError.message }), { headers: { "Content-Type": "application/json" } })
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
status: 'success',
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
)
|
||||
return new Response(JSON.stringify({ status: 'success' }), { headers: { "Content-Type": "application/json" } })
|
||||
}
|
||||
|
||||
export const config = {
|
||||
path: '/api/scrobble',
|
||||
}
|
||||
}
|
||||
|
|
2
cache/jsonfeed-to-mastodon-timestamp.json
vendored
2
cache/jsonfeed-to-mastodon-timestamp.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"timestamp": 1714881849961
|
||||
"timestamp": 1715048125452
|
||||
}
|
33
cache/jsonfeed-to-mastodon.json
vendored
33
cache/jsonfeed-to-mastodon.json
vendored
|
@ -13147,5 +13147,38 @@
|
|||
"https://social.lol/users/cory/statuses/112386496913231157"
|
||||
],
|
||||
"lastTootTimestamp": 1714881849954
|
||||
},
|
||||
"https://www.baldurbjarnason.com/2024/react-electron-llms-labour-arbitrage/": {
|
||||
"id": "aHR0cHM6Ly93d3cuYmFsZHVyYmphcm5hc29uLmNvbS8yMDI0L3JlYWN0LWVsZWN0cm9uLWxsbXMtbGFib3VyLWFyYml0cmFnZS8=",
|
||||
"title": "🔗: React, Electron, and LLMs have a common purpose: the labour arbitrage theory of dev tool popularity via Baldur Bjarnason",
|
||||
"url": "https://www.baldurbjarnason.com/2024/react-electron-llms-labour-arbitrage/",
|
||||
"content_text": "🔗: React, Electron, and LLMs have a common purpose: the labour arbitrage theory of dev tool popularity via Baldur Bjarnason #WebDev #Tech #AI https://www.baldurbjarnason.com/2024/react-electron-llms-labour-arbitrage/",
|
||||
"date_published": "Mon, 06 May 2024 15:45:00 GMT",
|
||||
"toots": [
|
||||
"https://social.lol/users/cory/statuses/112395465951324987"
|
||||
],
|
||||
"lastTootTimestamp": 1715018706606
|
||||
},
|
||||
"https://coryd.dev/posts/2024/the-tech-industry-doesnt-deserve-optimism-it-has-earned-skepticism/": {
|
||||
"id": "aHR0cHM6Ly9jb3J5ZC5kZXYvcG9zdHMvMjAyNC90aGUtdGVjaC1pbmR1c3RyeS1kb2VzbnQtZGVzZXJ2ZS1vcHRpbWlzbS1pdC1oYXMtZWFybmVkLXNrZXB0aWNpc20v",
|
||||
"title": "📝: The tech industry doesn't deserve optimism it has earned skepticism",
|
||||
"url": "https://coryd.dev/posts/2024/the-tech-industry-doesnt-deserve-optimism-it-has-earned-skepticism/",
|
||||
"content_text": "📝: The tech industry doesn't deserve optimism it has earned skepticism #Tech #AI #SocialMedia https://coryd.dev/posts/2024/the-tech-industry-doesnt-deserve-optimism-it-has-earned-skepticism/",
|
||||
"date_published": "Mon, 06 May 2024 19:14:00 GMT",
|
||||
"toots": [
|
||||
"https://social.lol/users/cory/statuses/112395608482110883"
|
||||
],
|
||||
"lastTootTimestamp": 1715020881436
|
||||
},
|
||||
"https://coryd.dev/now?ts=2024-18#artists": {
|
||||
"id": "aHR0cHM6Ly9jb3J5ZC5kZXYvbm93P3RzPTIwMjQtMTgjYXJ0aXN0cw==",
|
||||
"title": "🎧: My top artists for the week: Deftones @ 50 plays, Undergang @ 36 plays, Charmer @ 31 plays, Pig Destroyer @ 26 plays, Civerous @ 26 plays, Worst Party Ever @ 26 plays, Galvanizer @ 24 plays, Rites of Spring @ 23 plays #Music",
|
||||
"url": "https://coryd.dev/now?ts=2024-18#artists",
|
||||
"content_text": "🎧: My top artists for the week: Deftones @ 50 plays, Undergang @ 36 plays, Charmer @ 31 plays, Pig Destroyer @ 26 plays, Civerous @ 26 plays, Worst Party Ever @ 26 plays, Galvanizer @ 24 plays, Rites of Spring @ 23 plays #Music https://coryd.dev/now?ts=2024-18#artists",
|
||||
"date_published": "Tue, 30 Apr 2024 08:00:00 GMT",
|
||||
"toots": [
|
||||
"https://social.lol/users/cory/statuses/112397393943415021"
|
||||
],
|
||||
"lastTootTimestamp": 1715048125444
|
||||
}
|
||||
}
|
|
@ -144,7 +144,7 @@ export default {
|
|||
const date = new Date(entry[dateKey])
|
||||
let excerpt = ''
|
||||
let url = ''
|
||||
const feedNote = '<hr/><p>This is a full text feed, but not all content can be rendered perfeclty within the feed. If something looks off, feel free to <a href="https://coryd.dev">visit my site</a> for the original post.</p>'
|
||||
const feedNote = '<hr/><p>This is a full text feed, but not all content can be rendered perfectly within the feed. If something looks off, feel free to <a href="https://coryd.dev">visit my site</a> for the original post.</p>'
|
||||
|
||||
// set the entry url
|
||||
if (entry.url.includes('http')) url = entry.url
|
||||
|
@ -182,12 +182,12 @@ export default {
|
|||
url: item['url'],
|
||||
type: item.type
|
||||
}
|
||||
if (item.type === 'album') {
|
||||
if (item.type === 'albums') {
|
||||
normalized['title'] = item['title']
|
||||
normalized['alt'] = `${item['title']} by ${item['artist']}`
|
||||
normalized['subtext'] = `${item['artist']}`
|
||||
}
|
||||
if (item.type === 'artist') {
|
||||
if (item.type === 'artists') {
|
||||
normalized['title'] = item['title']
|
||||
normalized['alt'] = `${item['plays']} plays of ${item['title']}`
|
||||
normalized['subtext'] = `${item['plays']} plays`
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
###
|
||||
# PLUGINS
|
||||
###
|
||||
[[plugins]]
|
||||
package = "/plugins/fetch-scrobbles"
|
||||
[[plugins]]
|
||||
package = "netlify-plugin-webmentions"
|
||||
[plugins.inputs]
|
||||
|
|
152
package-lock.json
generated
152
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "coryd.dev",
|
||||
"version": "13.7.2",
|
||||
"version": "14.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "coryd.dev",
|
||||
"version": "13.7.2",
|
||||
"version": "14.0.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cdransf/api-text": "^1.2.2",
|
||||
|
@ -26,7 +26,7 @@
|
|||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
|
||||
"@11tyrocks/eleventy-plugin-lightningcss": "^1.4.0",
|
||||
"@cdransf/eleventy-plugin-tabler-icons": "^1.3.0",
|
||||
"@netlify/blobs": "^7.2.0",
|
||||
"@supabase/supabase-js": "^2.43.1",
|
||||
"dotenv-flow": "^4.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
|
@ -858,15 +858,6 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@netlify/blobs": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-7.3.0.tgz",
|
||||
"integrity": "sha512-wN/kNTZo4xjlUM/C0WILOkJbe8p4AFquSGkZEIoIcgnsx5ikp2GyqGiq1WMLee7QdbnqeIV2g2hn/PjT324E5w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.16.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -956,6 +947,80 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.64.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.2.tgz",
|
||||
"integrity": "sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz",
|
||||
"integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz",
|
||||
"integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.5.tgz",
|
||||
"integrity": "sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14",
|
||||
"@types/phoenix": "^1.5.4",
|
||||
"@types/ws": "^8.5.10",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz",
|
||||
"integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.43.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.1.tgz",
|
||||
"integrity": "sha512-A+RV50mWNtyKo6M0u4G6AOqEifQD+MoOjZcpRkPMPpEAFgMsc2dt3kBlBlR/MgZizWQgUKhsvrwKk0efc8g6Ug==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.64.2",
|
||||
"@supabase/functions-js": "2.3.1",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "1.15.2",
|
||||
"@supabase/realtime-js": "2.9.5",
|
||||
"@supabase/storage-js": "2.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
|
@ -981,6 +1046,30 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz",
|
||||
"integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz",
|
||||
"integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
|
||||
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@zachleat/webcare-webshare": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@zachleat/webcare-webshare/-/webcare-webshare-1.0.3.tgz",
|
||||
|
@ -1765,9 +1854,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.756",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz",
|
||||
"integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==",
|
||||
"version": "1.4.758",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.758.tgz",
|
||||
"integrity": "sha512-/o9x6TCdrYZBMdGeTifAP3wlF/gVT+TtWJe3BSmtNh92Mw81U9hrYwW9OAGUh+sEOX/yz5e34sksqRruZbjYrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
|
@ -3018,18 +3107,6 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
|
||||
|
@ -4314,13 +4391,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||
"version": "7.6.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz",
|
||||
"integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
|
@ -4669,6 +4743,12 @@
|
|||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
@ -4819,12 +4899,6 @@
|
|||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/youtube-video-element": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/youtube-video-element/-/youtube-video-element-1.0.1.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "coryd.dev",
|
||||
"version": "13.7.2",
|
||||
"version": "14.0.4",
|
||||
"description": "The source for my personal site. Built using 11ty.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@ -36,7 +36,7 @@
|
|||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
|
||||
"@11tyrocks/eleventy-plugin-lightningcss": "^1.4.0",
|
||||
"@cdransf/eleventy-plugin-tabler-icons": "^1.3.0",
|
||||
"@netlify/blobs": "^7.2.0",
|
||||
"@supabase/supabase-js": "^2.43.1",
|
||||
"dotenv-flow": "^4.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import { getStore, setEnvironmentContext } from '@netlify/blobs'
|
||||
import { DateTime } from 'luxon'
|
||||
import fs from 'fs'
|
||||
|
||||
const getKeys = (months = 1) => {
|
||||
const currentDate = DateTime.now()
|
||||
const weeks = Math.floor((currentDate.daysInMonth * months) / 7)
|
||||
let count = 0
|
||||
const keys = [`${currentDate.year}-${currentDate.weekNumber}`]
|
||||
|
||||
while (count < weeks) {
|
||||
const weeks = 1 * (count + 1)
|
||||
const date = DateTime.now().minus({ weeks })
|
||||
keys.push(`${date.year}-${date.weekNumber}`)
|
||||
count++;
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
const filterOldScrobbles = (scrobbles, months = 1) => {
|
||||
const currentDate = DateTime.now()
|
||||
const weeks = Math.floor((currentDate.daysInMonth * months) / 7)
|
||||
const windowEnd = DateTime.now().minus({ weeks });
|
||||
return scrobbles.filter(scrobble => {
|
||||
const timestamp = DateTime.fromISO(scrobble.timestamp);
|
||||
return timestamp >= windowEnd;
|
||||
});
|
||||
}
|
||||
|
||||
export const onPreBuild = async ({ constants }) => {
|
||||
setEnvironmentContext({
|
||||
siteID: constants.SITE_ID,
|
||||
token: constants.NETLIFY_API_TOKEN,
|
||||
})
|
||||
|
||||
const currentDate = DateTime.now()
|
||||
const lastWeek = currentDate.minus({ weeks: 1 })
|
||||
const monthKeys = getKeys()
|
||||
const monthChartData = { data: [] }
|
||||
const threeMonthKeys = getKeys(3)
|
||||
const threeMonthChartData = { data: [] }
|
||||
const yearKeys = getKeys(12)
|
||||
const yearChartData = { data: [] }
|
||||
const scrobbles = getStore('scrobbles')
|
||||
const artists = getStore('artists')
|
||||
const albums = getStore('albums')
|
||||
const weeklyChartData = await scrobbles.get(`${lastWeek.year}-${lastWeek.weekNumber}`, { type: 'json'})
|
||||
const windowData = await scrobbles.get('window', { type: 'json'})
|
||||
const artistsMap = await artists.get('artists-map', { type: 'json' })
|
||||
const albumsMap = await albums.get('albums-map', { type: 'json' })
|
||||
const nowPlaying = await scrobbles.get('now-playing', { type: 'json'})
|
||||
|
||||
for (const key of monthKeys) {
|
||||
let scrobbleData
|
||||
try {
|
||||
scrobbleData = await scrobbles.get(key, { type: 'json'})
|
||||
} catch (err) {
|
||||
console.log('Error fetching scrobble data using monthKeys')
|
||||
break
|
||||
}
|
||||
if (scrobbleData) monthChartData?.['data']?.push(...scrobbleData?.['data'])
|
||||
}
|
||||
|
||||
for (const key of threeMonthKeys) {
|
||||
let scrobbleData
|
||||
try {
|
||||
scrobbleData = await scrobbles.get(key, { type: 'json'})
|
||||
} catch (err) {
|
||||
console.log('Error fetching scrobble data using threeMonthKeys')
|
||||
break
|
||||
}
|
||||
if (scrobbleData) threeMonthChartData?.['data']?.push(...scrobbleData?.['data'])
|
||||
}
|
||||
|
||||
for (const key of yearKeys) {
|
||||
let scrobbleData
|
||||
try {
|
||||
scrobbleData = await scrobbles?.get(key, { type: 'json'})
|
||||
} catch (err) {
|
||||
console.log('Error fetching scrobble data using yearKeys')
|
||||
break
|
||||
}
|
||||
if (scrobbleData) yearChartData?.['data']?.push(...scrobbleData?.['data'])
|
||||
}
|
||||
|
||||
fs.writeFileSync('./src/_data/json/weekly-top-artists-chart.json', JSON.stringify({...weeklyChartData, timestamp: `${lastWeek.set({ hour: 8, minute: 0, second: 0, millisecond: 0 }).toMillis()}` }))
|
||||
fs.writeFileSync('./src/_data/json/scrobbles-window.json', JSON.stringify(windowData))
|
||||
fs.writeFileSync('./src/_data/json/artists-map.json', JSON.stringify(artistsMap))
|
||||
fs.writeFileSync('./src/_data/json/albums-map.json', JSON.stringify(albumsMap))
|
||||
fs.writeFileSync('./src/_data/json/now-playing.json', JSON.stringify(nowPlaying))
|
||||
fs.writeFileSync('./src/_data/json/scrobbles-month-chart.json', JSON.stringify({ data: filterOldScrobbles(monthChartData.data) }))
|
||||
fs.writeFileSync('./src/_data/json/scrobbles-three-month-chart.json', JSON.stringify({ data: filterOldScrobbles(threeMonthChartData.data, 3) }))
|
||||
fs.writeFileSync('./src/_data/json/scrobbles-year-chart.json', JSON.stringify({ data: filterOldScrobbles(yearChartData.data, 12) }))
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
name: netlify-plugin-fetch-scrobbles
|
34
plugins/fetch-scrobbles/package-lock.json
generated
34
plugins/fetch-scrobbles/package-lock.json
generated
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"name": "netlify-plugin-fetch-scrobbles",
|
||||
"version": "0.0.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "netlify-plugin-fetch-scrobbles",
|
||||
"version": "0.0.8",
|
||||
"devDependencies": {
|
||||
"@netlify/blobs": "^7.3.0",
|
||||
"luxon": "^3.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@netlify/blobs": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-7.3.0.tgz",
|
||||
"integrity": "sha512-wN/kNTZo4xjlUM/C0WILOkJbe8p4AFquSGkZEIoIcgnsx5ikp2GyqGiq1WMLee7QdbnqeIV2g2hn/PjT324E5w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.16.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"name": "netlify-plugin-fetch-scrobbles",
|
||||
"version": "0.0.8",
|
||||
"description": "netlify-plugin-fetch-scrobbles",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@netlify/blobs": "^7.3.0",
|
||||
"luxon": "^3.4.4"
|
||||
}
|
||||
}
|
91
src/_data/artists.js
Normal file
91
src/_data/artists.js
Normal 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
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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.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
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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/>`
|
||||
}]
|
||||
}
|
|
@ -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,21 +68,21 @@ 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/albumReleases.liquid", albumReleases:albumReleases %}
|
||||
{% render "partials/now/album-releases.liquid", albumReleases:albumReleases %}
|
||||
<h2 id="books" class="section-header flex-centered">
|
||||
{% tablericon "books" "Books" %}
|
||||
Books
|
||||
|
|
|
@ -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>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 0 B |
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
|
@ -201,6 +201,7 @@ hr {
|
|||
table {
|
||||
display: block;
|
||||
overflow-x: scroll;
|
||||
overscroll-behavior: contain;
|
||||
width: 100%;
|
||||
max-width: fit-content;
|
||||
margin: 0 auto;
|
||||
|
|
|
@ -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
|
||||
%}
|
|
@ -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 %}
|
|
@ -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">
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -136,7 +136,7 @@ layout: main
|
|||
{{ content }}
|
||||
{% render "partials/now/media-grid.liquid", data:artists, icon: "microphone-2", title: "Artists", shape: "square", count: 8, loading: 'eager' %}
|
||||
{% render "partials/now/media-grid.liquid", data:albums, icon: "vinyl", title: "Albums", shape: "square", count: 8, loading: 'lazy' %}
|
||||
{% render "partials/now/albumReleases.liquid", albumReleases:albumReleases %}
|
||||
{% render "partials/now/album-releases.liquid", albumReleases:albumReleases %}
|
||||
{% render "partials/now/media-grid.liquid", data:books, icon: "books", title: "Books", shape: "vertical", count: 6, loading: 'lazy' %}
|
||||
{% render "partials/now/links.liquid", links:links %}
|
||||
{% render "partials/now/media-grid.liquid", data:movies, icon: "movie", title: "Movies", shape: "vertical", count: 6, loading: 'lazy' %}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
date: 2024-05-06T11:14-08:00
|
||||
title: The tech industry doesn't deserve optimism it has earned skepticism
|
||||
description: "Take a step back look around at the tech products you use, the industry and its impact on society more broadly and ask yourself: does its track record warrant optimism or have they earned a healthy degree of skepticism?"
|
||||
tags:
|
||||
- tech
|
||||
- AI
|
||||
- 'social media'
|
||||
---
|
||||
Take a step back look around at the tech products you use, the industry and its impact on society more broadly and ask yourself: does its track record warrant optimism or have they earned a healthy degree of skepticism?<!-- excerpt -->
|
||||
|
||||
The web started out premised on and promising open connection. It delivered on that early promise when it was in its nascent form but the commercialization of the web in the form of early gatekeepers rapidly closed that off.
|
||||
|
||||
We started with protocols and were then herded into platforms that offered convenience. When they grew large enough, that convenience gave way to captivity.
|
||||
|
||||
We were promised a mobile revolution and greater connectivity as the smartphone era took hold and companies — initially — delivered on that promise. Video calls became ubiquitous, social media platforms grew their reach and landed in our pockets, we were able to capture video and photos in an instant.
|
||||
|
||||
Those social media companies, again, offered convenience and an — as well know — to good to be true promise of free and open access. We closed our blogs, got in line and ceded control of our social graphs. Drawbridges were rolled up, ads increased and nobody left — at least not in droves. Everyone is there so everyone stays.
|
||||
|
||||
Journalists and newspapers were drawn in, promised an audience and were gifted with capricious intermediaries that destroyed the profession and industry.
|
||||
|
||||
We lost our handle on what is and was true, stopped having conversations and started yelling at their representations. It became easier to shout at someone on line than it was to have a healthier discourse.
|
||||
|
||||
They took jobs that once represented viable professions, routed them through apps, took away benefits and stability, steamrolled regulators, operated services at a loss and captured users. Now we're raising prices, but not to the benefit of anyone but the tech-enabled middlemen.
|
||||
|
||||
They moved TV and movies to the internet, promised a fair price and flexible access to broad content catalogs. Now we're consolidating, raising prices and content keeps disappearing.
|
||||
|
||||
They offered musicians easier distribution and larger audiences while users' curated music collections faded away. Now we're raising prices, slashing payouts to artists and treating music as more and more of an afterthought.
|
||||
|
||||
We traded local businesses for massive ecommerce retailers. Those retailers promised convenience and precision delivery — they provided that, but at the cost of backbreaking labor and precarious employment for delivery drivers.
|
||||
|
||||
They promised an electric vehicle and transportation revolution. We delivered premium electric vehicles, grew with the help of subsidies, scrapped plans for affordable vehicles and kept over-promising while conveniently moving the goal posts.
|
||||
|
||||
They promised decentralized finance, ignored the energy and environmental costs, re-opened fossil-fueled power plants and failed to deliver much more than a series of high profile scandals and regulatory interventions. Instead of a new medium of exchange, we got volatile speculation and grift.
|
||||
|
||||
Now they're promising AI and ignoring yet more collateral damage. We're throwing piles of cache at GPUs, hardware and data centers. We're using increasingly large volumes of water. We're asserting the right to any and all data we can access. All of this while we're providing minimal productivity increases or value at scale.
|
||||
|
||||
The tech industry has made a lot of problems, it's delivered for company owners and shareholders but, as with so many things, they often externalize and downplay the harms. They call for more optimism and will gladly push a shiny new toy, app, nonsensical vision for a privatized utopia — you name it — but what they deliver, in reality, is very far removed from that vision. <strong class="highlight-text">They aren't entitled to optimism — they've earned skepticism.</strong>
|
Reference in a new issue