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 ADDED
@@ -0,0 +1,73 @@
1
+ # clifm
2
+
3
+ last.fm cli client
4
+
5
+ ## installation
6
+
7
+ ```bash
8
+ npm install -g clifm
9
+ ```
10
+
11
+ ## usage
12
+
13
+ ```bash
14
+ clifm np
15
+ clifm tart 1m
16
+ clifm profile
17
+ ```
18
+
19
+ ## first run
20
+
21
+ on first run, clifm will prompt you for:
22
+ - last.fm api key (get from https://www.last.fm/api)
23
+ - default username
24
+
25
+ ## commands
26
+
27
+ - `clifm topartists <period>` or `clifm tart <period>` - top artists
28
+ - `clifm toptracks <period>` or `clifm tt <period>` - top tracks
29
+ - `clifm topalbums <period>` or `clifm talb <period>` - top albums
30
+ - `clifm topgenres <period>` or `clifm tg <period>` - top genres
31
+ - `clifm nowplaying` or `clifm np` - now playing or last track
32
+ - `clifm profile` or `clifm pr` - user profile
33
+ - `clifm recent` or `clifm rt` - recent tracks
34
+ - `clifm loved` or `clifm lt` - loved tracks
35
+ - `clifm artist <name>` or `clifm ar <name>` - artist info
36
+ - `clifm album <artist> <album>` or `clifm alb <artist> <album>` - album info
37
+ - `clifm track <artist> <track>` or `clifm tr <artist> <track>` - track info
38
+ - `clifm weekly` or `clifm wc` - weekly chart
39
+ - `clifm taste` - taste summary
40
+ - `clifm streaks` - listening streaks
41
+ - `clifm compare <user1> <user2>` or `clifm cmp <user1> <user2>` - compare users
42
+ - `clifm recommend` or `clifm rec` - recommendations
43
+ - `clifm discovery` or `clifm disc` - discovery artists
44
+ - `clifm stats` - stats overview
45
+ - `clifm calendar` - scrobbles per weekday
46
+ - `clifm eras` - dominant eras
47
+ - `clifm library` - library size
48
+ - `clifm milestones` - listening milestones
49
+ - `clifm set defaultuser <username>` - set default user
50
+ - `clifm config` - show config
51
+ - `clifm doctor` - check health
52
+
53
+ ## periods
54
+
55
+ - `1w` - last week
56
+ - `1m` - last month
57
+ - `2m` - last 3 months
58
+ - `4m` - last 6 months
59
+ - `6m` - last 12 months
60
+ - `12m` - all time
61
+ - `alltime` - all time
62
+
63
+ ## global options
64
+
65
+ - `-u, --user <username>` - override default user
66
+ - `--json` - output raw json
67
+ - `--no-color` - disable colors
68
+ - `--limit <number>` - limit list output
69
+ - `--page <number>` - pagination
70
+
71
+ ## license
72
+
73
+ MIT
package/bun.lock ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "clifm",
7
+ "dependencies": {
8
+ "chalk": "^5.3.0",
9
+ "commander": "^12.1.0",
10
+ "node-fetch": "^3.3.2",
11
+ },
12
+ "devDependencies": {
13
+ "@types/node": "^25.0.3",
14
+ },
15
+ },
16
+ },
17
+ "packages": {
18
+ "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
19
+
20
+ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
21
+
22
+ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
23
+
24
+ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
25
+
26
+ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
27
+
28
+ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
29
+
30
+ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
31
+
32
+ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
33
+
34
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
35
+
36
+ "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
37
+ }
38
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
package/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander"
4
+ import chalk from "chalk"
5
+ import { getConfig, hasConfig } from "./src/config.js"
6
+ import { runSetup } from "./src/setup.js"
7
+ import { registerCommands } from "./src/commands/index.js"
8
+
9
+ const program = new Command()
10
+
11
+ async function main() {
12
+ try {
13
+ if (!hasConfig()) {
14
+ await runSetup()
15
+ }
16
+
17
+ const config = getConfig()
18
+
19
+ program
20
+ .name("clifm")
21
+ .description("last.fm cli client")
22
+ .version("1.0.0")
23
+ .option("-u, --user <username>", "override default user")
24
+ .option("--json", "output raw json")
25
+ .option("--no-color", "disable colors")
26
+ .option("--limit <number>", "limit list output", "10")
27
+ .option("--page <number>", "page number", "1")
28
+
29
+ registerCommands(program, config)
30
+
31
+ await program.parseAsync(process.argv)
32
+ } catch (error) {
33
+ console.error(chalk.red(error.message || "unknown error"))
34
+ process.exit(1)
35
+ }
36
+ }
37
+
38
+ main()
@@ -0,0 +1,11 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ typescript: {
4
+ ignoreBuildErrors: true,
5
+ },
6
+ images: {
7
+ unoptimized: true,
8
+ },
9
+ }
10
+
11
+ export default nextConfig
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "clifm",
3
+ "version": "1.0.0",
4
+ "description": "last.fm cli client",
5
+ "keywords": [
6
+ "lastfm",
7
+ "last.fm",
8
+ "cli",
9
+ "music",
10
+ "scrobble"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Avery <av@skillissue.lol>",
14
+ "type": "module",
15
+ "main": "index.js",
16
+ "bin": {
17
+ "clifm": "./index.js"
18
+ },
19
+ "scripts": {
20
+ "test": "node index.js --help"
21
+ },
22
+ "dependencies": {
23
+ "chalk": "^5.3.0",
24
+ "commander": "^12.1.0",
25
+ "node-fetch": "^3.3.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^25.0.3"
29
+ },
30
+ "engines": {
31
+ "node": ">=20.0.0"
32
+ }
33
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ '@tailwindcss/postcss': {},
5
+ },
6
+ }
7
+
8
+ export default config
package/src/api.js ADDED
@@ -0,0 +1,91 @@
1
+ import fetch from "node-fetch"
2
+
3
+ const BASE_URL = "https://ws.audioscrobbler.com/2.0/"
4
+
5
+ export class LastFmAPI {
6
+ constructor(apiKey) {
7
+ this.apiKey = apiKey
8
+ }
9
+
10
+ async call(method, params = {}) {
11
+ const url = new URL(BASE_URL)
12
+ url.searchParams.set("method", method)
13
+ url.searchParams.set("api_key", this.apiKey)
14
+ url.searchParams.set("format", "json")
15
+
16
+ for (const [key, value] of Object.entries(params)) {
17
+ if (value !== undefined && value !== null) {
18
+ url.searchParams.set(key, value.toString())
19
+ }
20
+ }
21
+
22
+ try {
23
+ const response = await fetch(url.toString())
24
+ const data = await response.json()
25
+
26
+ if (data.error) {
27
+ throw new Error(data.message || "last.fm api error")
28
+ }
29
+
30
+ return data
31
+ } catch (error) {
32
+ if (error.message.includes("last.fm")) {
33
+ throw error
34
+ }
35
+ throw new Error("failed to connect to last.fm") // generic as fuck
36
+ }
37
+ }
38
+
39
+ async getUserInfo(user) {
40
+ return this.call("user.getInfo", { user })
41
+ }
42
+
43
+ async getTopArtists(user, period, limit = 10, page = 1) {
44
+ return this.call("user.getTopArtists", { user, period, limit, page })
45
+ }
46
+
47
+ async getTopTracks(user, period, limit = 10, page = 1) {
48
+ return this.call("user.getTopTracks", { user, period, limit, page })
49
+ }
50
+
51
+ async getTopAlbums(user, period, limit = 10, page = 1) {
52
+ return this.call("user.getTopAlbums", { user, period, limit, page })
53
+ }
54
+
55
+ async getTopTags(user, limit = 10) {
56
+ return this.call("user.getTopTags", { user, limit })
57
+ }
58
+
59
+ async getRecentTracks(user, limit = 10, page = 1) {
60
+ return this.call("user.getRecentTracks", { user, limit, page })
61
+ }
62
+
63
+ async getLovedTracks(user, limit = 10, page = 1) {
64
+ return this.call("user.getLovedTracks", { user, limit, page })
65
+ }
66
+
67
+ async getArtistInfo(artist, user) {
68
+ return this.call("artist.getInfo", { artist, user })
69
+ }
70
+
71
+ async getAlbumInfo(artist, album, user) {
72
+ return this.call("album.getInfo", { artist, album, user })
73
+ }
74
+
75
+ async getTrackInfo(artist, track, user) {
76
+ return this.call("track.getInfo", { artist, track, user })
77
+ }
78
+
79
+ async getWeeklyArtistChart(user) {
80
+ return this.call("user.getWeeklyArtistChart", { user })
81
+ }
82
+
83
+ // geez these could have been done with a better approach (above & below)
84
+ async getSimilarArtists(artist, limit = 10) {
85
+ return this.call("artist.getSimilar", { artist, limit })
86
+ }
87
+
88
+ async getArtistTopTags(artist) {
89
+ return this.call("artist.getTopTags", { artist })
90
+ }
91
+ }
@@ -0,0 +1,67 @@
1
+ import chalk from "chalk"
2
+ import { LastFmAPI } from "../api.js"
3
+ import { formatNumber } from "../utils/format.js"
4
+
5
+ export function albumCommand(program, config) {
6
+ program
7
+ .command("album <artist> <album>")
8
+ .alias("alb")
9
+ .description("show album info")
10
+ .action(async (artist, album) => {
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.getAlbumInfo(artist, album, user)
17
+
18
+ if (opts.json) {
19
+ console.log(JSON.stringify(data, null, 2))
20
+ return
21
+ }
22
+
23
+ const alb = data.album
24
+ if (!alb) {
25
+ console.log("album not found")
26
+ return
27
+ }
28
+
29
+ console.log()
30
+ console.log(chalk.cyan("album · ") + alb.name)
31
+ console.log(chalk.dim("by ") + alb.artist)
32
+ console.log()
33
+
34
+ if (alb.playcount) {
35
+ console.log(chalk.dim("playcount:") + " " + formatNumber(Number.parseInt(alb.playcount)))
36
+ }
37
+ if (alb.userplaycount) {
38
+ console.log(chalk.dim("your plays:") + " " + formatNumber(Number.parseInt(alb.userplaycount)))
39
+ }
40
+
41
+ const tags = alb.tags?.tag || []
42
+ if (tags.length > 0) {
43
+ console.log(
44
+ chalk.dim("tags:") +
45
+ " " +
46
+ tags
47
+ .slice(0, 5)
48
+ .map((t) => t.name)
49
+ .join(", "),
50
+ )
51
+ }
52
+
53
+ const tracks = alb.tracks?.track || []
54
+ if (tracks.length > 0) {
55
+ console.log()
56
+ console.log(chalk.dim("tracks:"))
57
+ tracks.slice(0, 10).forEach((t, idx) => {
58
+ console.log(` ${chalk.dim(idx + 1 + ".")} ${t.name}`)
59
+ })
60
+ }
61
+
62
+ console.log()
63
+ } catch (error) {
64
+ throw error
65
+ }
66
+ })
67
+ }
@@ -0,0 +1,63 @@
1
+ import chalk from "chalk"
2
+ import { LastFmAPI } from "../api.js"
3
+ import { formatNumber } from "../utils/format.js"
4
+
5
+ export function artistCommand(program, config) {
6
+ program
7
+ .command("artist <name>")
8
+ .alias("ar")
9
+ .description("show artist info")
10
+ .action(async (name) => {
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.getArtistInfo(name, user)
17
+
18
+ if (opts.json) {
19
+ console.log(JSON.stringify(data, null, 2))
20
+ return
21
+ }
22
+
23
+ const artist = data.artist
24
+ if (!artist) {
25
+ console.log("artist not found")
26
+ return
27
+ }
28
+
29
+ console.log()
30
+ console.log(chalk.cyan("artist · ") + artist.name)
31
+ console.log()
32
+
33
+ if (artist.stats?.playcount) {
34
+ console.log(chalk.dim("playcount:") + " " + formatNumber(Number.parseInt(artist.stats.playcount)))
35
+ }
36
+ if (artist.stats?.userplaycount) {
37
+ console.log(chalk.dim("your plays:") + " " + formatNumber(Number.parseInt(artist.stats.userplaycount)))
38
+ }
39
+
40
+ const tags = artist.tags?.tag || []
41
+ if (tags.length > 0) {
42
+ console.log(
43
+ chalk.dim("tags:") +
44
+ " " +
45
+ tags
46
+ .slice(0, 5)
47
+ .map((t) => t.name)
48
+ .join(", "),
49
+ )
50
+ }
51
+
52
+ if (artist.bio?.summary) {
53
+ console.log()
54
+ const summary = artist.bio.summary.replace(/<[^>]*>/g, "").split("\n")[0]
55
+ console.log(summary.substring(0, 300) + (summary.length > 300 ? "..." : ""))
56
+ }
57
+
58
+ console.log()
59
+ } catch (error) {
60
+ throw error
61
+ }
62
+ })
63
+ }
@@ -0,0 +1,47 @@
1
+ import chalk from "chalk"
2
+ import { LastFmAPI } from "../api.js"
3
+
4
+ export function calendarCommand(program, config) {
5
+ program
6
+ .command("calendar")
7
+ .description("show scrobbles per weekday")
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
+ const weekdays = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
23
+ const counts = new Array(7).fill(0)
24
+
25
+ tracks.forEach((track) => {
26
+ if (track.date?.uts) {
27
+ const date = new Date(track.date.uts * 1000)
28
+ counts[date.getDay()]++
29
+ }
30
+ })
31
+
32
+ console.log()
33
+ console.log(chalk.cyan("calendar · ") + user)
34
+ console.log()
35
+
36
+ weekdays.forEach((day, idx) => {
37
+ const count = counts[idx]
38
+ const bar = "█".repeat(Math.floor(count / 2))
39
+ console.log(`${day.padEnd(10)} ${chalk.yellow(bar)} ${count}`)
40
+ })
41
+
42
+ console.log()
43
+ } catch (error) {
44
+ throw error
45
+ }
46
+ })
47
+ }
@@ -0,0 +1,56 @@
1
+ import chalk from "chalk"
2
+ import { LastFmAPI } from "../api.js"
3
+
4
+ export function compareCommand(program, config) {
5
+ program
6
+ .command("compare <user1> <user2>")
7
+ .alias("cmp")
8
+ .description("compare two users")
9
+ .action(async (user1, user2) => {
10
+ try {
11
+ const opts = program.opts()
12
+
13
+ const api = new LastFmAPI(config.apiKey)
14
+ const artists1 = await api.getTopArtists(user1, "overall", 50)
15
+ const artists2 = await api.getTopArtists(user2, "overall", 50)
16
+
17
+ if (opts.json) {
18
+ console.log(JSON.stringify({ user1: artists1, user2: artists2 }, null, 2))
19
+ return
20
+ }
21
+
22
+ const list1 = artists1.topartists?.artist || []
23
+ const list2 = artists2.topartists?.artist || []
24
+
25
+ if (list1.length === 0 || list2.length === 0) {
26
+ console.log("insufficient data to compare")
27
+ return
28
+ }
29
+
30
+ const names1 = new Set(list1.map((a) => a.name.toLowerCase()))
31
+ const names2 = new Set(list2.map((a) => a.name.toLowerCase()))
32
+
33
+ const shared = list1.filter((a) => names2.has(a.name.toLowerCase()))
34
+ const compatibility = ((shared.length / Math.min(list1.length, list2.length)) * 100).toFixed(0)
35
+
36
+ console.log()
37
+ console.log(chalk.cyan("compare · ") + user1 + chalk.dim(" vs ") + user2)
38
+ console.log()
39
+ console.log(chalk.dim("compatibility:") + " " + compatibility + "%")
40
+ console.log()
41
+
42
+ if (shared.length > 0) {
43
+ console.log(chalk.dim("shared artists:"))
44
+ shared.slice(0, 10).forEach((artist, idx) => {
45
+ console.log(` ${chalk.dim(idx + 1 + ".")} ${artist.name}`)
46
+ })
47
+ } else {
48
+ console.log("no shared artists in top 50")
49
+ }
50
+
51
+ console.log()
52
+ } catch (error) {
53
+ throw error
54
+ }
55
+ })
56
+ }
@@ -0,0 +1,23 @@
1
+ import chalk from "chalk"
2
+ import { getConfig } from "../config.js"
3
+ import { maskApiKey } from "../utils/format.js"
4
+
5
+ export function configShowCommand(program, config) {
6
+ program
7
+ .command("config")
8
+ .description("show config")
9
+ .action(() => {
10
+ try {
11
+ const cfg = getConfig()
12
+
13
+ console.log()
14
+ console.log(chalk.cyan("config"))
15
+ console.log()
16
+ console.log(chalk.dim("api key:") + " " + maskApiKey(cfg.apiKey))
17
+ console.log(chalk.dim("default user:") + " " + cfg.defaultUser)
18
+ console.log()
19
+ } catch (error) {
20
+ throw error
21
+ }
22
+ })
23
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from "chalk"
2
+ import { LastFmAPI } from "../api.js"
3
+
4
+ export function discoveryCommand(program, config) {
5
+ program
6
+ .command("discovery")
7
+ .alias("disc")
8
+ .description("show discovery artists")
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", 50)
16
+
17
+ if (opts.json) {
18
+ console.log(JSON.stringify(artists, null, 2))
19
+ return
20
+ }
21
+
22
+ const list = artists.topartists?.artist || []
23
+ if (list.length === 0) {
24
+ console.log("no data available")
25
+ return
26
+ }
27
+
28
+ const discoveries = list
29
+ .map((a, idx) => ({ ...a, rank: idx + 1, playcount: Number.parseInt(a.playcount) }))
30
+ .filter((a) => a.playcount < 50 && a.rank < 30)
31
+ .sort((a, b) => a.rank - b.rank)
32
+
33
+ console.log()
34
+ console.log(chalk.cyan("discoveries · ") + user)
35
+ console.log()
36
+
37
+ if (discoveries.length === 0) {
38
+ console.log("no discoveries found")
39
+ } else {
40
+ discoveries.slice(0, 10).forEach((artist, idx) => {
41
+ console.log(`${chalk.dim(idx + 1 + ".")} ${artist.name} ${chalk.dim("—")} ${artist.playcount} plays`)
42
+ })
43
+ }
44
+
45
+ console.log()
46
+ } catch (error) {
47
+ throw error
48
+ }
49
+ })
50
+ }
@@ -0,0 +1,53 @@
1
+ import chalk from "chalk"
2
+ import { getConfig, hasConfig } from "../config.js"
3
+ import { LastFmAPI } from "../api.js"
4
+
5
+ export function doctorCommand(program, config) {
6
+ program
7
+ .command("doctor")
8
+ .description("check system health")
9
+ .action(async () => {
10
+ try {
11
+ console.log()
12
+ console.log(chalk.cyan("running diagnostics..."))
13
+ console.log()
14
+
15
+ if (!hasConfig()) {
16
+ console.log(chalk.red("✗") + " config file missing")
17
+ console.log()
18
+ return
19
+ }
20
+
21
+ console.log(chalk.green("✓") + " config file found")
22
+
23
+ const cfg = getConfig()
24
+
25
+ if (!cfg.apiKey) {
26
+ console.log(chalk.red("✗") + " api key missing")
27
+ } else {
28
+ console.log(chalk.green("✓") + " api key configured")
29
+ }
30
+
31
+ if (!cfg.defaultUser) {
32
+ console.log(chalk.red("✗") + " default user missing")
33
+ } else {
34
+ console.log(chalk.green("✓") + " default user configured")
35
+ }
36
+
37
+ try {
38
+ const api = new LastFmAPI(cfg.apiKey)
39
+ await api.getUserInfo(cfg.defaultUser)
40
+ console.log(chalk.green("✓") + " last.fm api connection successful")
41
+ } catch (error) {
42
+ console.log(chalk.red("✗") + " last.fm api connection failed")
43
+ console.log(chalk.dim(" " + error.message))
44
+ }
45
+
46
+ console.log()
47
+ console.log(chalk.green("diagnostics complete"))
48
+ console.log()
49
+ } catch (error) {
50
+ throw error
51
+ }
52
+ })
53
+ }