@naxodev/opencode-music-player 0.2.6 → 0.3.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 +13 -14
- package/SUPPORT.md +1 -1
- package/dist/index.js +2272 -0
- package/dist/tui.js +1 -0
- package/package.json +28 -20
- package/artwork-placement.ts +0 -169
- package/artwork.ts +0 -240
- package/artwork.tsx +0 -246
- package/index.tsx +0 -493
- package/kitty-graphics.ts +0 -105
- package/system-media.ts +0 -593
- package/tmux-offset.ts +0 -120
- package/tui.tsx +0 -1
- package/types.ts +0 -144
- package/ui.tsx +0 -695
- package/waveform.tsx +0 -205
package/dist/tui.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./index.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naxodev/opencode-music-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "macOS system Now Playing controls for the OpenCode 2 TUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -27,22 +27,12 @@
|
|
|
27
27
|
"media-control"
|
|
28
28
|
],
|
|
29
29
|
"exports": {
|
|
30
|
-
".": "./index.
|
|
31
|
-
"./tui": "./tui.
|
|
30
|
+
".": "./dist/index.js",
|
|
31
|
+
"./tui": "./dist/tui.js"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"CHANGELOG.md",
|
|
35
|
-
"
|
|
36
|
-
"artwork.ts",
|
|
37
|
-
"artwork.tsx",
|
|
38
|
-
"index.tsx",
|
|
39
|
-
"tui.tsx",
|
|
40
|
-
"kitty-graphics.ts",
|
|
41
|
-
"ui.tsx",
|
|
42
|
-
"system-media.ts",
|
|
43
|
-
"tmux-offset.ts",
|
|
44
|
-
"types.ts",
|
|
45
|
-
"waveform.tsx",
|
|
35
|
+
"dist",
|
|
46
36
|
"SUPPORT.md",
|
|
47
37
|
"LICENSE",
|
|
48
38
|
"README.md"
|
|
@@ -58,6 +48,8 @@
|
|
|
58
48
|
"darwin"
|
|
59
49
|
],
|
|
60
50
|
"scripts": {
|
|
51
|
+
"build": "bun run scripts/build.ts",
|
|
52
|
+
"prepack": "bun run build",
|
|
61
53
|
"format": "prettier --write .",
|
|
62
54
|
"format:check": "prettier --check .",
|
|
63
55
|
"check": "tsc --noEmit",
|
|
@@ -69,17 +61,33 @@
|
|
|
69
61
|
},
|
|
70
62
|
"dependencies": {
|
|
71
63
|
"@naxodev/music-core": "^0.1.4",
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"
|
|
64
|
+
"pngjs": "^7.0.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@opencode/plugin": "2.0.3",
|
|
68
|
+
"@opentui/core": "0.5.10",
|
|
69
|
+
"@opentui/solid": "0.5.10"
|
|
77
70
|
},
|
|
78
71
|
"devDependencies": {
|
|
79
|
-
"@opencode
|
|
72
|
+
"@opencode/plugin": "2.0.3",
|
|
73
|
+
"@opentui/core": "0.5.10",
|
|
74
|
+
"@opentui/solid": "0.5.10",
|
|
75
|
+
"solid-js": "1.9.15",
|
|
76
|
+
"@opencode/theme": "2.0.3",
|
|
80
77
|
"@types/bun": "^1.3.14",
|
|
81
78
|
"@types/pngjs": "^6.0.5",
|
|
82
79
|
"prettier": "3.9.6",
|
|
83
80
|
"typescript": "7.0.2"
|
|
81
|
+
},
|
|
82
|
+
"peerDependenciesMeta": {
|
|
83
|
+
"@opencode/plugin": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@opentui/core": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@opentui/solid": {
|
|
90
|
+
"optional": true
|
|
91
|
+
}
|
|
84
92
|
}
|
|
85
93
|
}
|
package/artwork-placement.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure planner for native Kitty album-artwork placement.
|
|
3
|
-
*
|
|
4
|
-
* Decides which cleanup/draw actions to run when slot geometry or image
|
|
5
|
-
* identity changes. No I/O, no PNG bytes, no terminal writes.
|
|
6
|
-
*
|
|
7
|
-
* **Write-failure / `nextState` commit rule (Phase 2 executor):**
|
|
8
|
-
* `nextState` is the state to commit **only after every planned action is
|
|
9
|
-
* applied successfully**. On any failed write, the executor must keep the
|
|
10
|
-
* previous `state` so the next frame retries from a known-good snapshot.
|
|
11
|
-
* This module only proposes the post-success state; it does not simulate I/O.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type NativeArtworkPlacement = {
|
|
15
|
-
imageId: number
|
|
16
|
-
x: number
|
|
17
|
-
y: number
|
|
18
|
-
width: number
|
|
19
|
-
height: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type NativeArtworkState = {
|
|
23
|
-
/** Kitty image id currently believed transmitted (`0` = none). */
|
|
24
|
-
transmitted: number
|
|
25
|
-
/** Last successfully applied geometry, or `null`. */
|
|
26
|
-
placement: NativeArtworkPlacement | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type NativeArtworkPlacementInput = {
|
|
30
|
-
state: NativeArtworkState
|
|
31
|
-
/** Desired Kitty image id for current artwork; ignored when clearing. */
|
|
32
|
-
imageId: number
|
|
33
|
-
/** Desired absolute cell geometry (caller applies any tmux offset). */
|
|
34
|
-
x: number
|
|
35
|
-
y: number
|
|
36
|
-
width: number
|
|
37
|
-
height: number
|
|
38
|
-
/** Host can show native graphics. */
|
|
39
|
-
kittySupported: boolean
|
|
40
|
-
/** Container present, not destroyed, `width >= 1`, `height >= 1`. */
|
|
41
|
-
slotValid: boolean
|
|
42
|
-
/** Component tearing down. */
|
|
43
|
-
disposed: boolean
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export type NativeArtworkPlacementAction =
|
|
47
|
-
| { type: "delete-image"; imageId: number }
|
|
48
|
-
| { type: "delete-placement"; imageId: number }
|
|
49
|
-
| {
|
|
50
|
-
type: "transmit-and-display"
|
|
51
|
-
imageId: number
|
|
52
|
-
x: number
|
|
53
|
-
y: number
|
|
54
|
-
width: number
|
|
55
|
-
height: number
|
|
56
|
-
}
|
|
57
|
-
| {
|
|
58
|
-
type: "place"
|
|
59
|
-
imageId: number
|
|
60
|
-
placementId: number
|
|
61
|
-
x: number
|
|
62
|
-
y: number
|
|
63
|
-
width: number
|
|
64
|
-
height: number
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type NativeArtworkPlacementPlan = {
|
|
68
|
-
actions: NativeArtworkPlacementAction[]
|
|
69
|
-
/**
|
|
70
|
-
* Proposed state after successful apply of every action.
|
|
71
|
-
* Do not commit on partial/failed writes — see module JSDoc.
|
|
72
|
-
*/
|
|
73
|
-
nextState: NativeArtworkState
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const CLEARED_STATE: NativeArtworkState = {
|
|
77
|
-
transmitted: 0,
|
|
78
|
-
placement: null,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Stable key matching the historical `imageId:x:y:width:height` string shape. */
|
|
82
|
-
export function placementKey(placement: NativeArtworkPlacement): string {
|
|
83
|
-
return [
|
|
84
|
-
placement.imageId,
|
|
85
|
-
placement.x,
|
|
86
|
-
placement.y,
|
|
87
|
-
placement.width,
|
|
88
|
-
placement.height,
|
|
89
|
-
].join(":")
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function samePlacement(
|
|
93
|
-
a: NativeArtworkPlacement | null,
|
|
94
|
-
b: NativeArtworkPlacement,
|
|
95
|
-
): boolean {
|
|
96
|
-
return a !== null && placementKey(a) === placementKey(b)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Plan Kitty cleanup/draw actions for the next native-artwork frame.
|
|
101
|
-
*
|
|
102
|
-
* Decision table:
|
|
103
|
-
* 1. disposed / unsupported / invalid slot → delete transmitted image (if any)
|
|
104
|
-
* 2. different image id (incl. first paint) → delete old image, then transmit-and-display
|
|
105
|
-
* 3. same image id, geometry changed → delete-placement then place
|
|
106
|
-
* 4. same image id, same geometry → no-op
|
|
107
|
-
*/
|
|
108
|
-
export function planNativeArtworkPlacement(
|
|
109
|
-
input: NativeArtworkPlacementInput,
|
|
110
|
-
): NativeArtworkPlacementPlan {
|
|
111
|
-
const { state, imageId, x, y, width, height } = input
|
|
112
|
-
const desired: NativeArtworkPlacement = { imageId, x, y, width, height }
|
|
113
|
-
|
|
114
|
-
if (input.disposed || !input.kittySupported || !input.slotValid) {
|
|
115
|
-
if (state.transmitted !== 0) {
|
|
116
|
-
return {
|
|
117
|
-
actions: [{ type: "delete-image", imageId: state.transmitted }],
|
|
118
|
-
nextState: CLEARED_STATE,
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return { actions: [], nextState: state }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (state.transmitted !== imageId) {
|
|
125
|
-
const actions: NativeArtworkPlacementAction[] = []
|
|
126
|
-
if (state.transmitted !== 0) {
|
|
127
|
-
actions.push({ type: "delete-image", imageId: state.transmitted })
|
|
128
|
-
} else {
|
|
129
|
-
// A remount loses local state, but the terminal may still hold this ID.
|
|
130
|
-
actions.push({ type: "delete-image", imageId })
|
|
131
|
-
}
|
|
132
|
-
actions.push({
|
|
133
|
-
type: "transmit-and-display",
|
|
134
|
-
imageId,
|
|
135
|
-
x,
|
|
136
|
-
y,
|
|
137
|
-
width,
|
|
138
|
-
height,
|
|
139
|
-
})
|
|
140
|
-
return {
|
|
141
|
-
actions,
|
|
142
|
-
nextState: { transmitted: imageId, placement: desired },
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Same image id.
|
|
147
|
-
if (samePlacement(state.placement, desired)) {
|
|
148
|
-
return { actions: [], nextState: state }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const actions: NativeArtworkPlacementAction[] = []
|
|
152
|
-
// Applied placement or transmitted image → clear old placement first.
|
|
153
|
-
if (state.placement !== null || state.transmitted !== 0) {
|
|
154
|
-
actions.push({ type: "delete-placement", imageId })
|
|
155
|
-
}
|
|
156
|
-
actions.push({
|
|
157
|
-
type: "place",
|
|
158
|
-
imageId,
|
|
159
|
-
placementId: imageId,
|
|
160
|
-
x,
|
|
161
|
-
y,
|
|
162
|
-
width,
|
|
163
|
-
height,
|
|
164
|
-
})
|
|
165
|
-
return {
|
|
166
|
-
actions,
|
|
167
|
-
nextState: { transmitted: imageId, placement: desired },
|
|
168
|
-
}
|
|
169
|
-
}
|
package/artwork.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm } from "node:fs/promises"
|
|
2
|
-
import { tmpdir } from "node:os"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { PNG } from "pngjs"
|
|
5
|
-
import type { Artwork } from "./types.ts"
|
|
6
|
-
import {
|
|
7
|
-
acquireCatalogArtwork,
|
|
8
|
-
MAX_ARTWORK_BYTES,
|
|
9
|
-
selectCatalogResolution,
|
|
10
|
-
type CatalogTarget as TrackIdentity,
|
|
11
|
-
type ArtworkFetcher as Fetcher,
|
|
12
|
-
} from "@naxodev/music-core"
|
|
13
|
-
export {
|
|
14
|
-
selectCatalogResolution,
|
|
15
|
-
selectCatalogTrack,
|
|
16
|
-
readLimitedResponse,
|
|
17
|
-
downloadCatalogImage,
|
|
18
|
-
type CatalogTrack,
|
|
19
|
-
} from "@naxodev/music-core"
|
|
20
|
-
|
|
21
|
-
const MAX_CONVERTED_PNG_BYTES = 1_000_000
|
|
22
|
-
const MAX_IMAGE_DIMENSION = 4_096
|
|
23
|
-
const MAX_IMAGE_PIXELS = 12_000_000
|
|
24
|
-
const CONVERSION_TIMEOUT_MS = 3_000
|
|
25
|
-
|
|
26
|
-
export function selectArtworkUrl(
|
|
27
|
-
target: TrackIdentity,
|
|
28
|
-
results: import("@naxodev/music-core").CatalogTrack[],
|
|
29
|
-
): string | null {
|
|
30
|
-
return selectCatalogResolution(target, results).artworkUrl
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function imageDimensionsAreSafe(width: number, height: number): boolean {
|
|
34
|
-
return (
|
|
35
|
-
Number.isInteger(width) &&
|
|
36
|
-
Number.isInteger(height) &&
|
|
37
|
-
width > 0 &&
|
|
38
|
-
height > 0 &&
|
|
39
|
-
width <= MAX_IMAGE_DIMENSION &&
|
|
40
|
-
height <= MAX_IMAGE_DIMENSION &&
|
|
41
|
-
width * height <= MAX_IMAGE_PIXELS
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function runCommandWithTimeout(
|
|
46
|
-
command: string[],
|
|
47
|
-
timeoutMs: number,
|
|
48
|
-
): Promise<{ code: number; out: string; timed_out: boolean }> {
|
|
49
|
-
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "ignore" })
|
|
50
|
-
let timedOut = false
|
|
51
|
-
const timer = setTimeout(() => {
|
|
52
|
-
timedOut = true
|
|
53
|
-
proc.kill(9)
|
|
54
|
-
}, timeoutMs)
|
|
55
|
-
try {
|
|
56
|
-
const [out, code] = await Promise.all([
|
|
57
|
-
new Response(proc.stdout).text(),
|
|
58
|
-
proc.exited,
|
|
59
|
-
])
|
|
60
|
-
return { code, out, timed_out: timedOut }
|
|
61
|
-
} finally {
|
|
62
|
-
clearTimeout(timer)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function dimensionsFromSips(
|
|
67
|
-
output: string,
|
|
68
|
-
): { width: number; height: number } | null {
|
|
69
|
-
const width = Number(output.match(/pixelWidth:\s*(\d+)/)?.[1])
|
|
70
|
-
const height = Number(output.match(/pixelHeight:\s*(\d+)/)?.[1])
|
|
71
|
-
return imageDimensionsAreSafe(width, height) ? { width, height } : null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function squarePng(
|
|
75
|
-
bytes: Uint8Array,
|
|
76
|
-
size: number,
|
|
77
|
-
): Promise<Uint8Array | null> {
|
|
78
|
-
if (bytes.byteLength === 0 || bytes.byteLength > MAX_ARTWORK_BYTES)
|
|
79
|
-
return null
|
|
80
|
-
const dir = await mkdtemp(join(tmpdir(), "opencode-music-art-"))
|
|
81
|
-
const input = join(dir, "input")
|
|
82
|
-
const output = join(dir, "art.png")
|
|
83
|
-
try {
|
|
84
|
-
await Bun.write(input, bytes)
|
|
85
|
-
const inspected = await runCommandWithTimeout(
|
|
86
|
-
["sips", "-g", "pixelWidth", "-g", "pixelHeight", input],
|
|
87
|
-
CONVERSION_TIMEOUT_MS,
|
|
88
|
-
)
|
|
89
|
-
if (
|
|
90
|
-
inspected.timed_out ||
|
|
91
|
-
inspected.code !== 0 ||
|
|
92
|
-
!dimensionsFromSips(inspected.out)
|
|
93
|
-
) {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const converted = await runCommandWithTimeout(
|
|
98
|
-
[
|
|
99
|
-
"sips",
|
|
100
|
-
"-z",
|
|
101
|
-
String(size),
|
|
102
|
-
String(size),
|
|
103
|
-
"-s",
|
|
104
|
-
"format",
|
|
105
|
-
"png",
|
|
106
|
-
input,
|
|
107
|
-
"--out",
|
|
108
|
-
output,
|
|
109
|
-
],
|
|
110
|
-
CONVERSION_TIMEOUT_MS,
|
|
111
|
-
)
|
|
112
|
-
if (converted.timed_out || converted.code !== 0) return null
|
|
113
|
-
const file = Bun.file(output)
|
|
114
|
-
if (file.size === 0 || file.size > MAX_CONVERTED_PNG_BYTES) return null
|
|
115
|
-
return new Uint8Array(await file.arrayBuffer())
|
|
116
|
-
} finally {
|
|
117
|
-
await rm(dir, { recursive: true, force: true })
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const hex = (r: number, g: number, b: number) =>
|
|
122
|
-
`#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`
|
|
123
|
-
|
|
124
|
-
function presentation(pngBytes: Uint8Array): Pick<Artwork, "cells" | "accent"> {
|
|
125
|
-
const image = PNG.sync.read(Buffer.from(pngBytes))
|
|
126
|
-
const cells: Artwork["cells"] = []
|
|
127
|
-
let accent = "#7aa2f7"
|
|
128
|
-
let accentScore = -1
|
|
129
|
-
|
|
130
|
-
for (let y = 0; y < image.height; y += 2) {
|
|
131
|
-
const row: Artwork["cells"][number] = []
|
|
132
|
-
for (let x = 0; x < image.width; x++) {
|
|
133
|
-
const colors = [y, Math.min(y + 1, image.height - 1)].map((py) => {
|
|
134
|
-
const offset = (py * image.width + x) * 4
|
|
135
|
-
const r = image.data[offset] ?? 0
|
|
136
|
-
const g = image.data[offset + 1] ?? 0
|
|
137
|
-
const b = image.data[offset + 2] ?? 0
|
|
138
|
-
const max = Math.max(r, g, b) / 255
|
|
139
|
-
const min = Math.min(r, g, b) / 255
|
|
140
|
-
const light = (max + min) / 2
|
|
141
|
-
const score = (max - min) * (1 - Math.abs(light - 0.5) * 2)
|
|
142
|
-
if (score > accentScore) {
|
|
143
|
-
accentScore = score
|
|
144
|
-
accent = hex(r, g, b)
|
|
145
|
-
}
|
|
146
|
-
return hex(r, g, b)
|
|
147
|
-
})
|
|
148
|
-
row.push({ upper: colors[0]!, lower: colors[1]! })
|
|
149
|
-
}
|
|
150
|
-
cells.push(row)
|
|
151
|
-
}
|
|
152
|
-
return { cells, accent }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export type ArtworkResolution = {
|
|
156
|
-
artwork: Artwork | null
|
|
157
|
-
duration_ms: number
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export async function resolveArtworkDetails(
|
|
161
|
-
id: string,
|
|
162
|
-
target: TrackIdentity,
|
|
163
|
-
nativeBase64: string | null,
|
|
164
|
-
legacyId = id,
|
|
165
|
-
fetcher: Fetcher = fetch,
|
|
166
|
-
signal?: AbortSignal,
|
|
167
|
-
): Promise<ArtworkResolution> {
|
|
168
|
-
if (signal?.aborted) return { artwork: null, duration_ms: target.duration_ms }
|
|
169
|
-
const candidates: Uint8Array[] = []
|
|
170
|
-
if (nativeBase64) {
|
|
171
|
-
try {
|
|
172
|
-
const maxBase64Length = Math.ceil(MAX_ARTWORK_BYTES / 3) * 4
|
|
173
|
-
if (nativeBase64.length <= maxBase64Length) {
|
|
174
|
-
const bytes = new Uint8Array(Buffer.from(nativeBase64, "base64"))
|
|
175
|
-
if (bytes.byteLength <= MAX_ARTWORK_BYTES) candidates.push(bytes)
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// Invalid system metadata falls through to the catalog lookup.
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
for (const bytes of candidates) {
|
|
183
|
-
try {
|
|
184
|
-
const nativePng = await squarePng(bytes, 300)
|
|
185
|
-
const thumbnail = nativePng ? await squarePng(nativePng, 24) : null
|
|
186
|
-
if (nativePng && thumbnail) {
|
|
187
|
-
return {
|
|
188
|
-
artwork: {
|
|
189
|
-
id,
|
|
190
|
-
legacy_id: legacyId,
|
|
191
|
-
png_base64: Buffer.from(nativePng).toString("base64"),
|
|
192
|
-
...presentation(thumbnail),
|
|
193
|
-
},
|
|
194
|
-
duration_ms: target.duration_ms,
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
} catch {
|
|
198
|
-
// Try the catalog when an app publishes unsupported artwork data.
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const catalog = await acquireCatalogArtwork(target, {
|
|
203
|
-
fetch: fetcher,
|
|
204
|
-
signal,
|
|
205
|
-
})
|
|
206
|
-
if (catalog.kind !== "available" || signal?.aborted)
|
|
207
|
-
return {
|
|
208
|
-
artwork: null,
|
|
209
|
-
duration_ms: catalog.duration_ms ?? target.duration_ms,
|
|
210
|
-
}
|
|
211
|
-
if (!catalog.bytes) {
|
|
212
|
-
return { artwork: null, duration_ms: catalog.duration_ms }
|
|
213
|
-
}
|
|
214
|
-
try {
|
|
215
|
-
const nativePng = await squarePng(catalog.bytes, 300)
|
|
216
|
-
const thumbnail = nativePng ? await squarePng(nativePng, 24) : null
|
|
217
|
-
if (!nativePng || !thumbnail) {
|
|
218
|
-
return { artwork: null, duration_ms: catalog.duration_ms }
|
|
219
|
-
}
|
|
220
|
-
return {
|
|
221
|
-
artwork: {
|
|
222
|
-
id,
|
|
223
|
-
legacy_id: legacyId,
|
|
224
|
-
png_base64: Buffer.from(nativePng).toString("base64"),
|
|
225
|
-
...presentation(thumbnail),
|
|
226
|
-
},
|
|
227
|
-
duration_ms: catalog.duration_ms,
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
return { artwork: null, duration_ms: catalog.duration_ms }
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export async function resolveArtwork(
|
|
235
|
-
id: string,
|
|
236
|
-
target: TrackIdentity,
|
|
237
|
-
nativeBase64: string | null,
|
|
238
|
-
): Promise<Artwork | null> {
|
|
239
|
-
return (await resolveArtworkDetails(id, target, nativeBase64)).artwork
|
|
240
|
-
}
|