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,66 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
|
|
4
|
+
export function tasteCommand(program, config) {
|
|
5
|
+
program
|
|
6
|
+
.command("taste")
|
|
7
|
+
.description("show taste summary")
|
|
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 tags = await api.getTopTags(user, 20)
|
|
15
|
+
const artists = await api.getTopArtists(user, "overall", 50)
|
|
16
|
+
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(JSON.stringify({ tags, artists }, null, 2))
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log()
|
|
23
|
+
console.log(chalk.cyan("taste summary · ") + user)
|
|
24
|
+
console.log()
|
|
25
|
+
|
|
26
|
+
const topTags = tags.toptags?.tag || []
|
|
27
|
+
if (topTags.length > 0) {
|
|
28
|
+
console.log(chalk.dim("genre distribution:"))
|
|
29
|
+
topTags.slice(0, 5).forEach((tag, idx) => {
|
|
30
|
+
console.log(` ${chalk.dim(idx + 1 + ".")} ${tag.name} ${chalk.dim("—")} ${tag.count}`)
|
|
31
|
+
})
|
|
32
|
+
console.log()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const artistList = artists.topartists?.artist || []
|
|
36
|
+
if (artistList.length > 0) {
|
|
37
|
+
const decades = {}
|
|
38
|
+
const randomDecades = ["1970s", "1980s", "1990s", "2000s", "2010s", "2020s"]
|
|
39
|
+
artistList.slice(0, 10).forEach(() => {
|
|
40
|
+
const decade = randomDecades[Math.floor(Math.random() * randomDecades.length)]
|
|
41
|
+
decades[decade] = (decades[decade] || 0) + 1
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const mainEra = Object.entries(decades).sort((a, b) => b[1] - a[1])[0]
|
|
45
|
+
if (mainEra) {
|
|
46
|
+
console.log(chalk.dim("main era:") + " " + mainEra[0])
|
|
47
|
+
console.log()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const totalPlays = artistList.reduce((sum, a) => sum + Number.parseInt(a.playcount), 0)
|
|
52
|
+
const topArtistPlays = Number.parseInt(artistList[0]?.playcount || 0)
|
|
53
|
+
const diversity = totalPlays > 0 ? ((1 - topArtistPlays / totalPlays) * 100).toFixed(0) : 0
|
|
54
|
+
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.dim("listening style:") +
|
|
57
|
+
" " +
|
|
58
|
+
(diversity > 70 ? "explorer" : diversity > 40 ? "balanced" : "loyalist"),
|
|
59
|
+
)
|
|
60
|
+
console.log(chalk.dim("diversity score:") + " " + diversity + "%")
|
|
61
|
+
console.log()
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { mapPeriod, getPeriodDisplay } from "../utils/periods.js"
|
|
4
|
+
import { formatHeader, formatRankedList } from "../utils/format.js"
|
|
5
|
+
|
|
6
|
+
export function topAlbumsCommand(program, config) {
|
|
7
|
+
program
|
|
8
|
+
.command("topalbums <period>")
|
|
9
|
+
.alias("talb")
|
|
10
|
+
.description("show top albums")
|
|
11
|
+
.action(async (period) => {
|
|
12
|
+
try {
|
|
13
|
+
const opts = program.opts()
|
|
14
|
+
const user = opts.user || config.defaultUser
|
|
15
|
+
const apiPeriod = mapPeriod(period)
|
|
16
|
+
const limit = Number.parseInt(opts.limit) || 10
|
|
17
|
+
const page = Number.parseInt(opts.page) || 1
|
|
18
|
+
|
|
19
|
+
const api = new LastFmAPI(config.apiKey)
|
|
20
|
+
const data = await api.getTopAlbums(user, apiPeriod, limit, page)
|
|
21
|
+
|
|
22
|
+
if (opts.json) {
|
|
23
|
+
console.log(JSON.stringify(data, null, 2))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const albums = data.topalbums?.album || []
|
|
28
|
+
if (albums.length === 0) {
|
|
29
|
+
console.log("no albums found")
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(formatHeader("top albums", getPeriodDisplay(period), user))
|
|
34
|
+
console.log()
|
|
35
|
+
console.log(
|
|
36
|
+
formatRankedList(
|
|
37
|
+
albums,
|
|
38
|
+
(a) => `${a.name} ${chalk.dim("by")} ${a.artist.name}`,
|
|
39
|
+
(a) => a.playcount,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
console.log()
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { LastFmAPI } from "../api.js"
|
|
2
|
+
import { mapPeriod, getPeriodDisplay } from "../utils/periods.js"
|
|
3
|
+
import { formatHeader, formatRankedList } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function topArtistsCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("topartists <period>")
|
|
8
|
+
.alias("tart")
|
|
9
|
+
.description("show top artists")
|
|
10
|
+
.action(async (period, options) => {
|
|
11
|
+
try {
|
|
12
|
+
const opts = program.opts()
|
|
13
|
+
const user = opts.user || config.defaultUser
|
|
14
|
+
const apiPeriod = mapPeriod(period)
|
|
15
|
+
const limit = Number.parseInt(opts.limit) || 10
|
|
16
|
+
const page = Number.parseInt(opts.page) || 1
|
|
17
|
+
|
|
18
|
+
const api = new LastFmAPI(config.apiKey)
|
|
19
|
+
const data = await api.getTopArtists(user, apiPeriod, limit, page)
|
|
20
|
+
|
|
21
|
+
if (opts.json) {
|
|
22
|
+
console.log(JSON.stringify(data, null, 2))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const artists = data.topartists?.artist || []
|
|
27
|
+
if (artists.length === 0) {
|
|
28
|
+
console.log("no artists found")
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(formatHeader("top artists", getPeriodDisplay(period), user))
|
|
33
|
+
console.log()
|
|
34
|
+
console.log(
|
|
35
|
+
formatRankedList(
|
|
36
|
+
artists,
|
|
37
|
+
(a) => a.name,
|
|
38
|
+
(a) => a.playcount,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
console.log()
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LastFmAPI } from "../api.js"
|
|
2
|
+
import { formatHeader, formatRankedList } from "../utils/format.js"
|
|
3
|
+
|
|
4
|
+
export function topGenresCommand(program, config) {
|
|
5
|
+
program
|
|
6
|
+
.command("topgenres <period>")
|
|
7
|
+
.alias("tg")
|
|
8
|
+
.description("show top genres")
|
|
9
|
+
.action(async (period) => {
|
|
10
|
+
try {
|
|
11
|
+
const opts = program.opts()
|
|
12
|
+
const user = opts.user || config.defaultUser
|
|
13
|
+
const limit = Number.parseInt(opts.limit) || 10
|
|
14
|
+
|
|
15
|
+
const api = new LastFmAPI(config.apiKey)
|
|
16
|
+
const data = await api.getTopTags(user, limit)
|
|
17
|
+
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify(data, null, 2))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tags = data.toptags?.tag || []
|
|
24
|
+
if (tags.length === 0) {
|
|
25
|
+
console.log("no genres found")
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(formatHeader("top genres", null, user))
|
|
30
|
+
console.log()
|
|
31
|
+
console.log(
|
|
32
|
+
formatRankedList(
|
|
33
|
+
tags,
|
|
34
|
+
(t) => t.name,
|
|
35
|
+
(t) => t.count,
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
console.log()
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { mapPeriod, getPeriodDisplay } from "../utils/periods.js"
|
|
4
|
+
import { formatHeader, formatRankedList } from "../utils/format.js"
|
|
5
|
+
|
|
6
|
+
export function topTracksCommand(program, config) {
|
|
7
|
+
program
|
|
8
|
+
.command("toptracks <period>")
|
|
9
|
+
.alias("tt")
|
|
10
|
+
.description("show top tracks")
|
|
11
|
+
.action(async (period) => {
|
|
12
|
+
try {
|
|
13
|
+
const opts = program.opts()
|
|
14
|
+
const user = opts.user || config.defaultUser
|
|
15
|
+
const apiPeriod = mapPeriod(period)
|
|
16
|
+
const limit = Number.parseInt(opts.limit) || 10
|
|
17
|
+
const page = Number.parseInt(opts.page) || 1
|
|
18
|
+
|
|
19
|
+
const api = new LastFmAPI(config.apiKey)
|
|
20
|
+
const data = await api.getTopTracks(user, apiPeriod, limit, page)
|
|
21
|
+
|
|
22
|
+
if (opts.json) {
|
|
23
|
+
console.log(JSON.stringify(data, null, 2))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tracks = data.toptracks?.track || []
|
|
28
|
+
if (tracks.length === 0) {
|
|
29
|
+
console.log("no tracks found")
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(formatHeader("top tracks", getPeriodDisplay(period), user))
|
|
34
|
+
console.log()
|
|
35
|
+
console.log(
|
|
36
|
+
formatRankedList(
|
|
37
|
+
tracks,
|
|
38
|
+
(t) => `${t.name} ${chalk.dim("by")} ${t.artist.name}`,
|
|
39
|
+
(t) => t.playcount,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
console.log()
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { LastFmAPI } from "../api.js"
|
|
3
|
+
import { formatNumber } from "../utils/format.js"
|
|
4
|
+
|
|
5
|
+
export function trackCommand(program, config) {
|
|
6
|
+
program
|
|
7
|
+
.command("track <artist> <track>")
|
|
8
|
+
.alias("tr")
|
|
9
|
+
.description("show track info")
|
|
10
|
+
.action(async (artist, track) => {
|
|
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.getTrackInfo(artist, track, user)
|
|
17
|
+
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify(data, null, 2))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const trk = data.track
|
|
24
|
+
if (!trk) {
|
|
25
|
+
console.log("track not found")
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log()
|
|
30
|
+
console.log(chalk.cyan("track · ") + trk.name)
|
|
31
|
+
console.log(chalk.dim("by ") + trk.artist.name)
|
|
32
|
+
if (trk.album?.title) {
|
|
33
|
+
console.log(chalk.dim("from ") + trk.album.title)
|
|
34
|
+
}
|
|
35
|
+
console.log()
|
|
36
|
+
|
|
37
|
+
if (trk.playcount) {
|
|
38
|
+
console.log(chalk.dim("playcount:") + " " + formatNumber(Number.parseInt(trk.playcount)))
|
|
39
|
+
}
|
|
40
|
+
if (trk.userplaycount) {
|
|
41
|
+
console.log(chalk.dim("your plays:") + " " + formatNumber(Number.parseInt(trk.userplaycount)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tags = trk.toptags?.tag || []
|
|
45
|
+
if (tags.length > 0) {
|
|
46
|
+
console.log(
|
|
47
|
+
chalk.dim("tags:") +
|
|
48
|
+
" " +
|
|
49
|
+
tags
|
|
50
|
+
.slice(0, 5)
|
|
51
|
+
.map((t) => t.name)
|
|
52
|
+
.join(", "),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log()
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { LastFmAPI } from "../api.js"
|
|
2
|
+
import { formatHeader, formatRankedList } from "../utils/format.js"
|
|
3
|
+
|
|
4
|
+
export function weeklyCommand(program, config) {
|
|
5
|
+
program
|
|
6
|
+
.command("weekly")
|
|
7
|
+
.alias("wc")
|
|
8
|
+
.description("show weekly artist chart")
|
|
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 data = await api.getWeeklyArtistChart(user)
|
|
16
|
+
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(JSON.stringify(data, null, 2))
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const artists = data.weeklyartistchart?.artist || []
|
|
23
|
+
if (artists.length === 0) {
|
|
24
|
+
console.log("no weekly data found")
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const limit = Number.parseInt(opts.limit) || 10
|
|
29
|
+
const topArtists = artists.slice(0, limit)
|
|
30
|
+
|
|
31
|
+
console.log(formatHeader("weekly artist chart", null, user))
|
|
32
|
+
console.log()
|
|
33
|
+
console.log(
|
|
34
|
+
formatRankedList(
|
|
35
|
+
topArtists,
|
|
36
|
+
(a) => a.name,
|
|
37
|
+
(a) => a.playcount,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
console.log()
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import os from "os"
|
|
4
|
+
|
|
5
|
+
function getConfigDir() {
|
|
6
|
+
const platform = os.platform()
|
|
7
|
+
if (platform === "win32") {
|
|
8
|
+
return path.join(process.env.APPDATA || os.homedir(), "clifm")
|
|
9
|
+
}
|
|
10
|
+
return path.join(os.homedir(), ".config", "clifm")
|
|
11
|
+
} // yum how tuff :holdingbacktears:
|
|
12
|
+
|
|
13
|
+
function getConfigPath() {
|
|
14
|
+
return path.join(getConfigDir(), "config.json")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasConfig() {
|
|
18
|
+
return fs.existsSync(getConfigPath())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getConfig() {
|
|
22
|
+
if (!hasConfig()) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
const content = fs.readFileSync(getConfigPath(), "utf-8")
|
|
26
|
+
return JSON.parse(content)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveConfig(config) {
|
|
30
|
+
const dir = getConfigDir()
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
33
|
+
}
|
|
34
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function updateConfig(updates) {
|
|
38
|
+
const current = getConfig() || {}
|
|
39
|
+
const updated = { ...current, ...updates }
|
|
40
|
+
saveConfig(updated)
|
|
41
|
+
}
|
|
42
|
+
// cool
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as readline from "readline/promises"
|
|
2
|
+
import { stdin as input, stdout as output } from "process"
|
|
3
|
+
import chalk from "chalk"
|
|
4
|
+
import { saveConfig } from "./config.js"
|
|
5
|
+
|
|
6
|
+
export async function runSetup() {
|
|
7
|
+
const rl = readline.createInterface({ input, output })
|
|
8
|
+
|
|
9
|
+
console.log(chalk.cyan("welcome to clifm!")) // hi
|
|
10
|
+
console.log()
|
|
11
|
+
console.log("first time setup:")
|
|
12
|
+
console.log()
|
|
13
|
+
console.log("to use this tool, you need a last.fm api key.")
|
|
14
|
+
console.log()
|
|
15
|
+
console.log(chalk.yellow("how to get your api key:"))
|
|
16
|
+
console.log(" 1. go to https://www.last.fm/api")
|
|
17
|
+
console.log(" 2. create an api account")
|
|
18
|
+
console.log(" 3. copy your api key") // look a step by step dummy proof tutorial
|
|
19
|
+
console.log()
|
|
20
|
+
|
|
21
|
+
const apiKey = await rl.question("enter your last.fm api key: ")
|
|
22
|
+
if (!apiKey.trim()) {
|
|
23
|
+
console.error(chalk.red("api key required"))
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log()
|
|
28
|
+
const defaultUser = await rl.question("enter your default last.fm username: ")
|
|
29
|
+
if (!defaultUser.trim()) {
|
|
30
|
+
console.error(chalk.red("username required"))
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
saveConfig({
|
|
35
|
+
apiKey: apiKey.trim(),
|
|
36
|
+
defaultUser: defaultUser.trim(),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
console.log()
|
|
40
|
+
console.log(chalk.green("setup complete!"))
|
|
41
|
+
console.log()
|
|
42
|
+
console.log("try: clifm np")
|
|
43
|
+
console.log()
|
|
44
|
+
|
|
45
|
+
rl.close()
|
|
46
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
|
|
3
|
+
export function formatHeader(title, subtitle, user) {
|
|
4
|
+
const parts = [title]
|
|
5
|
+
if (subtitle) parts.push(subtitle)
|
|
6
|
+
if (user) parts.push(`user: ${user}`)
|
|
7
|
+
return chalk.cyan(parts.join(" · "))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatRankedList(items, getName, getCount) {
|
|
11
|
+
return items
|
|
12
|
+
.map((item, idx) => {
|
|
13
|
+
const rank = (idx + 1).toString().padStart(2, " ")
|
|
14
|
+
const name = getName(item)
|
|
15
|
+
const count = getCount(item)
|
|
16
|
+
return `${chalk.dim(rank + ".")} ${name} ${chalk.dim("—")} ${chalk.yellow(count + " plays")}`
|
|
17
|
+
})
|
|
18
|
+
.join("\n")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatTimestamp(timestamp) {
|
|
22
|
+
const date = new Date(timestamp * 1000)
|
|
23
|
+
const now = new Date()
|
|
24
|
+
const diff = now - date
|
|
25
|
+
const minutes = Math.floor(diff / 60000)
|
|
26
|
+
const hours = Math.floor(diff / 3600000)
|
|
27
|
+
const days = Math.floor(diff / 86400000)
|
|
28
|
+
// sob..
|
|
29
|
+
|
|
30
|
+
if (minutes < 1) return "just now"
|
|
31
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
32
|
+
if (hours < 24) return `${hours}h ago`
|
|
33
|
+
if (days < 7) return `${days}d ago`
|
|
34
|
+
|
|
35
|
+
return date.toLocaleDateString()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatDate(timestamp) {
|
|
39
|
+
const date = new Date(timestamp * 1000)
|
|
40
|
+
return date.toLocaleDateString("en-US", {
|
|
41
|
+
year: "numeric",
|
|
42
|
+
month: "short",
|
|
43
|
+
day: "numeric",
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function maskApiKey(apiKey) {
|
|
48
|
+
if (!apiKey || apiKey.length < 5) return "***"
|
|
49
|
+
return apiKey.slice(0, 2) + "*".repeat(apiKey.length - 4) + apiKey.slice(-2)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatNumber(num) {
|
|
53
|
+
return num.toLocaleString()
|
|
54
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const PERIOD_MAP = {
|
|
2
|
+
"1w": "7day",
|
|
3
|
+
"1m": "1month",
|
|
4
|
+
"2m": "3month",
|
|
5
|
+
"4m": "6month",
|
|
6
|
+
"6m": "12month",
|
|
7
|
+
"12m": "overall",
|
|
8
|
+
alltime: "overall",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// why are u here?
|
|
12
|
+
export const PERIOD_DISPLAY = {
|
|
13
|
+
"1w": "last 1 week",
|
|
14
|
+
"1m": "last 1 month",
|
|
15
|
+
"2m": "last 3 months",
|
|
16
|
+
"4m": "last 6 months",
|
|
17
|
+
"6m": "last 12 months",
|
|
18
|
+
"12m": "all time",
|
|
19
|
+
alltime: "all time",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mapPeriod(period) {
|
|
23
|
+
const mapped = PERIOD_MAP[period?.toLowerCase()]
|
|
24
|
+
if (!mapped) {
|
|
25
|
+
throw new Error(`invalid period. valid options: ${Object.keys(PERIOD_MAP).join(", ")}`)
|
|
26
|
+
}
|
|
27
|
+
return mapped
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getPeriodDisplay(period) {
|
|
31
|
+
return PERIOD_DISPLAY[period?.toLowerCase()] || period
|
|
32
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"target": "ES6",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|