feat: apple music -> last.fm
This commit is contained in:
parent
fc59d929b4
commit
657e21e9cd
22 changed files with 88 additions and 1347 deletions
29
src/_data/albums.js
Normal file
29
src/_data/albums.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const EleventyFetch = require('@11ty/eleventy-fetch')
|
||||
const ALBUM_DENYLIST = ['no-love-deep-web', 'unremittance']
|
||||
|
||||
module.exports = async function () {
|
||||
const MUSIC_KEY = process.env.API_KEY_LASTFM
|
||||
const url = `https://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user=cdrn_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
}).catch()
|
||||
const data = await res
|
||||
return data['topalbums'].album.map((album) => {
|
||||
return {
|
||||
name: album['name'],
|
||||
artist: album['artist']['name'],
|
||||
plays: album['playcount'],
|
||||
rank: album['@attr']['rank'],
|
||||
image: !ALBUM_DENYLIST.includes(album['name'].replace(/\s+/g, '-').toLowerCase())
|
||||
? album['image'][album['image'].length - 1]['#text'].replace(
|
||||
'https://lastfm.freetls.fastly.net',
|
||||
'https://albums.coryd.dev'
|
||||
)
|
||||
: `https://cdn.coryd.dev/albums/${album['name'].name
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()}.jpg`,
|
||||
url: `https://musicbrainz.org/album/${album['mbid']}`,
|
||||
}
|
||||
})
|
||||
}
|
22
src/_data/artists.js
Normal file
22
src/_data/artists.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const EleventyFetch = require('@11ty/eleventy-fetch')
|
||||
|
||||
module.exports = async function () {
|
||||
const MUSIC_KEY = process.env.API_KEY_LASTFM
|
||||
const url = `https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdrn_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
}).catch()
|
||||
const data = await res
|
||||
return data['topartists'].artist.map((artist) => {
|
||||
return {
|
||||
name: artist['name'],
|
||||
plays: artist['playcount'],
|
||||
rank: artist['@attr']['rank'],
|
||||
image:
|
||||
`https://cdn.coryd.dev/artists/${artist['name'].replace(/\s+/g, '-').toLowerCase()}.jpg` ||
|
||||
'https://cdn.coryd.dev/artists/missing-artist.jpg',
|
||||
url: `https://musicbrainz.org/artist/${artist['mbid']}`,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"aliases": [
|
||||
{
|
||||
"artist": "Aesop Rock",
|
||||
"aliases": ["Aesop Rock & Homeboy Sandman", "Aesop Rock & Blockhead"]
|
||||
},
|
||||
{
|
||||
"artist": "Fen",
|
||||
"aliases": ["Sleepwalker & Fen"]
|
||||
},
|
||||
{
|
||||
"artist": "Osees",
|
||||
"aliases": ["OCS", "The Ohsees", "Thee Oh Sees", "Thee Oh See's"]
|
||||
},
|
||||
{
|
||||
"artist": "Ryan Adams",
|
||||
"aliases": ["Ryan Adams & the Cardinals"]
|
||||
},
|
||||
{
|
||||
"artist": "Thou",
|
||||
"aliases": ["Great Falls / Thou", "Moloch / Thou", "Thou & The Body"]
|
||||
},
|
||||
{
|
||||
"artist": "Tom Waits",
|
||||
"aliases": ["Tom Waits & Crystal Gayle", "Crystal Gayle"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"i.rXXXdmUa6Nme-1689970612847": {
|
||||
"name": "Sacrificial Blood Oath In The Temple Of K'zadu",
|
||||
"artist": "Gateway",
|
||||
"album": "Galgendood",
|
||||
"art": "https://store-033.blobstore.apple.com/sq-mq-us-033-000002/18/f1/a3/18f1a37a-8c9a-169a-5458-464aea20ce05/image?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230721T202228Z&X-Amz-SignedHeaders=host&X-Amz-Expires=86400&X-Amz-Credential=MKIAU0HKO2RBEAT0UMZS%2F20230721%2Fstore-033%2Fs3%2Faws4_request&X-Amz-Signature=85790600221880597074559ed3674564f17ca3df6634d6fa15496baf7aca5d56",
|
||||
"url": "https://rateyourmusic.com/search?searchtype=l&searchterm=Galgendood%20Gateway",
|
||||
"id": "i.rXXXdmUa6Nme",
|
||||
"playTime": 1689970612847,
|
||||
"duration": 338808
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"words": [
|
||||
"a",
|
||||
"and",
|
||||
"but",
|
||||
"an",
|
||||
"for",
|
||||
"if",
|
||||
"in",
|
||||
"is",
|
||||
"it",
|
||||
"nor",
|
||||
"of",
|
||||
"or",
|
||||
"so",
|
||||
"the",
|
||||
"yet"
|
||||
],
|
||||
"artists": ["NoMeansNo"]
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3')
|
||||
const _ = require('lodash')
|
||||
const mockedMusic = require('./json/mocks/music.json')
|
||||
const { getReadableData } = require('../utils/aws')
|
||||
const { aliasArtist, sanitizeMedia } = require('../utils/media')
|
||||
const { titleCase } = require('../utils/grammar')
|
||||
|
||||
const diffTracks = (cache, tracks) => {
|
||||
const trackCompareSet = Object.values(tracks)
|
||||
const cacheCompareSet = _.orderBy(Object.values(cache), ['time'], ['desc'])
|
||||
const diffedTracks = {}
|
||||
const comparedTracks = _.differenceWith(trackCompareSet, cacheCompareSet, (a, b) =>
|
||||
_.isEqual(a.id, b.id)
|
||||
)
|
||||
|
||||
for (let i = 0; i < comparedTracks.length; i++)
|
||||
diffedTracks[`${comparedTracks[i]?.id}-${comparedTracks[i].playTime}`] = comparedTracks[i]
|
||||
|
||||
return diffedTracks
|
||||
}
|
||||
|
||||
const formatTracks = (tracks) => {
|
||||
let formattedTracks = {}
|
||||
let time = new Date().getTime()
|
||||
|
||||
Object.values(tracks).forEach((track) => {
|
||||
const artistFormatted = titleCase(aliasArtist(track.attributes['artistName']))
|
||||
const albumFormatted = titleCase(sanitizeMedia(track.attributes['albumName']))
|
||||
const trackFormatted = sanitizeMedia(track.attributes['name'])
|
||||
formattedTracks[`${track.id}-${time}`] = {
|
||||
name: trackFormatted,
|
||||
artist: artistFormatted,
|
||||
album: albumFormatted,
|
||||
genre: track['relationships']?.['library'].data[0]?.attributes['genreNames'][0] || '',
|
||||
art: track.attributes.artwork.url.replace('{w}', '300').replace('{h}', '300'),
|
||||
url:
|
||||
track['relationships'] && track['relationships'].albums.data.length > 0
|
||||
? `https://song.link/${track['relationships'].albums.data.pop().attributes.url}`
|
||||
: `https://rateyourmusic.com/search?searchtype=l&searchterm=${encodeURI(
|
||||
albumFormatted
|
||||
)}%20${encodeURI(artistFormatted)}`,
|
||||
id: track.id,
|
||||
playTime: time - parseInt(track.attributes['durationInMillis']),
|
||||
duration: parseInt(track.attributes['durationInMillis']),
|
||||
}
|
||||
})
|
||||
return formattedTracks
|
||||
}
|
||||
|
||||
const deriveCharts = (tracks) => {
|
||||
const charts = {
|
||||
artists: {},
|
||||
albums: {},
|
||||
}
|
||||
|
||||
Object.values(tracks).forEach((track) => {
|
||||
if (!charts.artists[track.artist]) {
|
||||
charts.artists[track.artist] = {
|
||||
artist: track.artist,
|
||||
genre: track.genre,
|
||||
url: `https://rateyourmusic.com/search?searchterm=${encodeURI(track.artist)}`,
|
||||
plays: 1,
|
||||
}
|
||||
} else {
|
||||
charts.artists[track.artist].plays++
|
||||
}
|
||||
|
||||
if (!charts.albums[track.album]) {
|
||||
charts.albums[track.album] = {
|
||||
name: track.album,
|
||||
artist: track.artist,
|
||||
art: track.art,
|
||||
url: track.url,
|
||||
plays: 1,
|
||||
}
|
||||
} else {
|
||||
charts.albums[track.album].plays++
|
||||
}
|
||||
})
|
||||
|
||||
return charts
|
||||
}
|
||||
|
||||
module.exports = async function () {
|
||||
const client = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId: process.env.ACCESS_KEY_WASABI,
|
||||
secretAccessKey: process.env.SECRET_KEY_WASABI,
|
||||
},
|
||||
endpoint: {
|
||||
url: 'https://s3.us-west-1.wasabisys.com',
|
||||
},
|
||||
region: 'us-west-1',
|
||||
})
|
||||
const WASABI_BUCKET = process.env.BUCKET_WASABI
|
||||
const APPLE_BEARER = process.env.API_BEARER_APPLE_MUSIC
|
||||
const APPLE_MUSIC_TOKEN = process.env.API_TOKEN_APPLE_MUSIC
|
||||
const APPLE_TOKEN_RESPONSE = await fetch(process.env.APPLE_RENEW_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${APPLE_BEARER}`,
|
||||
'X-Apple-Music-User-Token': APPLE_MUSIC_TOKEN,
|
||||
},
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.catch()
|
||||
|
||||
const DATE = new Date()
|
||||
DATE.setDate(DATE.getDate() + ((7 - DATE.getDay()) % 7))
|
||||
const DATE_STAMP = `${DATE.getFullYear()}-${DATE.getDate()}-${DATE.getMonth()}`
|
||||
|
||||
const APPLE_TOKEN = APPLE_TOKEN_RESPONSE['music-token']
|
||||
const PAGE_SIZE = 30
|
||||
const PAGES = 10
|
||||
|
||||
let charts
|
||||
let CURRENT_PAGE = 0
|
||||
let hasNextPage = true
|
||||
let res = []
|
||||
let cachedTracks = mockedMusic
|
||||
|
||||
while (CURRENT_PAGE < PAGES && hasNextPage) {
|
||||
const URL = `https://api.music.apple.com/v1/me/recent/played/tracks?limit=${PAGE_SIZE}&offset=${
|
||||
PAGE_SIZE * CURRENT_PAGE
|
||||
}&include[songs]=albums,library&extend=artistUrl`
|
||||
const tracks = await fetch(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${APPLE_BEARER}`,
|
||||
'music-user-token': `${APPLE_TOKEN}`,
|
||||
},
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.catch()
|
||||
if (!tracks.next) hasNextPage = false
|
||||
if (tracks.data.length) res = [...res, ...tracks.data]
|
||||
CURRENT_PAGE++
|
||||
}
|
||||
|
||||
if (process.env.ELEVENTY_PRODUCTION === 'true') {
|
||||
try {
|
||||
const cachedTracksOutput = await client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: WASABI_BUCKET,
|
||||
Key: `${DATE_STAMP}-music-history.json`,
|
||||
})
|
||||
)
|
||||
const cachedTracksData = getReadableData(cachedTracksOutput.Body)
|
||||
cachedTracks = await cachedTracksData.then((tracks) => JSON.parse(tracks)).catch()
|
||||
} catch (e) {
|
||||
console.log('No cached tracks')
|
||||
cachedTracks = {}
|
||||
}
|
||||
}
|
||||
|
||||
const diffedTracks = diffTracks(cachedTracks, formatTracks(res))
|
||||
const updatedCache = {
|
||||
...cachedTracks,
|
||||
...diffedTracks,
|
||||
}
|
||||
|
||||
charts = deriveCharts(updatedCache)
|
||||
charts.artists = _.orderBy(Object.values(charts.artists), ['plays'], ['desc']).splice(0, 8)
|
||||
charts.albums = _.orderBy(Object.values(charts.albums), ['plays'], ['desc']).splice(0, 8)
|
||||
|
||||
if (!_.isEmpty(diffedTracks) && process.env.ELEVENTY_PRODUCTION === 'true') {
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: WASABI_BUCKET,
|
||||
Key: `${DATE_STAMP}-music-history.json`,
|
||||
Body: JSON.stringify(updatedCache),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return charts
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
module.exports = async function () {
|
||||
return {
|
||||
"name": "Cory Dransfeldt",
|
||||
"email": "hi@coryd.dev",
|
||||
"url": "https://coryd.dev",
|
||||
"logo": "https://coryd.dev/assets/img/logo.webp",
|
||||
"title": "Cory Dransfeldt",
|
||||
"description": "I'm a software developer in Camarillo, California. I enjoy hanging out with my beautiful family and 4 rescue dogs, technology, automation, music, writing, reading and tv and movies.",
|
||||
"letterboxd-host": "https://a.ltrbxd.com",
|
||||
"cdn-movies": "https://movies.coryd.dev"
|
||||
name: 'Cory Dransfeldt',
|
||||
email: 'hi@coryd.dev',
|
||||
url: 'https://coryd.dev',
|
||||
logo: 'https://coryd.dev/assets/img/logo.webp',
|
||||
title: 'Cory Dransfeldt',
|
||||
description:
|
||||
"I'm a software developer in Camarillo, California. I enjoy hanging out with my beautiful family and 4 rescue dogs, technology, automation, music, writing, reading and tv and movies.",
|
||||
'letterboxd-host': 'https://a.ltrbxd.com',
|
||||
'cdn-movies': 'https://movies.coryd.dev',
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue