From 19168b123837af05b836305fafad782375cd9e7a Mon Sep 17 00:00:00 2001
From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com>
Date: Sat, 4 Oct 2025 11:16:42 +0200
Subject: [PATCH] Add advanced CLI UI and logging to build and serve scripts
- Enhance `build.mjs` and `serve.mjs` with colorful banners, progress
bars, tables, and section headers for a professional CLI experience -
Add detailed build summaries, error reporting, and plugin statistics to
`build.mjs` - Improve argument parsing and verbose logging in
`build.mjs` - Add plugin authors to Last.fm manifest - Refactor Last.fm
plugin code for consistent formatting and improved settings typing - Add
plugin and file statistics, network interface table, and graceful
shutdown to `serve.mjs` - Remove `.vscode/settings.json` from repo
---
.vscode/settings.json | 8 -
build.mjs | 720 ++++++++++++++++++----
plugins/Last.fm/manifest.json | 10 +
plugins/Last.fm/src/changelog.ts | 64 +-
plugins/Last.fm/src/constants.ts | 62 +-
plugins/Last.fm/src/index.tsx | 124 ++--
plugins/Last.fm/src/manager.ts | 344 +++++------
plugins/Last.fm/src/ui/ChangelogView.tsx | 122 ++--
plugins/Last.fm/src/ui/pages/Debug.tsx | 72 +--
plugins/Last.fm/src/ui/pages/Settings.tsx | 354 +++++------
plugins/Last.fm/src/utils/activity.ts | 42 +-
plugins/Last.fm/src/utils/debug.ts | 92 +--
plugins/Last.fm/src/utils/lastfm.ts | 304 ++++-----
plugins/defs.d.ts | 94 ++-
serve.mjs | 582 +++++++++++++++--
15 files changed, 1999 insertions(+), 995 deletions(-)
delete mode 100644 .vscode/settings.json
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index e8028f8..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "editor.formatOnPaste": true,
- "editor.formatOnSave": true,
- "files.insertFinalNewline": true,
- "editor.codeActionsOnSave": {
- "source.organizeImports": "explicit"
- },
-}
diff --git a/build.mjs b/build.mjs
index 8fb409a..e27b872 100644
--- a/build.mjs
+++ b/build.mjs
@@ -1,151 +1,651 @@
import commonjs from "@rollup/plugin-commonjs";
import nodeResolve from "@rollup/plugin-node-resolve";
import { createHash } from "crypto";
-import { readFile, readdir, writeFile } from "fs/promises";
+import { readFile, readdir, writeFile, stat } from "fs/promises";
import { argv } from "process";
import { rollup, watch } from "rollup";
import swc from "@swc/core";
import esbuild from "rollup-plugin-esbuild";
+// ═══════════════════════════════════════════════════════════════════════════════
+// 🎨 STYLING & ANIMATIONS
+// ═══════════════════════════════════════════════════════════════════════════════
+
+const colors = {
+ reset: "\x1b[0m",
+ bold: "\x1b[1m",
+ dim: "\x1b[2m",
+ italic: "\x1b[3m",
+ underline: "\x1b[4m",
+
+ // Standard colors
+ black: "\x1b[30m",
+ red: "\x1b[31m",
+ green: "\x1b[32m",
+ yellow: "\x1b[33m",
+ blue: "\x1b[34m",
+ magenta: "\x1b[35m",
+ cyan: "\x1b[36m",
+ white: "\x1b[37m",
+
+ // Bright colors
+ brightRed: "\x1b[91m",
+ brightGreen: "\x1b[92m",
+ brightYellow: "\x1b[93m",
+ brightBlue: "\x1b[94m",
+ brightMagenta: "\x1b[95m",
+ brightCyan: "\x1b[96m",
+ brightWhite: "\x1b[97m",
+
+ // Backgrounds
+ bgRed: "\x1b[41m",
+ bgGreen: "\x1b[42m",
+ bgYellow: "\x1b[43m",
+ bgBlue: "\x1b[44m",
+ bgMagenta: "\x1b[45m",
+ bgCyan: "\x1b[46m",
+};
+
+const symbols = {
+ // Build status
+ success: "✓",
+ error: "✗",
+ warning: "⚠",
+ info: "ℹ",
+
+ // Activities
+ building: "🔨",
+ watching: "👁",
+ rocket: "🚀",
+ gear: "⚙",
+ lightning: "⚡",
+ fire: "🔥",
+ sparkles: "✨",
+
+ // UI elements
+ arrow: "→",
+ bullet: "•",
+ star: "★",
+ diamond: "◆",
+
+ // Box drawing
+ boxLight: "─",
+ boxHeavy: "━",
+ boxDouble: "═",
+ cornerTL: "┌",
+ cornerTR: "┐",
+ cornerBL: "└",
+ cornerBR: "┘",
+ cross: "┼",
+ teeLeft: "├",
+ teeRight: "┤",
+ vertical: "│",
+};
+
+const spinners = {
+ dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
+ line: ["│", "╱", "─", "╲"],
+ bounce: ["⠁", "⠂", "⠄", "⠂"],
+ pulse: ["●", "◐", "◑", "◒", "◓", "◔", "◕", "◖", "◗"],
+};
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// 🛠 UTILITIES
+// ═══════════════════════════════════════════════════════════════════════════════
+
+function formatTime(ms) {
+ if (ms < 1000) return `${ms}ms`;
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
+}
+
+function formatSize(bytes) {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
+}
+
+function createGradient(text, startColor, endColor) {
+ // Simple gradient effect by alternating colors
+ return text
+ .split("")
+ .map((char, i) => {
+ const color = i % 2 === 0 ? startColor : endColor;
+ return `${color}${char}${colors.reset}`;
+ })
+ .join("");
+}
+
+function createBox(content, title = "", style = "light") {
+ const lines = content.split("\n");
+ const maxLength = Math.max(
+ ...lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").length),
+ title.length,
+ );
+ const width = maxLength + 4;
+
+ const horizontal =
+ style === "heavy"
+ ? symbols.boxHeavy
+ : style === "double"
+ ? symbols.boxDouble
+ : symbols.boxLight;
+
+ let result = "";
+
+ // Top border
+ result += `${symbols.cornerTL}${horizontal.repeat(width - 2)}${symbols.cornerTR}\n`;
+
+ // Title
+ if (title) {
+ const padding = Math.floor((width - title.length - 4) / 2);
+ result += `${symbols.vertical} ${" ".repeat(padding)}${colors.bold}${title}${colors.reset}${" ".repeat(padding)} ${symbols.vertical}\n`;
+ result += `${symbols.teeLeft}${horizontal.repeat(width - 2)}${symbols.teeRight}\n`;
+ }
+
+ // Content
+ lines.forEach((line) => {
+ const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, "");
+ const padding = width - cleanLine.length - 4;
+ result += `${symbols.vertical} ${line}${" ".repeat(padding)} ${symbols.vertical}\n`;
+ });
+
+ // Bottom border
+ result += `${symbols.cornerBL}${horizontal.repeat(width - 2)}${symbols.cornerBR}`;
+
+ return result;
+}
+
+class Spinner {
+ constructor(text = "", spinnerType = "dots") {
+ this.text = text;
+ this.frames = spinners[spinnerType];
+ this.current = 0;
+ this.isSpinning = false;
+ this.interval = null;
+ }
+
+ start() {
+ if (this.isSpinning) return;
+ this.isSpinning = true;
+
+ this.interval = setInterval(() => {
+ process.stdout.write(
+ `\r${colors.cyan}${this.frames[this.current]}${colors.reset} ${this.text}`,
+ );
+ this.current = (this.current + 1) % this.frames.length;
+ }, 100);
+ }
+
+ succeed(text) {
+ this.stop();
+ console.log(
+ `${colors.green}${symbols.success}${colors.reset} ${text || this.text}`,
+ );
+ }
+
+ fail(text) {
+ this.stop();
+ console.log(
+ `${colors.red}${symbols.error}${colors.reset} ${text || this.text}`,
+ );
+ }
+
+ stop() {
+ if (!this.isSpinning) return;
+ this.isSpinning = false;
+ clearInterval(this.interval);
+ process.stdout.write("\r\x1b[K"); // Clear line
+ }
+}
+
+function createProgressBar(current, total, width = 40, showPercentage = true) {
+ const percentage = Math.round((current / total) * 100);
+ const filled = Math.round((current / total) * width);
+ const empty = width - filled;
+
+ const filledBar = `${colors.brightCyan}${"█".repeat(filled)}${colors.reset}`;
+ const emptyBar = `${colors.dim}${"░".repeat(empty)}${colors.reset}`;
+
+ let result = `${filledBar}${emptyBar}`;
+
+ if (showPercentage) {
+ result += ` ${colors.bold}${percentage.toString().padStart(3)}%${colors.reset}`;
+ }
+
+ return result;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// 🎭 LOGGER
+// ═══════════════════════════════════════════════════════════════════════════════
+
+const logger = {
+ banner() {
+ const title = createGradient(
+ "VENDETTA PLUGIN BUILDER",
+ colors.brightCyan,
+ colors.brightMagenta,
+ );
+ const subtitle = `${colors.dim}Professional-grade TypeScript plugin compilation${colors.reset}`;
+
+ console.log();
+ console.log(` ${symbols.rocket} ${title}`);
+ console.log(` ${subtitle}`);
+ console.log();
+ },
+
+ section(title) {
+ const line = symbols.boxHeavy.repeat(60);
+ console.log(`\n${colors.brightBlue}${line}${colors.reset}`);
+ console.log(`${colors.bold}${colors.brightBlue} ${title}${colors.reset}`);
+ console.log(`${colors.brightBlue}${line}${colors.reset}\n`);
+ },
+
+ info(message, icon = symbols.info) {
+ console.log(`${colors.brightBlue}${icon}${colors.reset} ${message}`);
+ },
+
+ success(message, icon = symbols.success) {
+ console.log(`${colors.brightGreen}${icon}${colors.reset} ${message}`);
+ },
+
+ warning(message, icon = symbols.warning) {
+ console.log(`${colors.brightYellow}${icon}${colors.reset} ${message}`);
+ },
+
+ error(message, icon = symbols.error) {
+ console.log(`${colors.brightRed}${icon}${colors.reset} ${message}`);
+ },
+
+ plugin: {
+ building(name) {
+ return new Spinner(
+ `${colors.bold}${name}${colors.reset} ${colors.dim}Building...${colors.reset}`,
+ "pulse",
+ );
+ },
+
+ success(name, time, size) {
+ const timeStr = `${colors.dim}${time}${colors.reset}`;
+ const sizeStr = size ? `${colors.dim}${size}${colors.reset}` : "";
+ console.log(
+ `${colors.brightGreen}${symbols.success}${colors.reset} ${colors.bold}${name}${colors.reset} ${timeStr} ${sizeStr}`,
+ );
+ },
+
+ error(name, error) {
+ console.log(
+ `${colors.brightRed}${symbols.error}${colors.reset} ${colors.bold}${name}${colors.reset}`,
+ );
+ if (error) {
+ console.log(`${colors.red}${colors.dim} ${error}${colors.reset}`);
+ }
+ },
+
+ watching(name, time) {
+ const timeStr = `${colors.dim}${time}${colors.reset}`;
+ console.log(
+ `${colors.brightCyan}${symbols.lightning}${colors.reset} ${colors.bold}${name}${colors.reset} ${colors.dim}rebuilt${colors.reset} ${timeStr}`,
+ );
+ },
+ },
+
+ table(headers, rows) {
+ const colWidths = headers.map((header, i) =>
+ Math.max(
+ header.length,
+ ...rows.map(
+ (row) =>
+ (row[i] || "").toString().replace(/\x1b\[[0-9;]*m/g, "").length,
+ ),
+ ),
+ );
+
+ // Header
+ const headerRow = headers
+ .map(
+ (header, i) =>
+ `${colors.bold}${header.padEnd(colWidths[i])}${colors.reset}`,
+ )
+ .join(` ${colors.dim}│${colors.reset} `);
+
+ console.log(` ${headerRow}`);
+
+ // Separator
+ const separator = colWidths
+ .map((width) => symbols.boxLight.repeat(width))
+ .join(`${colors.dim}─┼─${colors.reset}`);
+ console.log(` ${colors.dim}${separator}${colors.reset}`);
+
+ // Rows
+ rows.forEach((row) => {
+ const formattedRow = row
+ .map((cell, i) => {
+ const cleanCell = (cell || "")
+ .toString()
+ .replace(/\x1b\[[0-9;]*m/g, "");
+ const padding = colWidths[i] - cleanCell.length;
+ return cell + " ".repeat(padding);
+ })
+ .join(` ${colors.dim}│${colors.reset} `);
+
+ console.log(` ${formattedRow}`);
+ });
+ },
+
+ summary(stats) {
+ const { built, failed, total, totalTime, watchMode } = stats;
+
+ const rows = [
+ ["Total Plugins", total.toString()],
+ ["Built Successfully", `${colors.brightGreen}${built}${colors.reset}`],
+ [
+ "Failed",
+ failed > 0
+ ? `${colors.brightRed}${failed}${colors.reset}`
+ : `${colors.dim}0${colors.reset}`,
+ ],
+ ["Total Time", formatTime(totalTime)],
+ [
+ "Mode",
+ watchMode
+ ? `${colors.brightCyan}Watch${colors.reset}`
+ : `${colors.dim}Build${colors.reset}`,
+ ],
+ ];
+
+ this.table(["Metric", "Value"], rows);
+ },
+};
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// 🚀 MAIN EXECUTION
+// ═══════════════════════════════════════════════════════════════════════════════
+
const args = argv.slice(2);
console.clear();
-const inputPlugins = args.filter(x => !x.startsWith("-"));
-const flags = args.map(x => x.toLowerCase()).filter(x => x.startsWith("-"));
+// Parse arguments
+const inputPlugins = args.filter((x) => !x.startsWith("-"));
+const flags = args.map((x) => x.toLowerCase()).filter((x) => x.startsWith("-"));
const isWatch = flags.includes("--watch") || flags.includes("-w");
+const isVerbose = flags.includes("--verbose") || flags.includes("-v");
const toBuild = inputPlugins.length ? inputPlugins : await readdir("./plugins");
-console.log(`Building ${toBuild.length} plugin(s)...`);
+// Display banner
+logger.banner();
-let failed = 0;
+// Build configuration
+const config = {
+ plugins: toBuild.length,
+ mode: isWatch ? "Watch" : "Build",
+ verbose: isVerbose,
+ startTime: Date.now(),
+};
-for (const plugin of toBuild) {
- try {
- await buildPlugin(plugin);
- } catch (e) {
- console.error(e.stack || e);
- failed++;
- }
+logger.info(
+ `${colors.dim}Found ${colors.reset}${colors.bold}${config.plugins}${colors.reset}${colors.dim} plugin(s) to ${config.mode.toLowerCase()}${colors.reset}`,
+);
+if (isWatch) {
+ logger.info(
+ `${colors.dim}Running in watch mode - changes will trigger rebuilds${colors.reset}`,
+ );
}
-console.log("\n" + (
- failed
- ? "\x1b[31m" + `Failed to build ${failed} plugin(s)` + "\x1b[0m"
- : "\x1b[32m" + "All plugin(s) built successfully!" + "\x1b[0m"
-));
+// Build statistics
+let built = 0;
+let failed = 0;
+const buildResults = [];
+const startTime = Date.now();
-isWatch && console.log("\nWatching for changes...");
+// Build all plugins
+logger.section("Building Plugins");
+
+for (let i = 0; i < toBuild.length; i++) {
+ const plugin = toBuild[i];
+
+ if (!isWatch) {
+ console.log(
+ `\n ${createProgressBar(i, toBuild.length)} ${colors.dim}(${i}/${toBuild.length})${colors.reset}`,
+ );
+ }
+
+ const pluginStartTime = Date.now();
+ let spinner;
+
+ try {
+ if (!plugin.endsWith(".ts")) {
+ spinner = logger.plugin.building(plugin);
+ spinner.start();
+
+ const result = await buildPlugin(plugin);
+ const buildTime = Date.now() - pluginStartTime;
+
+ spinner.stop();
+ logger.plugin.success(
+ plugin,
+ formatTime(buildTime),
+ result.size ? formatSize(result.size) : "",
+ );
+
+ buildResults.push({
+ name: plugin,
+ status: "success",
+ time: buildTime,
+ size: result.size || 0,
+ });
+
+ built++;
+ }
+ } catch (e) {
+ if (spinner) spinner.stop();
+
+ logger.plugin.error(plugin, isVerbose ? e.stack : e.message);
+
+ buildResults.push({
+ name: plugin,
+ status: "error",
+ time: Date.now() - pluginStartTime,
+ error: e.message,
+ });
+
+ failed++;
+ }
+}
+
+// Final progress
+if (!isWatch) {
+ console.log(
+ `\n ${createProgressBar(toBuild.length, toBuild.length)} ${colors.dim}(${toBuild.length}/${toBuild.length})${colors.reset}`,
+ );
+}
+
+// Summary
+logger.section("Build Summary");
+
+const totalTime = Date.now() - startTime;
+logger.summary({
+ built,
+ failed,
+ total: toBuild.length,
+ totalTime,
+ watchMode: isWatch,
+});
+
+// Results table
+if (buildResults.length > 0 && !isWatch) {
+ console.log();
+ const tableRows = buildResults.map((result) => [
+ result.name,
+ result.status === "success"
+ ? `${colors.brightGreen}${symbols.success} Success${colors.reset}`
+ : `${colors.brightRed}${symbols.error} Failed${colors.reset}`,
+ formatTime(result.time),
+ result.size ? formatSize(result.size) : `${colors.dim}N/A${colors.reset}`,
+ ]);
+
+ logger.table(["Plugin", "Status", "Time", "Size"], tableRows);
+}
+
+// Final status
+console.log();
+if (failed > 0) {
+ logger.error(
+ `Build completed with ${failed} error(s) ${symbols.bullet} ${built} succeeded`,
+ symbols.warning,
+ );
+} else {
+ logger.success(
+ `All plugins built successfully! ${symbols.sparkles}`,
+ symbols.fire,
+ );
+}
+
+// Watch mode notification
+if (isWatch) {
+ console.log();
+ logger.info(
+ `Watching for changes... ${colors.dim}Press ${colors.reset}${colors.bold}Ctrl+C${colors.reset}${colors.dim} to exit${colors.reset}`,
+ symbols.watching,
+ );
+}
+
+console.log();
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// 🔧 BUILD FUNCTION
+// ═══════════════════════════════════════════════════════════════════════════════
async function buildPlugin(plugin) {
- if (plugin.endsWith(".ts")) return;
+ if (plugin.endsWith(".ts")) return {};
- const manifest = Object.assign(
- JSON.parse(await readFile("./base_manifest.json")),
- JSON.parse(await readFile(`./plugins/${plugin}/manifest.json`))
- );
+ const manifest = Object.assign(
+ JSON.parse(await readFile("./base_manifest.json")),
+ JSON.parse(await readFile(`./plugins/${plugin}/manifest.json`)),
+ );
- const entry = "index.js";
- const outPath = `./dist/${plugin}/${entry}`;
+ const entry = "index.js";
+ const outPath = `./dist/${plugin}/${entry}`;
- /** @type {import("rollup").RollupOptions} */
- const options = {
- input: `./plugins/${plugin}/${manifest.main}`,
- output: {
- file: outPath,
- globals(id) {
- if (id.startsWith("@vendetta"))
- return id.substring(1).replace(/\//g, ".");
+ /** @type {import("rollup").RollupOptions} */
+ const options = {
+ input: `./plugins/${plugin}/${manifest.main}`,
+ output: {
+ file: outPath,
+ globals(id) {
+ if (id.startsWith("@vendetta"))
+ return id.substring(1).replace(/\//g, ".");
- const map = {
- "react": "window.React",
- "react-native": "vendetta.metro.common.ReactNative"
- };
+ const map = {
+ react: "window.React",
+ "react-native": "vendetta.metro.common.ReactNative",
+ };
- return map[id] || null;
+ return map[id] || null;
+ },
+ format: "iife",
+ compact: true,
+ exports: "named",
+ inlineDynamicImports: true,
+ },
+ onwarn: (warning) => {
+ if (
+ ![
+ "UNRESOLVED_IMPORT",
+ "MISSING_NAME_OPTION_FOR_IIFE_EXPORT",
+ "CIRCULAR_DEPENDENCY",
+ ].includes(warning.code) &&
+ isVerbose
+ ) {
+ logger.warning(`${plugin}: ${warning.message}`);
+ }
+ },
+ plugins: [
+ nodeResolve({
+ resolveOnly: (id) => !["react", "react-native"].includes(id),
+ }),
+ commonjs(),
+ {
+ name: "swc",
+ transform(code, id) {
+ return swc.transform(code, {
+ filename: id,
+ jsc: {
+ parser: {
+ syntax: "typescript",
+ tsx: true,
+ },
+ externalHelpers: true,
},
- format: "iife",
- compact: true,
- exports: "named",
- inlineDynamicImports: true,
- },
- onwarn: (warning) => {
- ![
- "UNRESOLVED_IMPORT",
- "MISSING_NAME_OPTION_FOR_IIFE_EXPORT",
- "CIRCULAR_DEPENDENCY"
- ].includes(warning.code) && console.warn(warning);
- },
- plugins: [
- nodeResolve({
- resolveOnly: (id) => !["react", "react-native"].includes(id)
- }),
- commonjs(),
- {
- name: "swc",
- transform(code, id) {
- return swc.transform(code, {
- filename: id,
- jsc: {
- parser: {
- syntax: "typescript",
- tsx: true,
- },
- externalHelpers: true,
- },
- env: {
- targets: "defaults",
- include: [
- "transform-classes",
- "transform-arrow-functions",
- ],
- },
- });
- },
+ env: {
+ targets: "defaults",
+ include: ["transform-classes", "transform-arrow-functions"],
},
- esbuild({ minify: true }),
- ]
- };
+ });
+ },
+ },
+ esbuild({ minify: true }),
+ ],
+ };
- const applyHash = async () => Object.assign(manifest, {
- hash: createHash("sha256").update(await readFile(outPath)).digest("hex"),
- main: entry
+ const applyHash = async () => {
+ const content = await readFile(outPath);
+ return Object.assign(manifest, {
+ hash: createHash("sha256").update(content).digest("hex"),
+ main: entry,
});
+ };
- if (!isWatch) {
- const bundle = await rollup(options);
- await bundle.write(options.output);
- await bundle.close();
+ if (!isWatch) {
+ const bundle = await rollup(options);
+ await bundle.write(options.output);
+ await bundle.close();
- await applyHash();
- await writeFile(`./dist/${plugin}/manifest.json`, JSON.stringify(manifest));
+ await applyHash();
+ await writeFile(`./dist/${plugin}/manifest.json`, JSON.stringify(manifest));
- console.log(`${plugin}: ` + "\x1b[32m" + "Build succeed!" + "\x1b[0m");
- return;
- }
+ // Get file size
+ const stats = await stat(outPath);
+ return { size: stats.size };
+ }
- const watcher = watch(options);
+ const watcher = watch(options);
- return await new Promise((resolve, reject) => {
- watcher.on("event", async (event) => {
- switch (event.code) {
- case "BUNDLE_END": {
- event.result.close();
+ return await new Promise((resolve, reject) => {
+ watcher.on("event", async (event) => {
+ switch (event.code) {
+ case "BUNDLE_START":
+ // Silent in watch mode, spinner handles this
+ break;
- await applyHash();
- await writeFile(`./dist/${plugin}/manifest.json`, JSON.stringify(manifest));
+ case "BUNDLE_END": {
+ event.result.close();
- console.log(`${plugin}: ` + "\x1b[32m" + `Build succeed! (${event.duration}ms)` + "\x1b[0m");
- resolve();
- break;
- }
- case "ERROR":
- case "FATAL":
- console.error(`${plugin}: ` + "\x1b[31m", "Failed! :(", "\x1b[0m");
- reject(event.error);
- break;
- }
- });
+ await applyHash();
+ await writeFile(
+ `./dist/${plugin}/manifest.json`,
+ JSON.stringify(manifest),
+ );
+
+ const buildTime = formatTime(event.duration || 0);
+ logger.plugin.watching(plugin, buildTime);
+ resolve({ size: 0 });
+ break;
+ }
+ case "ERROR":
+ case "FATAL":
+ logger.plugin.error(
+ plugin,
+ isVerbose ? event.error.stack : event.error.message,
+ );
+ reject(event.error);
+ break;
+ }
});
+ });
}
diff --git a/plugins/Last.fm/manifest.json b/plugins/Last.fm/manifest.json
index 9361d91..0de6420 100644
--- a/plugins/Last.fm/manifest.json
+++ b/plugins/Last.fm/manifest.json
@@ -1,6 +1,16 @@
{
"name": "Last.fm",
"description": "Show off your last.fm status on your Discord profile.",
+ "authors": [
+ {
+ "name": "pylix",
+ "id": "492949202121261067"
+ },
+ {
+ "name": "kmmiio99o",
+ "id": "879393496627306587"
+ }
+ ],
"main": "src/index.tsx",
"vendetta": {
"icon": "ic_stage_music"
diff --git a/plugins/Last.fm/src/changelog.ts b/plugins/Last.fm/src/changelog.ts
index ad14750..a83d620 100644
--- a/plugins/Last.fm/src/changelog.ts
+++ b/plugins/Last.fm/src/changelog.ts
@@ -5,38 +5,38 @@ interface ChangelogEntry {
}
export const changelog: ChangelogEntry[] = [
- {
- version: "2.3.0",
- date: "2025-09-19",
- changes: [
- "1. I TRIED SO HARD 2. remove backend error 3. factor settings tab 4. factor V2 5. factor V3 6. factor V4 7. factor V5 8. factor V6 9. Full settings refactor :3",
- ],
- },
- {
- version: "2.2.0",
- date: "2025-09-13",
- changes: ["Remember kids! Do not sniff crack!"],
- },
- {
- version: "2.0.0",
- date: "2025-09-13",
- changes: ["Fix settings scrolling"],
- },
- {
- version: "1.3.0",
- date: "2025-09-13",
- changes: ["fix reanimated"],
- },
- {
- version: "1.2.0",
- date: "2025-09-13",
- changes: ["test"],
- },
- {
- version: "1.1.0",
- date: "2025-09-12",
- changes: ["fixed reanimated issue??"],
- },
+ {
+ version: "2.3.0",
+ date: "2025-09-19",
+ changes: [
+ "1. I TRIED SO HARD 2. remove backend error 3. factor settings tab 4. factor V2 5. factor V3 6. factor V4 7. factor V5 8. factor V6 9. Full settings refactor :3",
+ ],
+ },
+ {
+ version: "2.2.0",
+ date: "2025-09-13",
+ changes: ["Remember kids! Do not sniff crack!"],
+ },
+ {
+ version: "2.0.0",
+ date: "2025-09-13",
+ changes: ["Fix settings scrolling"],
+ },
+ {
+ version: "1.3.0",
+ date: "2025-09-13",
+ changes: ["fix reanimated"],
+ },
+ {
+ version: "1.2.0",
+ date: "2025-09-13",
+ changes: ["test"],
+ },
+ {
+ version: "1.1.0",
+ date: "2025-09-12",
+ changes: ["fixed reanimated issue??"],
+ },
];
export const currentVersion = "2.3.0";
diff --git a/plugins/Last.fm/src/constants.ts b/plugins/Last.fm/src/constants.ts
index e96e18d..f4e7372 100644
--- a/plugins/Last.fm/src/constants.ts
+++ b/plugins/Last.fm/src/constants.ts
@@ -1,35 +1,35 @@
const Constants = {
- DEFAULT_APP_NAME: "Music",
- DEFAULT_TIME_INTERVAL: 5,
- /** The application ID of the Discord's developer app */
- APPLICATION_ID: "1054951789318909972",
- /** The minimum update interval in seconds to prevent rate limiting */
- MIN_UPDATE_INTERVAL: 3,
- /** Maximum retry attempts for API calls */
- MAX_RETRY_ATTEMPTS: 3,
- /** Retry delay in milliseconds */
- RETRY_DELAY: 5000,
- /** Base URL for Last.fm API */
- LFM_API_BASE_URL: "http://ws.audioscrobbler.com/2.0",
- /** These are the default album covers that are used by Last.fm */
- LFM_DEFAULT_COVER_HASHES: [
- "2a96cbd8b46e442fc41c2b86b821562f",
- "c6f59c1e5e7240a4c0d427abd71f3dbb",
- ],
- /** Default settings */
- DEFAULT_SETTINGS: {
- username: "",
- apiKey: "",
- appName: "Music",
- timeInterval: 5,
- showTimestamp: true,
- listeningTo: true,
- ignoreSpotify: true,
- verboseLogging: false,
- } /** GitHub repository URL */,
- GITHUB_URL: "https://github.com/kmmiio99o/letup",
- /** GitHub commits URL */
- GITHUB_COMMITS_URL: "https://github.com/kmmiio99o/letup/commits/main/",
+ DEFAULT_APP_NAME: "Music",
+ DEFAULT_TIME_INTERVAL: 5,
+ /** The application ID of the Discord's developer app */
+ APPLICATION_ID: "1054951789318909972",
+ /** The minimum update interval in seconds to prevent rate limiting */
+ MIN_UPDATE_INTERVAL: 3,
+ /** Maximum retry attempts for API calls */
+ MAX_RETRY_ATTEMPTS: 3,
+ /** Retry delay in milliseconds */
+ RETRY_DELAY: 5000,
+ /** Base URL for Last.fm API */
+ LFM_API_BASE_URL: "http://ws.audioscrobbler.com/2.0",
+ /** These are the default album covers that are used by Last.fm */
+ LFM_DEFAULT_COVER_HASHES: [
+ "2a96cbd8b46e442fc41c2b86b821562f",
+ "c6f59c1e5e7240a4c0d427abd71f3dbb",
+ ],
+ /** Default settings */
+ DEFAULT_SETTINGS: {
+ username: "",
+ apiKey: "",
+ appName: "Music",
+ timeInterval: 5,
+ showTimestamp: true,
+ listeningTo: true,
+ ignoreSpotify: true,
+ verboseLogging: false,
+ } /** GitHub repository URL */,
+ GITHUB_URL: "https://github.com/kmmiio99o/vd-plugins",
+ /** GitHub commits URL */
+ GITHUB_COMMITS_URL: "https://github.com/kmmiio99o/vd-plugins/commits/main/",
} as const;
export default Constants;
diff --git a/plugins/Last.fm/src/index.tsx b/plugins/Last.fm/src/index.tsx
index 499d8ac..5f751ea 100644
--- a/plugins/Last.fm/src/index.tsx
+++ b/plugins/Last.fm/src/index.tsx
@@ -17,10 +17,10 @@ import Settings from "./ui/pages/Settings";
const { FormText } = Forms;
export const pluginState = {
- pluginStopped: false,
- lastActivity: undefined,
- updateInterval: undefined,
- lastTrackUrl: undefined,
+ pluginStopped: false,
+ lastActivity: undefined,
+ updateInterval: undefined,
+ lastTrackUrl: undefined,
} as {
pluginStopped: boolean;
lastActivity?: any;
@@ -31,18 +31,18 @@ export const pluginState = {
// Initialize default settings
const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS;
for (const key in defaultSettings) {
- plugin.storage[key] =
+ plugin.storage[key] =
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings];
}
export const currentSettings = new Proxy(plugin.storage, {
- get(target, prop) {
- return target[prop];
- },
- set(target, prop, value) {
- target[prop] = value;
- return true;
- },
+ get(target, prop) {
+ return target[prop];
+ },
+ set(target, prop, value) {
+ target[prop] = value;
+ return true;
+ },
});
// Connection status tracking
@@ -51,62 +51,62 @@ const MAX_CONNECTION_ATTEMPTS = 3;
const RECONNECT_DELAY = 5000;
async function tryInitialize() {
- try {
- await initialize();
- connectionAttempts = 0;
- } catch (error) {
- console.error("[Last.fm] Initialization error:", error);
- connectionAttempts++;
+ try {
+ await initialize();
+ connectionAttempts = 0;
+ } catch (error) {
+ console.error("[Last.fm] Initialization error:", error);
+ connectionAttempts++;
- if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
- showToast("Retrying connection...", getAssetIDByName("ic_clock"));
- setTimeout(tryInitialize, RECONNECT_DELAY);
- } else {
- showToast("Failed to connect to Last.fm", getAssetIDByName("Small"));
+ if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
+ showToast("Retrying connection...", getAssetIDByName("ic_clock"));
+ setTimeout(tryInitialize, RECONNECT_DELAY);
+ } else {
+ showToast("Failed to connect to Last.fm", getAssetIDByName("Small"));
+ }
}
- }
}
export default {
- onLoad() {
- pluginState.pluginStopped = false;
+ onLoad() {
+ pluginState.pluginStopped = false;
- if (!currentSettings.username || !currentSettings.apiKey) {
- showToast(
- "Please configure Last.fm username and API key in settings",
- getAssetIDByName("ic_warning"),
- );
- return;
- }
-
- if (UserStore.getCurrentUser()) {
- tryInitialize();
- } else {
- const waitForUser = () => {
- if (UserStore.getCurrentUser()) {
- tryInitialize();
- FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
+ if (!currentSettings.username || !currentSettings.apiKey) {
+ showToast(
+ "Please configure Last.fm username and API key in settings",
+ getAssetIDByName("ic_warning"),
+ );
+ return;
}
- };
- FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
- }
- },
- onUnload() {
- pluginState.pluginStopped = true;
- stop();
- },
- // Handle settings updates
- onSettingsUpdate(newSettings: any) {
- Object.assign(currentSettings, newSettings);
- tryInitialize();
- },
- // Handle Discord reconnection
- onDiscordReconnect() {
- if (!pluginState.pluginStopped) {
- tryInitialize();
- }
- },
- // Settings component
- settings: Settings,
+ if (UserStore.getCurrentUser()) {
+ tryInitialize();
+ } else {
+ const waitForUser = () => {
+ if (UserStore.getCurrentUser()) {
+ tryInitialize();
+ FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
+ }
+ };
+
+ FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
+ }
+ },
+ onUnload() {
+ pluginState.pluginStopped = true;
+ stop();
+ },
+ // Handle settings updates
+ onSettingsUpdate(newSettings: any) {
+ Object.assign(currentSettings, newSettings);
+ tryInitialize();
+ },
+ // Handle Discord reconnection
+ onDiscordReconnect() {
+ if (!pluginState.pluginStopped) {
+ tryInitialize();
+ }
+ },
+ // Settings component
+ settings: Settings,
};
diff --git a/plugins/Last.fm/src/manager.ts b/plugins/Last.fm/src/manager.ts
index 4ed74f9..3ca14d3 100644
--- a/plugins/Last.fm/src/manager.ts
+++ b/plugins/Last.fm/src/manager.ts
@@ -9,9 +9,9 @@ import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
import { setDebugInfo } from "./utils/debug";
function formatTime(seconds: number): string {
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = Math.floor(seconds % 60);
- return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = Math.floor(seconds % 60);
+ return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}
import { lastfmClient } from "./utils/lastfm";
@@ -23,210 +23,210 @@ enum ActivityType {
}
const verboseLog = (...message: any[]) =>
- currentSettings.verboseLogging && console.log("[Last.fm]", ...message);
+ currentSettings.verboseLogging && console.log("[Last.fm]", ...message);
class PluginManager {
- private static instance: PluginManager;
- private updateTimer?: NodeJS.Timer;
- private reconnectTimer?: NodeJS.Timer;
- private consecutiveFailures: number = 0;
- private isReconnecting: boolean = false;
+ private static instance: PluginManager;
+ private updateTimer?: NodeJS.Timer;
+ private reconnectTimer?: NodeJS.Timer;
+ private consecutiveFailures = 0;
+ private isReconnecting = false;
- private constructor() {}
+ private constructor() {}
- public static getInstance(): PluginManager {
- if (!PluginManager.instance) {
- PluginManager.instance = new PluginManager();
+ public static getInstance(): PluginManager {
+ if (!PluginManager.instance) {
+ PluginManager.instance = new PluginManager();
+ }
+ return PluginManager.instance;
}
- return PluginManager.instance;
- }
- /**
+ /**
* Updates the Discord activity with the current Last.fm track
*/
- private async updateActivity() {
- if (pluginState.pluginStopped) {
- verboseLog("Plugin is stopped, skipping update");
- this.stopUpdates();
- return;
- }
-
- verboseLog("Fetching last track...");
-
- try {
- if (!currentSettings.username || !currentSettings.apiKey) {
- throw new Error("Username or API key not set");
- }
-
- if (currentSettings.ignoreSpotify) {
- const spotifyActivity = SelfPresenceStore.findActivity(
- (act) => act.sync_id,
- );
- if (spotifyActivity) {
- verboseLog("Spotify is currently playing, clearing activity");
- setDebugInfo("isSpotifyIgnored", true);
- clearActivity();
- return;
+ private async updateActivity() {
+ if (pluginState.pluginStopped) {
+ verboseLog("Plugin is stopped, skipping update");
+ this.stopUpdates();
+ return;
}
- setDebugInfo("isSpotifyIgnored", false);
- }
- const lastTrack = await lastfmClient.fetchLatestScrobble();
- setDebugInfo("lastTrack", lastTrack);
+ verboseLog("Fetching last track...");
- if (!lastTrack.nowPlaying) {
- verboseLog("Last track is not currently playing");
- clearActivity();
- return;
- }
+ try {
+ if (!currentSettings.username || !currentSettings.apiKey) {
+ throw new Error("Username or API key not set");
+ }
- if (pluginState.lastTrackUrl === lastTrack.url) {
- verboseLog("Track hasn't changed");
- return;
- }
+ if (currentSettings.ignoreSpotify) {
+ const spotifyActivity = SelfPresenceStore.findActivity(
+ (act) => act.sync_id,
+ );
+ if (spotifyActivity) {
+ verboseLog("Spotify is currently playing, clearing activity");
+ setDebugInfo("isSpotifyIgnored", true);
+ clearActivity();
+ return;
+ }
+ setDebugInfo("isSpotifyIgnored", false);
+ }
- const activity: Activity = {
- name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
- flags: 0,
- type: currentSettings.listeningTo
- ? ActivityType.LISTENING
- : ActivityType.PLAYING,
- details: lastTrack.name,
- state: `by ${lastTrack.artist}`,
- status_display_type: 1,
- application_id: Constants.APPLICATION_ID,
- };
+ const lastTrack = await lastfmClient.fetchLatestScrobble();
+ setDebugInfo("lastTrack", lastTrack);
- // Handle dynamic app name
- if (activity.name.includes("{{")) {
- for (const key in lastTrack) {
- activity.name = activity.name.replace(
- `{{${key}}}`,
- lastTrack[key as keyof typeof lastTrack] || "",
- );
+ if (!lastTrack.nowPlaying) {
+ verboseLog("Last track is not currently playing");
+ clearActivity();
+ return;
+ }
+
+ if (pluginState.lastTrackUrl === lastTrack.url) {
+ verboseLog("Track hasn't changed");
+ return;
+ }
+
+ const activity: Activity = {
+ name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
+ flags: 0,
+ type: currentSettings.listeningTo
+ ? ActivityType.LISTENING
+ : ActivityType.PLAYING,
+ details: lastTrack.name,
+ state: `by ${lastTrack.artist}`,
+ status_display_type: 1,
+ application_id: Constants.APPLICATION_ID,
+ };
+
+ // Handle dynamic app name
+ if (activity.name.includes("{{")) {
+ for (const key in lastTrack) {
+ activity.name = activity.name.replace(
+ `{{${key}}}`,
+ lastTrack[key as keyof typeof lastTrack] || "",
+ );
+ }
+ }
+
+ // Set timestamps if enabled
+ // Use `from` and `to` from track.getInfo
+ if (currentSettings.showTimestamp && typeof lastTrack.from === "number") {
+ activity.timestamps = {
+ start: lastTrack.from * 1000,
+ ...(typeof lastTrack.to === "number" && { end: lastTrack.to * 1000 }),
+ };
+ }
+
+ // Handle album art
+ if (lastTrack.album) {
+ const asset = await fetchAsset([lastTrack.albumArt]);
+ if (asset[0]) {
+ activity.assets = {
+ large_image: asset[0],
+ large_text: `on ${lastTrack.album}${
+ currentSettings.showTimestamp && lastTrack.to
+ ? ` • ${formatTime((lastTrack.to - lastTrack.from) / 1000)}`
+ : ""
+ }`,
+ };
+ }
+ }
+
+ verboseLog("Setting activity:", activity);
+ setDebugInfo("lastActivity", activity);
+
+ await sendRequest(activity);
+ pluginState.lastTrackUrl = lastTrack.url;
+ pluginState.lastActivity = activity;
+ this.consecutiveFailures = 0;
+
+ verboseLog("Activity set successfully!");
+ } catch (error) {
+ console.error("[Last.fm] Update error:", error);
+ this.handleError(error);
}
- }
+ }
- // Set timestamps if enabled
- // Use `from` and `to` from track.getInfo
- if (currentSettings.showTimestamp && typeof lastTrack.from === "number") {
- activity.timestamps = {
- start: lastTrack.from * 1000,
- ...(typeof lastTrack.to === "number" && { end: lastTrack.to * 1000 }),
- };
- }
+ private handleError(error: Error) {
+ this.consecutiveFailures++;
+ setDebugInfo("lastError", error);
- // Handle album art
- if (lastTrack.album) {
- const asset = await fetchAsset([lastTrack.albumArt]);
- if (asset[0]) {
- activity.assets = {
- large_image: asset[0],
- large_text: `on ${lastTrack.album}${
- currentSettings.showTimestamp && lastTrack.to
- ? ` • ${formatTime((lastTrack.to - lastTrack.from) / 1000)}`
- : ""
- }`,
- };
+ if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
+ verboseLog(
+ `Failed ${this.consecutiveFailures} times, initiating reconnection...`,
+ );
+ this.startReconnection();
}
- }
-
- verboseLog("Setting activity:", activity);
- setDebugInfo("lastActivity", activity);
-
- await sendRequest(activity);
- pluginState.lastTrackUrl = lastTrack.url;
- pluginState.lastActivity = activity;
- this.consecutiveFailures = 0;
-
- verboseLog("Activity set successfully!");
- } catch (error) {
- console.error("[Last.fm] Update error:", error);
- this.handleError(error);
}
- }
- private handleError(error: Error) {
- this.consecutiveFailures++;
- setDebugInfo("lastError", error);
+ private startReconnection() {
+ if (this.isReconnecting) return;
- if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
- verboseLog(
- `Failed ${this.consecutiveFailures} times, initiating reconnection...`,
- );
- this.startReconnection();
+ this.isReconnecting = true;
+ this.stopUpdates();
+
+ verboseLog("Starting reconnection process");
+ this.reconnectTimer = setInterval(() => {
+ verboseLog("Attempting to reconnect...");
+ this.initialize()
+ .then(() => {
+ verboseLog("Reconnection successful");
+ this.stopReconnection();
+ })
+ .catch((error) => {
+ verboseLog("Reconnection failed:", error);
+ });
+ }, Constants.RETRY_DELAY);
}
- }
- private startReconnection() {
- if (this.isReconnecting) return;
-
- this.isReconnecting = true;
- this.stopUpdates();
-
- verboseLog("Starting reconnection process");
- this.reconnectTimer = setInterval(() => {
- verboseLog("Attempting to reconnect...");
- this.initialize()
- .then(() => {
- verboseLog("Reconnection successful");
- this.stopReconnection();
- })
- .catch((error) => {
- verboseLog("Reconnection failed:", error);
- });
- }, Constants.RETRY_DELAY);
- }
-
- private stopReconnection() {
- if (this.reconnectTimer) {
- clearInterval(this.reconnectTimer);
- this.reconnectTimer = undefined;
+ private stopReconnection() {
+ if (this.reconnectTimer) {
+ clearInterval(this.reconnectTimer);
+ this.reconnectTimer = undefined;
+ }
+ this.isReconnecting = false;
+ this.consecutiveFailures = 0;
}
- this.isReconnecting = false;
- this.consecutiveFailures = 0;
- }
- /**
+ /**
* Stops all update and reconnection timers
*/
- private stopUpdates() {
- if (this.updateTimer) {
- clearInterval(this.updateTimer);
- this.updateTimer = undefined;
+ private stopUpdates() {
+ if (this.updateTimer) {
+ clearInterval(this.updateTimer);
+ this.updateTimer = undefined;
+ }
}
- }
- /**
+ /**
* Initializes the plugin and starts the update loop
*/
- public async initialize() {
- if (pluginState.pluginStopped) {
- throw new Error("Plugin is stopped");
+ public async initialize() {
+ if (pluginState.pluginStopped) {
+ throw new Error("Plugin is stopped");
+ }
+
+ this.stopUpdates();
+ await this.updateActivity();
+
+ const interval = Math.max(
+ (Number(currentSettings.timeInterval) ||
+ Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
+ Constants.MIN_UPDATE_INTERVAL * 1000,
+ );
+
+ this.updateTimer = setInterval(() => this.updateActivity(), interval);
+ verboseLog(`Update timer started with interval: ${interval}ms`);
}
- this.stopUpdates();
- await this.updateActivity();
-
- const interval = Math.max(
- (Number(currentSettings.timeInterval) ||
- Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
- Constants.MIN_UPDATE_INTERVAL * 1000,
- );
-
- this.updateTimer = setInterval(() => this.updateActivity(), interval);
- verboseLog(`Update timer started with interval: ${interval}ms`);
- }
-
- /**
+ /**
* Stops the plugin and cleans up
*/
- public stop() {
- pluginState.pluginStopped = true;
- this.stopUpdates();
- this.stopReconnection();
- clearActivity();
- }
+ public stop() {
+ pluginState.pluginStopped = true;
+ this.stopUpdates();
+ this.stopReconnection();
+ clearActivity();
+ }
}
// Export singleton instance methods
diff --git a/plugins/Last.fm/src/ui/ChangelogView.tsx b/plugins/Last.fm/src/ui/ChangelogView.tsx
index e8422cd..c3c3344 100644
--- a/plugins/Last.fm/src/ui/ChangelogView.tsx
+++ b/plugins/Last.fm/src/ui/ChangelogView.tsx
@@ -7,74 +7,74 @@ import { React } from "@vendetta/metro/common";
import { findByProps } from "@vendetta/metro";
const { TableRowGroup, TableRow, Stack } = findByProps(
- "TableSwitchRow",
- "TableRowGroup",
- "Stack",
- "TableRow",
+ "TableSwitchRow",
+ "TableRowGroup",
+ "Stack",
+ "TableRow",
);
const { ScrollView } = General;
export default function ChangelogView() {
- return (
-
-
-
- {
- clipboard.setString(currentVersion);
- showToast(
- "Version copied to clipboard",
- getAssetIDByName("toast_copy_link"),
- );
- }}
- >
-
+ return (
+
+
+
+ {
+ clipboard.setString(currentVersion);
+ showToast(
+ "Version copied to clipboard",
+ getAssetIDByName("toast_copy_link"),
+ );
+ }}
+ >
+
Copy
-
-
- }
- />
-
+
+
+ }
+ />
+
-
- {changelog.map((entry, index) => (
-
-
-
+
+ {changelog.map((entry, index) => (
+
+
+
Version {entry.version}
-
-
- {entry.date}
-
-
- {entry.changes.map((change, changeIndex) => (
-
+
+
+ {entry.date}
+
+
+ {entry.changes.map((change, changeIndex) => (
+
• {change}
-
- ))}
+
+ ))}
+
+ ))}
+
- ))}
-
-
-
- );
+
+ );
}
diff --git a/plugins/Last.fm/src/ui/pages/Debug.tsx b/plugins/Last.fm/src/ui/pages/Debug.tsx
index 43aa31f..2e65635 100644
--- a/plugins/Last.fm/src/ui/pages/Debug.tsx
+++ b/plugins/Last.fm/src/ui/pages/Debug.tsx
@@ -3,50 +3,50 @@ import { Codeblock, Forms } from "@vendetta/ui/components";
import { ScrollView, Text } from "react-native";
import { useEffect } from "react";
import {
- useDebugInfo,
- logComponentMount,
- logComponentError,
+ useDebugInfo,
+ logComponentMount,
+ logComponentError,
} from "../../utils/debug";
const { FormText } = Forms;
export default React.memo(function Debug() {
- // Log component mount
- useEffect(() => {
- logComponentMount("Debug");
- }, []);
+ // Log component mount
+ useEffect(() => {
+ logComponentMount("Debug");
+ }, []);
- try {
- const debugInfo = useDebugInfo();
+ try {
+ const debugInfo = useDebugInfo();
- // Safety check for debugInfo
- if (!debugInfo) {
- logComponentError("Debug", "useDebugInfo returned null or undefined");
- return (
-
-
+ // Safety check for debugInfo
+ if (!debugInfo) {
+ logComponentError("Debug", "useDebugInfo returned null or undefined");
+ return (
+
+
No debug information available
-
-
- );
- }
+
+
+ );
+ }
- return (
-
-
- {debugInfo}
-
-
- );
- } catch (error) {
- logComponentError("Debug", error);
- console.error("Debug component error:", error);
- return (
-
-
+ return (
+
+
+ {debugInfo}
+
+
+ );
+ } catch (error) {
+ logComponentError("Debug", error);
+ console.error("Debug component error:", error);
+ return (
+
+
Error loading debug information: {String(error)}
-
-
- );
- }
+
+
+ );
+ }
});
diff --git a/plugins/Last.fm/src/ui/pages/Settings.tsx b/plugins/Last.fm/src/ui/pages/Settings.tsx
index c1fb4de..bb6c9c3 100644
--- a/plugins/Last.fm/src/ui/pages/Settings.tsx
+++ b/plugins/Last.fm/src/ui/pages/Settings.tsx
@@ -8,10 +8,10 @@ import { findByProps } from "@vendetta/metro";
const { ScrollView } = findByProps("ScrollView");
const { TableRowGroup, TableSwitchRow, Stack, TableRow } = findByProps(
- "TableSwitchRow",
- "TableRowGroup",
- "Stack",
- "TableRow",
+ "TableSwitchRow",
+ "TableRowGroup",
+ "Stack",
+ "TableRow",
);
const { TextInput } = findByProps("TextInput");
const { FormText } = findByProps("FormText");
@@ -26,187 +26,187 @@ const set = (k: string, v: any) => (plugin.storage[k] = v);
// Define the Settings component
export default function Settings() {
- const [_, forceUpdate] = React.useReducer((x) => ~x, 0);
- const update = () => forceUpdate();
+ const [_, forceUpdate] = React.useReducer((x) => ~x, 0);
+ const update = () => forceUpdate();
- const [loading, setLoading] = React.useState(false);
+ const [loading, setLoading] = React.useState(false);
- const testConnection = async () => {
- const username = get("username");
- const apiKey = get("apiKey");
+ const testConnection = async () => {
+ const username = get("username");
+ const apiKey = get("apiKey");
- if (!username || !apiKey) {
- showToast(
- "Please enter both username and API key",
- getAssetIDByName("Small"),
- );
- return;
- }
+ if (!username || !apiKey) {
+ showToast(
+ "Please enter both username and API key",
+ getAssetIDByName("Small"),
+ );
+ return;
+ }
- setLoading(true);
- try {
- await lastfmClient.fetchLatestScrobble();
- showToast("Connection successful!", getAssetIDByName("Check"));
- await initialize();
- } catch (error) {
- showToast(
- `Connection failed: ${error.message}`,
- getAssetIDByName("Small"),
- );
- } finally {
- setLoading(false);
- }
- };
+ setLoading(true);
+ try {
+ await lastfmClient.fetchLatestScrobble();
+ showToast("Connection successful!", getAssetIDByName("Check"));
+ await initialize();
+ } catch (error) {
+ showToast(
+ `Connection failed: ${error.message}`,
+ getAssetIDByName("Small"),
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
- return (
-
-
-
-
- {
- set("username", v);
- update();
- }}
- isClearable
- />
- {
- set("apiKey", v);
- update();
- }}
- secureTextEntry={true}
- isClearable
- />
- Linking.openURL("https://www.last.fm/api/")}
- />
-
- : }
- onPress={testConnection}
- disabled={loading}
- />
-
+ return (
+
+
+
+
+ {
+ set("username", v);
+ update();
+ }}
+ isClearable
+ />
+ {
+ set("apiKey", v);
+ update();
+ }}
+ secureTextEntry={true}
+ isClearable
+ />
+ Linking.openURL("https://www.last.fm/api/")}
+ />
+
+ : }
+ onPress={testConnection}
+ disabled={loading}
+ />
+
-
-
- {
- set("appName", v);
- update();
- }}
- isClearable
- />
- {
- const interval = Number(v);
- if (interval >= Constants.MIN_UPDATE_INTERVAL) {
- set("timeInterval", interval);
- update();
- } else {
- showToast(
- `Minimum interval is ${Constants.MIN_UPDATE_INTERVAL} seconds`,
- getAssetIDByName("Small"),
- );
- }
- }}
- keyboardType="numeric"
- isClearable
- />
-
-
+
+
+ {
+ set("appName", v);
+ update();
+ }}
+ isClearable
+ />
+ {
+ const interval = Number(v);
+ if (interval >= Constants.MIN_UPDATE_INTERVAL) {
+ set("timeInterval", interval);
+ update();
+ } else {
+ showToast(
+ `Minimum interval is ${Constants.MIN_UPDATE_INTERVAL} seconds`,
+ getAssetIDByName("Small"),
+ );
+ }
+ }}
+ keyboardType="numeric"
+ isClearable
+ />
+
+
-
- {
- set("listeningTo", value);
- update();
- }}
- />
- {
- set("showTimestamp", value);
- update();
- }}
- />
- {
- set("ignoreSpotify", value);
- update();
- }}
- />
- {
- set("verboseLogging", value);
- update();
- }}
- />
-
+
+ {
+ set("listeningTo", value);
+ update();
+ }}
+ />
+ {
+ set("showTimestamp", value);
+ update();
+ }}
+ />
+ {
+ set("ignoreSpotify", value);
+ update();
+ }}
+ />
+ {
+ set("verboseLogging", value);
+ update();
+ }}
+ />
+
-
-
-
-
+
+
+
+
-
-
- }
- onPress={() => {
- Linking.openURL(Constants.GITHUB_COMMITS_URL);
- }}
- />
-
+
+
+ }
+ onPress={() => {
+ Linking.openURL(Constants.GITHUB_COMMITS_URL);
+ }}
+ />
+
- {/* Changelog Section */}
-
- {changelog.map((entry, index) => (
-
- ))}
-
-
-
- );
+ {/* Changelog Section */}
+
+ {changelog.map((entry, index) => (
+
+ ))}
+
+
+
+ );
}
diff --git a/plugins/Last.fm/src/utils/activity.ts b/plugins/Last.fm/src/utils/activity.ts
index 96ed16d..f0f7964 100644
--- a/plugins/Last.fm/src/utils/activity.ts
+++ b/plugins/Last.fm/src/utils/activity.ts
@@ -8,37 +8,37 @@ import { AssetManager } from "../modules";
/** Clears the user's activity */
export function clearActivity() {
- return sendRequest(null);
+ return sendRequest(null);
}
/** Sends the activity details to Discord */
export function sendRequest(activity: Activity | null) {
- if (pluginState.pluginStopped) {
- stop();
- activity = null;
- }
+ if (pluginState.pluginStopped) {
+ stop();
+ activity = null;
+ }
- pluginState.lastActivity = activity;
+ pluginState.lastActivity = activity;
- FluxDispatcher.dispatch({
- type: "LOCAL_ACTIVITY_UPDATE",
- activity: activity,
- pid: 2312,
- socketId: "Last.fm@Vendetta",
- });
+ FluxDispatcher.dispatch({
+ type: "LOCAL_ACTIVITY_UPDATE",
+ activity: activity,
+ pid: 2312,
+ socketId: "Last.fm@Vendetta",
+ });
}
/** Fetches a Discord application's asset */
export async function fetchAsset(
- asset: string[],
- appId: string = Constants.APPLICATION_ID,
+ asset: string[],
+ appId: string = Constants.APPLICATION_ID,
): Promise {
- if (!asset?.length) return [];
+ if (!asset?.length) return [];
- try {
- return await AssetManager.fetchAssetIds(appId, asset);
- } catch (error) {
- console.error("[Last.fm] Failed to fetch assets:", error);
- return [];
- }
+ try {
+ return await AssetManager.fetchAssetIds(appId, asset);
+ } catch (error) {
+ console.error("[Last.fm] Failed to fetch assets:", error);
+ return [];
+ }
}
diff --git a/plugins/Last.fm/src/utils/debug.ts b/plugins/Last.fm/src/utils/debug.ts
index 9646054..d64fab0 100644
--- a/plugins/Last.fm/src/utils/debug.ts
+++ b/plugins/Last.fm/src/utils/debug.ts
@@ -21,70 +21,70 @@ debugInfo.componentMountCount = 0;
debugInfo.settingsLoadAttempts = 0;
export function setDebugInfo(
- key: keyof typeof debugInfo,
- value: (typeof debugInfo)[typeof key],
+ key: keyof typeof debugInfo,
+ value: (typeof debugInfo)[typeof key],
) {
- debugInfo[key] = value;
- __forceUpdate?.();
+ debugInfo[key] = value;
+ __forceUpdate?.();
}
export function logComponentMount(componentName: string) {
- debugInfo.componentMountCount = (debugInfo.componentMountCount || 0) + 1;
- console.log(
- `[Last.fm Debug] Component mounted: ${componentName} (count: ${debugInfo.componentMountCount})`,
- );
+ debugInfo.componentMountCount = (debugInfo.componentMountCount || 0) + 1;
+ console.log(
+ `[Last.fm Debug] Component mounted: ${componentName} (count: ${debugInfo.componentMountCount})`,
+ );
}
export function logComponentError(componentName: string, error: any) {
- const errorMessage = `${componentName}: ${String(error)}`;
- debugInfo.componentMountErrors = debugInfo.componentMountErrors || [];
- debugInfo.componentMountErrors.push(errorMessage);
+ const errorMessage = `${componentName}: ${String(error)}`;
+ debugInfo.componentMountErrors = debugInfo.componentMountErrors || [];
+ debugInfo.componentMountErrors.push(errorMessage);
- if (String(error).includes("Reanimated")) {
- debugInfo.lastReanimatedError = errorMessage;
- }
+ if (String(error).includes("Reanimated")) {
+ debugInfo.lastReanimatedError = errorMessage;
+ }
- console.error(`[Last.fm Debug] Component error in ${componentName}:`, error);
- __forceUpdate?.();
+ console.error(`[Last.fm Debug] Component error in ${componentName}:`, error);
+ __forceUpdate?.();
}
export function logNavigationError(error: any) {
- debugInfo.lastNavigationError = String(error);
- console.error(`[Last.fm Debug] Navigation error:`, error);
- __forceUpdate?.();
+ debugInfo.lastNavigationError = String(error);
+ console.error("[Last.fm Debug] Navigation error:", error);
+ __forceUpdate?.();
}
export function incrementSettingsLoad() {
- debugInfo.settingsLoadAttempts = (debugInfo.settingsLoadAttempts || 0) + 1;
- console.log(
- `[Last.fm Debug] Settings load attempt: ${debugInfo.settingsLoadAttempts}`,
- );
+ debugInfo.settingsLoadAttempts = (debugInfo.settingsLoadAttempts || 0) + 1;
+ console.log(
+ `[Last.fm Debug] Settings load attempt: ${debugInfo.settingsLoadAttempts}`,
+ );
}
export function useDebugInfo(): string {
- try {
- [, __forceUpdate] = useReducer((x) => ~x, 0);
+ try {
+ [, __forceUpdate] = useReducer((x) => ~x, 0);
- // Add runtime information
- const runtimeInfo = {
- ...debugInfo,
- timestamp: new Date().toISOString(),
- reactVersion: require("react").version,
- hasReanimatedError: !!debugInfo.lastReanimatedError,
- totalErrors: debugInfo.componentMountErrors?.length || 0,
- };
+ // Add runtime information
+ const runtimeInfo = {
+ ...debugInfo,
+ timestamp: new Date().toISOString(),
+ reactVersion: require("react").version,
+ hasReanimatedError: !!debugInfo.lastReanimatedError,
+ totalErrors: debugInfo.componentMountErrors?.length || 0,
+ };
- return JSON.stringify(runtimeInfo, null, 4);
- } catch (error) {
- console.error("[Last.fm Debug] Error in useDebugInfo:", error);
- return JSON.stringify(
- {
- error: "Failed to generate debug info",
- errorMessage: String(error),
- timestamp: new Date().toISOString(),
- },
- null,
- 4,
- );
- }
+ return JSON.stringify(runtimeInfo, null, 4);
+ } catch (error) {
+ console.error("[Last.fm Debug] Error in useDebugInfo:", error);
+ return JSON.stringify(
+ {
+ error: "Failed to generate debug info",
+ errorMessage: String(error),
+ timestamp: new Date().toISOString(),
+ },
+ null,
+ 4,
+ );
+ }
}
diff --git a/plugins/Last.fm/src/utils/lastfm.ts b/plugins/Last.fm/src/utils/lastfm.ts
index 5403e66..fb7d621 100644
--- a/plugins/Last.fm/src/utils/lastfm.ts
+++ b/plugins/Last.fm/src/utils/lastfm.ts
@@ -44,161 +44,161 @@ interface LastFMTrack {
}
class LastFMClient {
- private static instance: LastFMClient;
- private retryCount: number = 0;
- private lastError: number = 0;
+ private static instance: LastFMClient;
+ private retryCount = 0;
+ private lastError = 0;
- private constructor() {}
+ private constructor() {}
- public static getInstance(): LastFMClient {
- if (!LastFMClient.instance) {
- LastFMClient.instance = new LastFMClient();
- }
- return LastFMClient.instance;
- }
-
- private validateSettings(): void {
- if (!currentSettings.username) {
- throw new Error("Last.fm username is not set");
- }
- if (!currentSettings.apiKey) {
- throw new Error("Last.fm API key is not set");
- }
- }
-
- private async handleError(error: LastFMError): Promise {
- this.lastError = error.error;
-
- const errorMessages: { [key: number]: string } = {
- 2: "Invalid API key",
- 11: "Service temporarily unavailable",
- 16: "Service temporarily unavailable",
- 29: "Rate limit exceeded",
- };
-
- const message =
- errorMessages[error.error] || error.message || "Unknown error";
- showToast(`Last.fm Error: ${message}`, getAssetIDByName("Small"));
-
- throw new Error(`Last.fm API Error ${error.error}: ${message}`);
- }
-
- private async makeRequest(
- params: Record,
- ): Promise {
- const queryParams = new URLSearchParams({
- ...params,
- api_key: currentSettings.apiKey,
- format: "json",
- }).toString();
-
- const response = await fetch(
- `${Constants.LFM_API_BASE_URL}?${queryParams}`,
- );
- const data: LastFMResponse = await response.json();
-
- if (!response.ok || data.error) {
- await this.handleError(data as LastFMError);
- }
-
- return data;
- }
-
- public async fetchLatestScrobble(): Promise<
- Track & { from: number; to: number | null }
- > {
- try {
- this.validateSettings();
-
- const data = await this.makeRequest({
- method: "user.getrecenttracks",
- user: currentSettings.username,
- limit: "1",
- extended: "1",
- });
-
- const lastTrack = data?.recenttracks?.track?.[0];
- setDebugInfo("lastAPIResponse", lastTrack);
-
- if (!lastTrack) {
- throw new Error("No tracks found");
- }
-
- // Reset retry count on successful request
- this.retryCount = 0;
-
- const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
- const from = isNowPlaying
- ? Math.floor(Date.now() / 1000)
- : parseInt(lastTrack?.date?.uts);
-
- let to: number | null = null;
- try {
- const trackInfo = await this.makeRequest({
- method: "track.getInfo",
- track: lastTrack.name,
- artist: lastTrack.artist.name,
- username: currentSettings.username,
- });
-
- const duration = parseInt(trackInfo?.track?.duration);
- if (duration > 0) {
- to = from + Math.floor(duration / 1000);
+ public static getInstance(): LastFMClient {
+ if (!LastFMClient.instance) {
+ LastFMClient.instance = new LastFMClient();
}
- } catch (err) {
- console.warn("Failed to fetch track duration", err);
- }
+ return LastFMClient.instance;
+ }
- return {
- name: lastTrack.name,
- artist: lastTrack.artist.name,
- album: lastTrack.album["#text"],
- albumArt: await this.handleAlbumCover(
- lastTrack.image?.find((x) => x.size === "large")?.["#text"],
- ),
- url: lastTrack.url,
- date: lastTrack.date?.["#text"] ?? "now",
- nowPlaying: isNowPlaying,
- loved: lastTrack.loved === "1",
- from,
- to,
- };
- } catch (error) {
- // Increment retry count and throw if we've exceeded max retries
- this.retryCount++;
- if (this.retryCount > Constants.MAX_RETRY_ATTEMPTS) {
+ private validateSettings(): void {
+ if (!currentSettings.username) {
+ throw new Error("Last.fm username is not set");
+ }
+ if (!currentSettings.apiKey) {
+ throw new Error("Last.fm API key is not set");
+ }
+ }
+
+ private async handleError(error: LastFMError): Promise {
+ this.lastError = error.error;
+
+ const errorMessages: { [key: number]: string } = {
+ 2: "Invalid API key",
+ 11: "Service temporarily unavailable",
+ 16: "Service temporarily unavailable",
+ 29: "Rate limit exceeded",
+ };
+
+ const message =
+ errorMessages[error.error] || error.message || "Unknown error";
+ showToast(`Last.fm Error: ${message}`, getAssetIDByName("Small"));
+
+ throw new Error(`Last.fm API Error ${error.error}: ${message}`);
+ }
+
+ private async makeRequest(
+ params: Record,
+ ): Promise {
+ const queryParams = new URLSearchParams({
+ ...params,
+ api_key: currentSettings.apiKey,
+ format: "json",
+ }).toString();
+
+ const response = await fetch(
+ `${Constants.LFM_API_BASE_URL}?${queryParams}`,
+ );
+ const data: LastFMResponse = await response.json();
+
+ if (!response.ok || data.error) {
+ await this.handleError(data as LastFMError);
+ }
+
+ return data;
+ }
+
+ public async fetchLatestScrobble(): Promise<
+ Track & { from: number; to: number | null }
+ > {
+ try {
+ this.validateSettings();
+
+ const data = await this.makeRequest({
+ method: "user.getrecenttracks",
+ user: currentSettings.username,
+ limit: "1",
+ extended: "1",
+ });
+
+ const lastTrack = data?.recenttracks?.track?.[0];
+ setDebugInfo("lastAPIResponse", lastTrack);
+
+ if (!lastTrack) {
+ throw new Error("No tracks found");
+ }
+
+ // Reset retry count on successful request
+ this.retryCount = 0;
+
+ const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
+ const from = isNowPlaying
+ ? Math.floor(Date.now() / 1000)
+ : parseInt(lastTrack?.date?.uts);
+
+ let to: number | null = null;
+ try {
+ const trackInfo = await this.makeRequest({
+ method: "track.getInfo",
+ track: lastTrack.name,
+ artist: lastTrack.artist.name,
+ username: currentSettings.username,
+ });
+
+ const duration = parseInt(trackInfo?.track?.duration);
+ if (duration > 0) {
+ to = from + Math.floor(duration / 1000);
+ }
+ } catch (err) {
+ console.warn("Failed to fetch track duration", err);
+ }
+
+ return {
+ name: lastTrack.name,
+ artist: lastTrack.artist.name,
+ album: lastTrack.album["#text"],
+ albumArt: await this.handleAlbumCover(
+ lastTrack.image?.find((x) => x.size === "large")?.["#text"],
+ ),
+ url: lastTrack.url,
+ date: lastTrack.date?.["#text"] ?? "now",
+ nowPlaying: isNowPlaying,
+ loved: lastTrack.loved === "1",
+ from,
+ to,
+ };
+ } catch (error) {
+ // Increment retry count and throw if we've exceeded max retries
+ this.retryCount++;
+ if (this.retryCount > Constants.MAX_RETRY_ATTEMPTS) {
+ this.retryCount = 0;
+ throw error;
+ }
+
+ // Wait before retrying
+ await new Promise((resolve) =>
+ setTimeout(resolve, Constants.RETRY_DELAY),
+ );
+ return this.fetchLatestScrobble();
+ }
+ }
+
+ private async handleAlbumCover(cover?: string): Promise {
+ if (!cover) return null;
+
+ // If the cover is a default one, return null
+ if (
+ Constants.LFM_DEFAULT_COVER_HASHES.some((hash) => cover.includes(hash))
+ ) {
+ return null;
+ }
+
+ return cover;
+ }
+
+ public getLastError(): number {
+ return this.lastError;
+ }
+
+ public resetRetryCount(): void {
this.retryCount = 0;
- throw error;
- }
-
- // Wait before retrying
- await new Promise((resolve) =>
- setTimeout(resolve, Constants.RETRY_DELAY),
- );
- return this.fetchLatestScrobble();
}
- }
-
- private async handleAlbumCover(cover?: string): Promise {
- if (!cover) return null;
-
- // If the cover is a default one, return null
- if (
- Constants.LFM_DEFAULT_COVER_HASHES.some((hash) => cover.includes(hash))
- ) {
- return null;
- }
-
- return cover;
- }
-
- public getLastError(): number {
- return this.lastError;
- }
-
- public resetRetryCount(): void {
- this.retryCount = 0;
- }
}
// Export a singleton instance
@@ -207,8 +207,8 @@ export const lastfmClient = LastFMClient.getInstance();
/** Fetches the latest user's scrobble */
export async function fetchLatestScrobble(): Promise<
Track & { from: number; to: number | null }
-> {
- return lastfmClient.fetchLatestScrobble();
+ > {
+ return lastfmClient.fetchLatestScrobble();
}
/**
@@ -216,5 +216,5 @@ export async function fetchLatestScrobble(): Promise<
* @param cover The album cover given by Last.fm
*/
export async function handleAlbumCover(cover: string): Promise {
- return lastfmClient.handleAlbumCover(cover);
+ return lastfmClient.handleAlbumCover(cover);
}
diff --git a/plugins/defs.d.ts b/plugins/defs.d.ts
index 1e93627..c475fcf 100644
--- a/plugins/defs.d.ts
+++ b/plugins/defs.d.ts
@@ -1,48 +1,74 @@
export type Activity = {
- name: string;
- application_id: string;
- flags: number;
- type: number;
- details?: string;
- state?: string;
- timestamps?: {
- _enabled?: boolean;
- start: number | string;
- end?: number | string;
- };
- assets: ActivityAssets;
- buttons: ActivityButton[];
+ name: string;
+ application_id: string;
+ flags: number;
+ type: number;
+ details?: string;
+ state?: string;
+ timestamps?: {
+ _enabled?: boolean;
+ start: number | string;
+ end?: number | string;
+ };
+ assets?: ActivityAssets;
+ buttons?: ActivityButton[];
+ status_display_type?: number;
};
export type ActivityButton = {
- label: string;
- url: string;
+ label: string;
+ url: string;
};
export type ActivityAssets = {
- large_image?: string;
- large_text?: string;
- small_image?: string;
- small_text?: string;
+ large_image?: string;
+ large_text?: string;
+ small_image?: string;
+ small_text?: string;
};
+export type ServiceType = "lastfm" | "librefm" | "listenbrainz";
+
export type LFMSettings = {
- appName: string;
- username: string;
- showTimestamp: boolean;
- timeInterval: number | string;
- listeningTo: boolean;
- ignoreSpotify: boolean;
- verboseLogging: boolean;
+ appName: string;
+ username: string;
+ apiKey: string;
+ showTimestamp: boolean;
+ timeInterval: number | string;
+ listeningTo: boolean;
+ ignoreSpotify: boolean;
+ verboseLogging: boolean;
+ service: ServiceType;
+ // Service-specific settings
+ librefmUsername?: string;
+ librefmApiKey?: string;
+ listenbrainzUsername?: string;
+ listenbrainzToken?: string;
};
export type Track = {
- name: string;
- artist: string;
- album: string;
- albumArt: string;
- url: string;
- date: string;
- nowPlaying: boolean;
- loved: boolean;
+ name: string;
+ artist: string;
+ album: string;
+ albumArt: string | null;
+ url: string;
+ date: string;
+ nowPlaying: boolean;
+ loved: boolean;
+ from: number;
+ to: number | null;
+ duration?: number;
+};
+
+export interface ServiceClient {
+ fetchLatestScrobble(): Promise