initial commit

This commit is contained in:
jane 2021-12-01 21:36:11 -05:00
commit 05112e3d6e
10 changed files with 359 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
data/*
node_modules/
pnpm-lock.yaml
.env

12
commands/api.js Normal file
View file

@ -0,0 +1,12 @@
import { callApi } from "../lib/lastfm.js"
const api = (username, method, args, options) => {
callApi(method, {
username: username,
...JSON.parse(args)
}).then((data) => {
console.log(data)
})
}
export default api

115
commands/process.js Normal file
View file

@ -0,0 +1,115 @@
import { loadAllData, mkSubDir, saveData } from "../lib/dataStore.js"
const process = async (username, options) => {
let data = {}
let processed = {
tracks: {},
listenTime: {
track: {},
album: {},
artist: {},
sortedTracks: [],
sortedAlbums: [],
sortedArtists: []
}
}
if (options.tracks) {
data.tracks = loadAllData(username, 'tracks')
// track-only data
for (let page in data.tracks) {
console.log(page, data.tracks[page])
if (!data.tracks[page].toptracks) continue;
let currentPage = data.tracks[page].toptracks.track
// console.log(currentPage)
for (let track of currentPage) {
console.log('processing track', track.name)
processed.tracks[track.mbid] = {
playcount: track.playcount,
name: track.name,
artist: track.artist.name
}
if (track.duration && parseInt(track.duration)) {
console.log('track has duration', track.duration)
let listenTime = parseInt(track.duration) * parseInt(track.playcount)
console.log('track listen time is', listenTime)
processed.listenTime.track[track.mbid] = {
seconds: listenTime,
minutes: listenTime / 60
}
console.log(track.mbid, processed.listenTime.track[track.mbid])
let key = ''
if (track.artist.mbid) { key = track.artist.mbid }
else { key = 'N-' + track.artist.name }
if (!processed.listenTime.artist[key]) {
processed.listenTime.artist[key] = {
seconds: listenTime,
minutes: listenTime / 60,
name: track.artist.name
}
}
else {
processed.listenTime.artist[key].seconds += listenTime
processed.listenTime.artist[key].minutes =
processed.listenTime.artist[key].seconds / 60
}
}
else {
processed.listenTime.track[track.mbid] = {
playcount: track.playcount
}
}
}
}
for (let track in processed.listenTime.tracks) {
processed.listenTime.sortedTracks.push({
mbid: track,
...processed.listenTime.track[track]
})
}
processed.listenTime.sortedTracks.sort(
(a, b) => {
return b.seconds - a.seconds
}
)
for (let artist in processed.listenTime.artist) {
processed.listenTime.sortedArtists.push(
{
mbid: artist,
...processed.listenTime.artist[artist]
}
)
}
processed.listenTime.sortedArtists.sort(
(a, b) => {
return b.seconds - a.seconds
}
)
}
if (options.albums) {
data.albums = loadAllData(username, 'albums')
for (let page in data.albums) {
let currentPage = data.albums[page].topalbums.album
}
}
if (options.artists) {
data.artists = loadAllData(username, 'artists')
for (let page in data.artists) {
let currentPage = data.artists[page].topartists.artist
}
}
console.log('saving processed data')
mkSubDir(username, 'processed')
saveData(username, 'processed/gross', processed)
saveData(username, 'processed/tracks', processed.tracks)
saveData(username, 'processed/listenTime', processed.listenTime)
}
export default process

105
commands/retrieve.js Normal file
View file

@ -0,0 +1,105 @@
import { callApi } from '../lib/lastfm.js'
import { hasData, mkSubDir, saveData } from '../lib/dataStore.js'
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const retrieve = async (username, options) => {
callApi("user.getinfo", { user: username }).then((data) => {
saveData(username, "basic_info", data)
})
if (!options.basic) {
await retrieveTracks(username, options)
await retrieveAlbums(username, options)
await retrieveArtists(username, options)
}
}
const retrieveTracks = async (username, options) => {
let period = '12month'
if (options.full) {
period = 'overall'
}
let pages = 0;
mkSubDir(username, 'tracks')
if (hasData(username, "tracks/tracks-1")) {
console.log('already have tracks');
return;
}
console.log('downloading page 1 of ???')
let data = await callApi("user.getTopTracks", {
user: username
})
pages = data.toptracks['@attr'].totalPages
saveData(username, "tracks/tracks-1", data)
for (let i = 2; i <= pages; i++) {
await sleep(500)
console.log(`downloading page ${i} of ${pages}`)
let data = await callApi("user.getTopTracks", {
user: username,
page: i
})
saveData(username, `tracks/tracks-${i}`, data)
}
}
const retrieveAlbums = async (username, options) => {
let period = '12month'
if (options.full) {
period = 'overall'
}
let pages = 0;
mkSubDir(username, 'albums')
if (hasData(username, "albums/albums-1")) {
console.log('already have albums');
return;
}
console.log('downloading page 1 of ???')
let data = await callApi("user.getTopAlbums", {
user: username
})
pages = data.topalbums['@attr'].totalPages
saveData(username, "albums/albums-1", data)
for (let i = 2; i <= pages; i++) {
await sleep(500)
console.log(`downloading page ${i} of ${pages}`)
let data = await callApi("user.getTopAlbums", {
user: username,
page: i
})
saveData(username, `albums/albums-${i}`, data)
}
}
const retrieveArtists = async (username, options) => {
let period = '12month'
if (options.full) {
period = 'overall'
}
let pages = 0;
mkSubDir(username, 'artists')
if (hasData(username, "artists/artists-1")) {
console.log('already have artists');
return;
}
console.log('downloading page 1 of ???')
let data = await callApi("user.getTopArtists", {
user: username
})
pages = data.topartists['@attr'].totalPages
saveData(username, "artists/artists-1", data)
for (let i = 2; i <= pages; i++) {
await sleep(500)
console.log(`downloading page ${i} of ${pages}`)
let data = await callApi("user.getTopArtists", {
user: username,
page: i
})
saveData(username, `artists/artists-${i}`, data)
}
}
export default retrieve

35
index.js Normal file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env node
import cli from 'commander'
import retrieve from './commands/retrieve.js'
import api from './commands/api.js'
import process from './commands/process.js'
import dotenv from 'dotenv'
dotenv.config()
cli.description("Last.fm Wrapper")
cli.name("wrapper")
cli.addHelpCommand(false)
cli.helpOption(true)
cli.command("retrieve")
.argument("username", "The username for which to download data.")
.option("-b, --basic", "Download only basic user-data")
.option("-f, --full", "Download listening data for previous years.")
.action(retrieve)
cli.command('process')
.argument('username', "")
.option("-t, --tracks", "")
.option("-al, --albums", "")
.option("-ar, --artists", "")
.action(process)
cli.command("api")
.argument("username", "")
.argument("method", "")
.argument("args", "")
.action(api)
// must ALWAYS be at the bottom
cli.parse(process.argv)

44
lib/dataStore.js Normal file
View file

@ -0,0 +1,44 @@
import 'fs'
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs'
export const mkSubDir = (username, subdir) => {
if (!existsSync(`./data/${username}`)) {
mkdirSync(`./data/${username}`)
}
if (!existsSync(`./data/${username}/${subdir}`)) {
mkdirSync(`./data/${username}/${subdir}`)
}
}
export const hasData = (username, dataFile) => {
return existsSync(`./data/${username}/${dataFile}.json`)
}
export const saveData = (username, dataFile, object) => {
if (!existsSync('./data')) {
mkdirSync('./data')
}
if (!existsSync(`./data/${username}`)) {
mkdirSync(`./data/${username}`)
}
// console.log(dataFile, object, JSON.stringify(object))
writeFileSync(`./data/${username}/${dataFile}.json`, JSON.stringify(object))
}
export const loadData = (username, dataFile) => {
let data = undefined;
if (existsSync(`./data/${username}/${dataFile}.json`)) {
data = JSON.parse(readFileSync(`./data/${username}/${dataFile}.json`))
}
return data;
}
export const loadAllData = (username, subdir) => {
let files = readdirSync(`./data/${username}/${subdir}`)
let data = []
for (let file of files) {
let parsed = JSON.parse(readFileSync(`./data/${username}/${subdir}/${file}`))
data.push(parsed)
}
return data
}

15
lib/lastfm.js Normal file
View file

@ -0,0 +1,15 @@
import fetch from 'node-fetch'
const API_URL = "http://ws.audioscrobbler.com/2.0/"
export const callApi = async (method, params = {}) => {
let url = `${API_URL}?method=${method}&api_key=${process.env.API_KEY}&format=json`
if (params != undefined) {
for (let [key, value] of Object.entries(params)) {
url += `&${key}=${value}`
}
}
let result = await fetch(url)
return result.json();
}

21
package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "lastfm-wrapped",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"bin": {
"wrapper": "index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^8.3.0",
"dotenv": "^10.0.0",
"node-fetch": "^3.1.0"
}
}

3
run.sh Executable file
View file

@ -0,0 +1,3 @@
#! /usr/bin/env nix-shell
#! nix-shell -i real-interpreter -p nodejs nodePackages.pnpm
pnpm start

5
shell.nix Normal file
View file

@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = [ pkgs.nodejs pkgs.nodePackages.pnpm ];
}