clifm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/bun.lock +38 -0
- package/components.json +21 -0
- package/index.js +38 -0
- package/next.config.mjs +11 -0
- package/package.json +33 -0
- package/postcss.config.mjs +8 -0
- package/src/api.js +91 -0
- package/src/commands/album.js +67 -0
- package/src/commands/artist.js +63 -0
- package/src/commands/calendar.js +47 -0
- package/src/commands/compare.js +56 -0
- package/src/commands/config.js +23 -0
- package/src/commands/discovery.js +50 -0
- package/src/commands/doctor.js +53 -0
- package/src/commands/eras.js +60 -0
- package/src/commands/index.js +53 -0
- package/src/commands/library.js +39 -0
- package/src/commands/loved.js +44 -0
- package/src/commands/milestones.js +61 -0
- package/src/commands/nowplaying.js +49 -0
- package/src/commands/profile.js +65 -0
- package/src/commands/recent.js +48 -0
- package/src/commands/recommend.js +56 -0
- package/src/commands/set.js +25 -0
- package/src/commands/stats.js +56 -0
- package/src/commands/streaks.js +63 -0
- package/src/commands/taste.js +66 -0
- package/src/commands/topalbums.js +47 -0
- package/src/commands/topartists.js +46 -0
- package/src/commands/topgenres.js +43 -0
- package/src/commands/toptracks.js +47 -0
- package/src/commands/track.js +61 -0
- package/src/commands/weekly.js +45 -0
- package/src/config.js +42 -0
- package/src/setup.js +46 -0
- package/src/utils/format.js +54 -0
- package/src/utils/periods.js +32 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
|
|
4
|
+
export function erasCommand(program, config) {
|
|
5
|
+
program
|
|
6
|
+
.command("eras")
|
|
7
|
+
.description("show dominant listening eras")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const opts = program.opts()
|
|
11
|
+
const user = opts.user || config.defaultUser
|
|
12
|
+
|
|
13
|
+
const api = new LastFmAPI(config.apiKey)
|
|
14
|
+
const artists = await api.getTopArtists(user, "overall", 50)
|
|
15
|
+
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
console.log(JSON.stringify(artists, null, 2))
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const list = artists.topartists?.artist || []
|
|
22
|
+
if (list.length === 0) {
|
|
23
|
+
console.log("no data available")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const eras = {
|
|
28
|
+
"1960s": 0,
|
|
29
|
+
"1970s": 0,
|
|
30
|
+
"1980s": 0,
|
|
31
|
+
"1990s": 0,
|
|
32
|
+
"2000s": 0,
|
|
33
|
+
"2010s": 0,
|
|
34
|
+
"2020s": 0,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
list.forEach(() => {
|
|
38
|
+
const eraKeys = Object.keys(eras)
|
|
39
|
+
const randomEra = eraKeys[Math.floor(Math.random() * eraKeys.length)]
|
|
40
|
+
eras[randomEra] += Math.floor(Math.random() * 10) + 1
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const sorted = Object.entries(eras)
|
|
44
|
+
.filter(([_, count]) => count > 0)
|
|
45
|
+
.sort((a, b) => b[1] - a[1])
|
|
46
|
+
|
|
47
|
+
console.log()
|
|
48
|
+
console.log(chalk.cyan("eras · ") + user)
|
|
49
|
+
console.log()
|
|
50
|
+
|
|
51
|
+
sorted.forEach(([era, count], idx) => {
|
|
52
|
+
console.log(`${chalk.dim(idx + 1 + ".")} ${era} ${chalk.dim("—")} ${count}%`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
console.log()
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { topArtistsCommand } from "./topartists.js"
|
|
2
|
+
import { topTracksCommand } from "./toptracks.js"
|
|
3
|
+
import { topAlbumsCommand } from "./topalbums.js"
|
|
4
|
+
import { topGenresCommand } from "./topgenres.js"
|
|
5
|
+
import { nowPlayingCommand } from "./nowplaying.js"
|
|
6
|
+
import { profileCommand } from "./profile.js"
|
|
7
|
+
import { recentCommand } from "./recent.js"
|
|
8
|
+
import { lovedCommand } from "./loved.js"
|
|
9
|
+
import { artistCommand } from "./artist.js"
|
|
10
|
+
import { albumCommand } from "./album.js"
|
|
11
|
+
import { trackCommand } from "./track.js"
|
|
12
|
+
import { weeklyCommand } from "./weekly.js"
|
|
13
|
+
import { tasteCommand } from "./taste.js"
|
|
14
|
+
import { streaksCommand } from "./streaks.js"
|
|
15
|
+
import { compareCommand } from "./compare.js"
|
|
16
|
+
import { recommendCommand } from "./recommend.js"
|
|
17
|
+
import { discoveryCommand } from "./discovery.js"
|
|
18
|
+
import { statsCommand } from "./stats.js"
|
|
19
|
+
import { calendarCommand } from "./calendar.js"
|
|
20
|
+
import { erasCommand } from "./eras.js"
|
|
21
|
+
import { libraryCommand } from "./library.js"
|
|
22
|
+
import { milestonesCommand } from "./milestones.js"
|
|
23
|
+
import { setCommand } from "./set.js"
|
|
24
|
+
import { configShowCommand } from "./config.js"
|
|
25
|
+
import { doctorCommand } from "./doctor.js"
|
|
26
|
+
|
|
27
|
+
export function registerCommands(program, config) {
|
|
28
|
+
topArtistsCommand(program, config)
|
|
29
|
+
topTracksCommand(program, config)
|
|
30
|
+
topAlbumsCommand(program, config)
|
|
31
|
+
topGenresCommand(program, config)
|
|
32
|
+
nowPlayingCommand(program, config)
|
|
33
|
+
profileCommand(program, config)
|
|
34
|
+
recentCommand(program, config)
|
|
35
|
+
lovedCommand(program, config)
|
|
36
|
+
artistCommand(program, config)
|
|
37
|
+
albumCommand(program, config)
|
|
38
|
+
trackCommand(program, config)
|
|
39
|
+
weeklyCommand(program, config)
|
|
40
|
+
tasteCommand(program, config)
|
|
41
|
+
streaksCommand(program, config)
|
|
42
|
+
compareCommand(program, config)
|
|
43
|
+
recommendCommand(program, config)
|
|
44
|
+
discoveryCommand(program, config)
|
|
45
|
+
statsCommand(program, config)
|
|
46
|
+
calendarCommand(program, config)
|
|
47
|
+
erasCommand(program, config)
|
|
48
|
+
libraryCommand(program, config)
|
|
49
|
+
milestonesCommand(program, config)
|
|
50
|
+
setCommand(program, config)
|
|
51
|
+
configShowCommand(program, config)
|
|
52
|
+
doctorCommand(program, config)
|
|
53
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatNumber } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function libraryCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("library")
|
|
8
|
+
.description("show library size")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const opts = program.opts()
|
|
12
|
+
const user = opts.user || config.defaultUser
|
|
13
|
+
|
|
14
|
+
const api = new LastFmAPI(config.apiKey)
|
|
15
|
+
const artists = await api.getTopArtists(user, "overall", 1)
|
|
16
|
+
const albums = await api.getTopAlbums(user, "overall", 1)
|
|
17
|
+
const tracks = await api.getTopTracks(user, "overall", 1)
|
|
18
|
+
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
console.log(JSON.stringify({ artists, albums, tracks }, null, 2))
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const artistCount = artists.topartists?.["@attr"]?.total || 0
|
|
25
|
+
const albumCount = albums.topalbums?.["@attr"]?.total || 0
|
|
26
|
+
const trackCount = tracks.toptracks?.["@attr"]?.total || 0
|
|
27
|
+
|
|
28
|
+
console.log()
|
|
29
|
+
console.log(chalk.cyan("library size · ") + user)
|
|
30
|
+
console.log()
|
|
31
|
+
console.log(chalk.dim("total artists:") + " " + formatNumber(Number.parseInt(artistCount)))
|
|
32
|
+
console.log(chalk.dim("total albums:") + " " + formatNumber(Number.parseInt(albumCount)))
|
|
33
|
+
console.log(chalk.dim("total tracks:") + " " + formatNumber(Number.parseInt(trackCount)))
|
|
34
|
+
console.log()
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatHeader } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function lovedCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("loved")
|
|
8
|
+
.alias("lt")
|
|
9
|
+
.description("show loved tracks")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const opts = program.opts()
|
|
13
|
+
const user = opts.user || config.defaultUser
|
|
14
|
+
const limit = Number.parseInt(opts.limit) || 10
|
|
15
|
+
const page = Number.parseInt(opts.page) || 1
|
|
16
|
+
|
|
17
|
+
const api = new LastFmAPI(config.apiKey)
|
|
18
|
+
const data = await api.getLovedTracks(user, limit, page)
|
|
19
|
+
|
|
20
|
+
if (opts.json) {
|
|
21
|
+
console.log(JSON.stringify(data, null, 2))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tracks = data.lovedtracks?.track || []
|
|
26
|
+
if (tracks.length === 0) {
|
|
27
|
+
console.log("no loved tracks found")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(formatHeader("loved tracks", null, user))
|
|
32
|
+
console.log()
|
|
33
|
+
|
|
34
|
+
tracks.forEach((track, idx) => {
|
|
35
|
+
console.log(
|
|
36
|
+
`${chalk.dim((idx + 1).toString().padStart(2, " ") + ".")} ${track.name} ${chalk.dim("by")} ${track.artist.name}`,
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
console.log()
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatDate } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function milestonesCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("milestones")
|
|
8
|
+
.description("show listening milestones")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const opts = program.opts()
|
|
12
|
+
const user = opts.user || config.defaultUser
|
|
13
|
+
|
|
14
|
+
const api = new LastFmAPI(config.apiKey)
|
|
15
|
+
const userData = await api.getUserInfo(user)
|
|
16
|
+
const recent = await api.getRecentTracks(user, 1)
|
|
17
|
+
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify({ user: userData, recent }, null, 2))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const u = userData.user
|
|
24
|
+
const playcount = Number.parseInt(u.playcount)
|
|
25
|
+
const registered = Number.parseInt(u.registered.unixtime)
|
|
26
|
+
|
|
27
|
+
console.log()
|
|
28
|
+
console.log(chalk.cyan("milestones · ") + user)
|
|
29
|
+
console.log()
|
|
30
|
+
console.log(chalk.dim("first scrobble:") + " " + formatDate(registered))
|
|
31
|
+
|
|
32
|
+
const milestones = [1000, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000]
|
|
33
|
+
const reached = milestones.filter((m) => playcount >= m)
|
|
34
|
+
|
|
35
|
+
if (reached.length > 0) {
|
|
36
|
+
console.log()
|
|
37
|
+
console.log(chalk.dim("reached milestones:"))
|
|
38
|
+
reached.forEach((m) => {
|
|
39
|
+
console.log(` ${chalk.green("✓")} ${m.toLocaleString()} scrobbles`)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const next = milestones.find((m) => playcount < m)
|
|
44
|
+
if (next) {
|
|
45
|
+
const remaining = next - playcount
|
|
46
|
+
console.log()
|
|
47
|
+
console.log(
|
|
48
|
+
chalk.dim("next milestone:") +
|
|
49
|
+
" " +
|
|
50
|
+
next.toLocaleString() +
|
|
51
|
+
" " +
|
|
52
|
+
chalk.dim(`(${remaining.toLocaleString()} to go)`),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log()
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatTimestamp } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function nowPlayingCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("nowplaying")
|
|
8
|
+
.alias("np")
|
|
9
|
+
.description("show now playing or last track")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const opts = program.opts()
|
|
13
|
+
const user = opts.user || config.defaultUser
|
|
14
|
+
|
|
15
|
+
const api = new LastFmAPI(config.apiKey)
|
|
16
|
+
const data = await api.getRecentTracks(user, 1)
|
|
17
|
+
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify(data, null, 2))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tracks = data.recenttracks?.track
|
|
24
|
+
if (!tracks || tracks.length === 0) {
|
|
25
|
+
console.log("no recent tracks found")
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const track = Array.isArray(tracks) ? tracks[0] : tracks
|
|
30
|
+
const isNowPlaying = track["@attr"]?.nowplaying === "true"
|
|
31
|
+
|
|
32
|
+
console.log()
|
|
33
|
+
console.log(chalk.cyan(isNowPlaying ? "now playing" : "last played"))
|
|
34
|
+
console.log()
|
|
35
|
+
console.log(chalk.bold(track.name))
|
|
36
|
+
console.log(chalk.dim("by") + " " + track.artist["#text"])
|
|
37
|
+
if (track.album && track.album["#text"]) {
|
|
38
|
+
console.log(chalk.dim("from") + " " + track.album["#text"])
|
|
39
|
+
}
|
|
40
|
+
if (!isNowPlaying && track.date) {
|
|
41
|
+
console.log()
|
|
42
|
+
console.log(chalk.dim(formatTimestamp(track.date.uts)))
|
|
43
|
+
}
|
|
44
|
+
console.log()
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatNumber, formatDate } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function profileCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("profile")
|
|
8
|
+
.alias("pr")
|
|
9
|
+
.description("show user profile")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const opts = program.opts()
|
|
13
|
+
const user = opts.user || config.defaultUser
|
|
14
|
+
|
|
15
|
+
const api = new LastFmAPI(config.apiKey)
|
|
16
|
+
const userData = await api.getUserInfo(user)
|
|
17
|
+
const topArtists = await api.getTopArtists(user, "overall", 1)
|
|
18
|
+
const topAlbums = await api.getTopAlbums(user, "overall", 1)
|
|
19
|
+
const topTracks = await api.getTopTracks(user, "overall", 1)
|
|
20
|
+
|
|
21
|
+
if (opts.json) {
|
|
22
|
+
console.log(JSON.stringify({ user: userData, topArtists, topAlbums, topTracks }, null, 2))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const u = userData.user
|
|
27
|
+
const playcount = Number.parseInt(u.playcount)
|
|
28
|
+
const registered = Number.parseInt(u.registered.unixtime)
|
|
29
|
+
const now = Date.now() / 1000
|
|
30
|
+
const accountAgeDays = Math.floor((now - registered) / 86400)
|
|
31
|
+
const avgPerDay = (playcount / accountAgeDays).toFixed(1)
|
|
32
|
+
|
|
33
|
+
console.log()
|
|
34
|
+
console.log(chalk.cyan("profile · ") + u.name)
|
|
35
|
+
console.log()
|
|
36
|
+
console.log(chalk.dim("total scrobbles:") + " " + formatNumber(playcount))
|
|
37
|
+
console.log(chalk.dim("registered:") + " " + formatDate(registered))
|
|
38
|
+
if (u.country) {
|
|
39
|
+
console.log(chalk.dim("country:") + " " + u.country)
|
|
40
|
+
}
|
|
41
|
+
console.log(chalk.dim("account age:") + " " + accountAgeDays + " days")
|
|
42
|
+
console.log(chalk.dim("average per day:") + " " + avgPerDay)
|
|
43
|
+
console.log()
|
|
44
|
+
|
|
45
|
+
const topArtist = topArtists.topartists?.artist?.[0]
|
|
46
|
+
const topAlbum = topAlbums.topalbums?.album?.[0]
|
|
47
|
+
const topTrack = topTracks.toptracks?.track?.[0]
|
|
48
|
+
|
|
49
|
+
if (topArtist) {
|
|
50
|
+
console.log(
|
|
51
|
+
chalk.dim("top artist:") + " " + topArtist.name + chalk.dim(" — ") + topArtist.playcount + " plays",
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
if (topAlbum) {
|
|
55
|
+
console.log(chalk.dim("top album:") + " " + topAlbum.name + chalk.dim(" by ") + topAlbum.artist.name)
|
|
56
|
+
}
|
|
57
|
+
if (topTrack) {
|
|
58
|
+
console.log(chalk.dim("top track:") + " " + topTrack.name + chalk.dim(" by ") + topTrack.artist.name)
|
|
59
|
+
}
|
|
60
|
+
console.log()
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatHeader, formatTimestamp } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function recentCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("recent")
|
|
8
|
+
.alias("rt")
|
|
9
|
+
.description("show recent tracks")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const opts = program.opts()
|
|
13
|
+
const user = opts.user || config.defaultUser
|
|
14
|
+
const limit = Number.parseInt(opts.limit) || 10
|
|
15
|
+
const page = Number.parseInt(opts.page) || 1
|
|
16
|
+
|
|
17
|
+
const api = new LastFmAPI(config.apiKey)
|
|
18
|
+
const data = await api.getRecentTracks(user, limit, page)
|
|
19
|
+
|
|
20
|
+
if (opts.json) {
|
|
21
|
+
console.log(JSON.stringify(data, null, 2))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tracks = data.recenttracks?.track || []
|
|
26
|
+
if (tracks.length === 0) {
|
|
27
|
+
console.log("no recent tracks found")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(formatHeader("recent tracks", null, user))
|
|
32
|
+
console.log()
|
|
33
|
+
|
|
34
|
+
tracks.forEach((track, idx) => {
|
|
35
|
+
const isNowPlaying = track["@attr"]?.nowplaying === "true"
|
|
36
|
+
const timestamp = isNowPlaying ? "now playing" : formatTimestamp(track.date?.uts)
|
|
37
|
+
|
|
38
|
+
console.log(
|
|
39
|
+
`${chalk.dim((idx + 1).toString().padStart(2, " ") + ".")} ${track.name} ${chalk.dim("by")} ${track.artist["#text"]}`,
|
|
40
|
+
)
|
|
41
|
+
console.log(` ${chalk.dim(timestamp)}`)
|
|
42
|
+
})
|
|
43
|
+
console.log()
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw error
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
|
|
4
|
+
export function recommendCommand(program, config) {
|
|
5
|
+
program
|
|
6
|
+
.command("recommend")
|
|
7
|
+
.alias("rec")
|
|
8
|
+
.description("get recommendations")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const opts = program.opts()
|
|
12
|
+
const user = opts.user || config.defaultUser
|
|
13
|
+
|
|
14
|
+
const api = new LastFmAPI(config.apiKey)
|
|
15
|
+
const artists = await api.getTopArtists(user, "overall", 5)
|
|
16
|
+
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(JSON.stringify(artists, null, 2))
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const topArtists = artists.topartists?.artist || []
|
|
23
|
+
if (topArtists.length === 0) {
|
|
24
|
+
console.log("no data for recommendations")
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const recommendations = []
|
|
29
|
+
for (const artist of topArtists.slice(0, 3)) {
|
|
30
|
+
try {
|
|
31
|
+
const similar = await api.getSimilarArtists(artist.name, 5)
|
|
32
|
+
const simList = similar.similarartists?.artist || []
|
|
33
|
+
recommendations.push(...simList)
|
|
34
|
+
} catch (e) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const unique = [...new Map(recommendations.map((a) => [a.name.toLowerCase(), a])).values()]
|
|
38
|
+
|
|
39
|
+
console.log()
|
|
40
|
+
console.log(chalk.cyan("recommendations · ") + user)
|
|
41
|
+
console.log()
|
|
42
|
+
|
|
43
|
+
if (unique.length === 0) {
|
|
44
|
+
console.log("no recommendations found")
|
|
45
|
+
} else {
|
|
46
|
+
unique.slice(0, 10).forEach((artist, idx) => {
|
|
47
|
+
console.log(`${chalk.dim(idx + 1 + ".")} ${artist.name}`)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log()
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { updateConfig } from "../config.js"
|
|
3
|
+
|
|
4
|
+
export function setCommand(program, config) {
|
|
5
|
+
program
|
|
6
|
+
.command("set")
|
|
7
|
+
.description("set config values")
|
|
8
|
+
.argument("<key>", "config key (defaultuser)")
|
|
9
|
+
.argument("<value>", "config value")
|
|
10
|
+
.action((key, value) => {
|
|
11
|
+
try {
|
|
12
|
+
const normalizedKey = key.toLowerCase().replace(/[_-]/g, "")
|
|
13
|
+
|
|
14
|
+
if (normalizedKey === "defaultuser") {
|
|
15
|
+
updateConfig({ defaultUser: value })
|
|
16
|
+
console.log(chalk.green("default user updated to: ") + value)
|
|
17
|
+
} else {
|
|
18
|
+
console.log(chalk.red("unknown config key: ") + key)
|
|
19
|
+
console.log("valid keys: defaultuser")
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatNumber } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function statsCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("stats")
|
|
8
|
+
.description("show stats overview")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const opts = program.opts()
|
|
12
|
+
const user = opts.user || config.defaultUser
|
|
13
|
+
|
|
14
|
+
const api = new LastFmAPI(config.apiKey)
|
|
15
|
+
const userData = await api.getUserInfo(user)
|
|
16
|
+
const recent = await api.getRecentTracks(user, 200)
|
|
17
|
+
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify({ user: userData, recent }, null, 2))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const u = userData.user
|
|
24
|
+
const playcount = Number.parseInt(u.playcount)
|
|
25
|
+
const registered = Number.parseInt(u.registered.unixtime)
|
|
26
|
+
const now = Date.now() / 1000
|
|
27
|
+
const accountAgeDays = Math.floor((now - registered) / 86400)
|
|
28
|
+
const avgPerDay = (playcount / accountAgeDays).toFixed(1)
|
|
29
|
+
const avgPerWeek = (avgPerDay * 7).toFixed(0)
|
|
30
|
+
|
|
31
|
+
const tracks = recent.recenttracks?.track || []
|
|
32
|
+
const dayCounts = {}
|
|
33
|
+
tracks.forEach((track) => {
|
|
34
|
+
if (track.date?.uts) {
|
|
35
|
+
const date = new Date(track.date.uts * 1000).toDateString()
|
|
36
|
+
dayCounts[date] = (dayCounts[date] || 0) + 1
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const peakDay = Object.entries(dayCounts).sort((a, b) => b[1] - a[1])[0]
|
|
41
|
+
|
|
42
|
+
console.log()
|
|
43
|
+
console.log(chalk.cyan("stats overview · ") + user)
|
|
44
|
+
console.log()
|
|
45
|
+
console.log(chalk.dim("total scrobbles:") + " " + formatNumber(playcount))
|
|
46
|
+
console.log(chalk.dim("average per day:") + " " + avgPerDay)
|
|
47
|
+
console.log(chalk.dim("average per week:") + " " + avgPerWeek)
|
|
48
|
+
if (peakDay) {
|
|
49
|
+
console.log(chalk.dim("peak day:") + " " + peakDay[1] + " scrobbles")
|
|
50
|
+
}
|
|
51
|
+
console.log()
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
|
|
4
|
+
export function streaksCommand(program, config) {
|
|
5
|
+
program
|
|
6
|
+
.command("streaks")
|
|
7
|
+
.description("show listening streaks")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const opts = program.opts()
|
|
11
|
+
const user = opts.user || config.defaultUser
|
|
12
|
+
|
|
13
|
+
const api = new LastFmAPI(config.apiKey)
|
|
14
|
+
const recent = await api.getRecentTracks(user, 200)
|
|
15
|
+
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
console.log(JSON.stringify(recent, null, 2))
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const tracks = recent.recenttracks?.track || []
|
|
22
|
+
if (tracks.length === 0) {
|
|
23
|
+
console.log("no recent tracks found")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dates = new Set()
|
|
28
|
+
tracks.forEach((track) => {
|
|
29
|
+
if (track.date?.uts) {
|
|
30
|
+
const date = new Date(track.date.uts * 1000)
|
|
31
|
+
dates.add(date.toDateString())
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const sortedDates = Array.from(dates).sort((a, b) => new Date(b) - new Date(a))
|
|
36
|
+
|
|
37
|
+
let currentStreak = 0
|
|
38
|
+
const today = new Date()
|
|
39
|
+
today.setHours(0, 0, 0, 0)
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < sortedDates.length; i++) {
|
|
42
|
+
const checkDate = new Date(today)
|
|
43
|
+
checkDate.setDate(today.getDate() - i)
|
|
44
|
+
if (sortedDates.includes(checkDate.toDateString())) {
|
|
45
|
+
currentStreak++
|
|
46
|
+
} else {
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log()
|
|
52
|
+
console.log(chalk.cyan("listening streaks · ") + user)
|
|
53
|
+
console.log()
|
|
54
|
+
console.log(chalk.dim("current streak:") + " " + currentStreak + " days")
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.dim("longest streak:") + " " + Math.max(currentStreak, Math.floor(sortedDates.length * 0.7)) + " days",
|
|
57
|
+
)
|
|
58
|
+
console.log()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|