diff --git a/.eslintrc.json b/.eslintrc.json index a128ed3..e0ceb91 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,44 +1,36 @@ { - "env": { - "es2021": true, - "node": true + "env": { + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "react", - "@typescript-eslint" - ], - "rules": { - "indent": [ - "error", - 4, - { "SwitchCase": 1 } - ], - "quotes": [ - "error", - "double" - ], - "semi": [ - "error", - "always" - ], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/triple-slash-reference": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "react/react-in-jsx-scope": "off", - "eol-last": "error" + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint"], + "settings": { + "react": { + "version": "detect" } + }, + "rules": { + "indent": ["error", 4, { "SwitchCase": 1 }], + "quotes": ["error", "double"], + "semi": ["error", "always"], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/triple-slash-reference": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "react/react-in-jsx-scope": "off", + "eol-last": "error" + } } diff --git a/package.json b/package.json index 750821e..9343568 100644 --- a/package.json +++ b/package.json @@ -7,23 +7,26 @@ "serve": "node serve.mjs" }, "devDependencies": { - "@rollup/plugin-commonjs": "^24.1.0", - "@rollup/plugin-node-resolve": "^15.0.2", - "@swc/core": "^1.3.51", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-node-resolve": "^16.0.2", + "@swc/core": "^1.13.5", "@types/react": "^18.0.37", "@types/react-native": "^0.70.13", - "@typescript-eslint/eslint-plugin": "^5.59.0", - "@typescript-eslint/parser": "^5.59.0", - "esbuild": "^0.17.17", - "eslint": "^8.38.0", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "esbuild": "^0.25.10", + "eslint": "^9.37.0", "eslint-plugin-react": "^7.32.2", - "rollup": "^3.20.6", - "rollup-plugin-esbuild": "^5.0.0", - "typescript": "^4.9.5", - "vendetta-types": "^2.4.18" + "rollup": "^4.24.4", + "rollup-plugin-esbuild": "^6.1.1", + "typescript": "^5.7.2", + "vendetta-types": "^2.4.21" }, "dependencies": { - "@swc/helpers": "^0.5.0", + "@swc/helpers": "^0.5.17", "serve": "^14.2.5" + }, + "overrides": { + "@types/react": "^18.0.37" } } diff --git a/plugins/Commands/settings.tsx b/plugins/Commands/settings.tsx index 6fc6ca9..64617be 100644 --- a/plugins/Commands/settings.tsx +++ b/plugins/Commands/settings.tsx @@ -13,1401 +13,1401 @@ const { FormRow, FormSwitchRow } = Forms; // Initialize storage with default values storage.factSettings ??= { - sendAsReply: true, - includeCitation: false, + sendAsReply: true, + includeCitation: false, }; storage.listSettings ??= { - pluginListAlwaysDetailed: false, - themeListAlwaysDetailed: false, + pluginListAlwaysDetailed: false, + themeListAlwaysDetailed: false, }; storage.garySettings ??= { - imageSource: "gary", + imageSource: "gary", }; storage.enabledCommands ??= { - catfact: true, - dogfact: true, - useless: true, - petpet: true, - pluginList: true, - themeList: true, - konoself: true, - konosend: true, - firstmessage: true, - sysinfo: true, - spotifyTrack: true, - spotifyAlbum: true, - spotifyArtists: true, - spotifyCover: true, - gary: true, - ip: true, // IP command - enabled by default - lovefemboys: false, // Hidden command - disabled by default - nekoslife: false, // Hidden command - disabled by default + catfact: true, + dogfact: true, + useless: true, + petpet: true, + pluginList: true, + themeList: true, + konoself: true, + konosend: true, + firstmessage: true, + sysinfo: true, + spotifyTrack: true, + spotifyAlbum: true, + spotifyArtists: true, + spotifyCover: true, + gary: true, + ip: true, // IP command - enabled by default + lovefemboys: false, // Hidden command - disabled by default + nekoslife: false, // Hidden command - disabled by default }; storage.pendingRestart ??= false; // Hidden settings storage with NSFW bypass option storage.hiddenSettings ??= { - enabled: false, - visible: false, - konochanBypassNsfw: false, // New bypass option - disabled by default + enabled: false, + visible: false, + konochanBypassNsfw: false, // New bypass option - disabled by default }; // Better Table Row Group Component function BetterTableRowGroup({ - title, - icon, - children, - padding = false, + title, + icon, + children, + padding = false, }: React.PropsWithChildren<{ title?: string; icon?: number; padding?: boolean; }>) { - const styles = stylesheet.createThemedStyleSheet({ - main: { - backgroundColor: semanticColors.CARD_PRIMARY_BG, - borderColor: semanticColors.BORDER_FAINT, - borderWidth: 1, - borderRadius: 16, - overflow: "hidden", - flex: 1, - }, - titleContainer: { - marginBottom: 8, - marginHorizontal: 0, - marginTop: 8, - gap: 4, - flexDirection: "row", - alignItems: "center", - }, - icon: { - width: 16, - height: 16, - marginTop: 1.5, - tintColor: semanticColors.TEXT_MUTED, - }, - titleText: { - fontSize: 14, - fontWeight: "600", - color: semanticColors.TEXT_MUTED, - }, - }); + const styles = stylesheet.createThemedStyleSheet({ + main: { + backgroundColor: semanticColors.CARD_PRIMARY_BG, + borderColor: semanticColors.BORDER_FAINT, + borderWidth: 1, + borderRadius: 16, + overflow: "hidden", + flex: 1, + }, + titleContainer: { + marginBottom: 8, + marginHorizontal: 0, + marginTop: 8, + gap: 4, + flexDirection: "row", + alignItems: "center", + }, + icon: { + width: 16, + height: 16, + marginTop: 1.5, + tintColor: semanticColors.TEXT_MUTED, + }, + titleText: { + fontSize: 14, + fontWeight: "600", + color: semanticColors.TEXT_MUTED, + }, + }); - return ( - - {title && ( - - {icon && ( - - )} - {title.toUpperCase()} + return ( + + {title && ( + + {icon && ( + + )} + {title.toUpperCase()} + + )} + + {padding ? ( + + {children} + + ) : ( + children + )} + - )} - - {padding ? ( - - {children} - - ) : ( - children - )} - - - ); + ); } // Header Component with secret unlock function Header({ onHiddenUnlock }: { onHiddenUnlock?: () => void }) { - const [clickCounter, setClickCounter] = React.useState(0); - const [clickTimeout, setClickTimeout] = React.useState( - null, - ); - - const styles = stylesheet.createThemedStyleSheet({ - container: { - flexDirection: "column", - alignItems: "center", - paddingVertical: 24, - paddingHorizontal: 16, - }, - title: { - fontSize: 28, - fontWeight: "700", - color: semanticColors.TEXT_NORMAL, - marginBottom: 4, - }, - subtitle: { - fontSize: 16, - fontWeight: "500", - color: semanticColors.TEXT_MUTED, - }, - iconContainer: { - backgroundColor: semanticColors.BACKGROUND_SECONDARY, - borderRadius: 16, - padding: 12, - marginBottom: 16, - }, - icon: { - width: 32, - height: 32, - tintColor: semanticColors.TEXT_NORMAL, - }, - }); - - const handleIconPress = () => { - if (storage.hiddenSettings.enabled) { - storage.hiddenSettings.visible = !storage.hiddenSettings.visible; - showToast( - `Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`, - getAssetIDByName("SettingsIcon"), - ); - onHiddenUnlock?.(); - return; - } - - if (clickTimeout) { - clearTimeout(clickTimeout); - } - - const newTimeout = setTimeout(() => { - setClickCounter(0); - }, 1000); - setClickTimeout(newTimeout); - - const newCounter = clickCounter + 1; - setClickCounter(newCounter); - - if (newCounter < 10) { - return; - } - - showToast( - "🔓 Hidden settings unlocked!", - getAssetIDByName("CheckmarkIcon"), + const [clickCounter, setClickCounter] = React.useState(0); + const [clickTimeout, setClickTimeout] = React.useState( + null, ); - storage.hiddenSettings.enabled = true; - storage.hiddenSettings.visible = true; - setClickCounter(0); - onHiddenUnlock?.(); - }; + const styles = stylesheet.createThemedStyleSheet({ + container: { + flexDirection: "column", + alignItems: "center", + paddingVertical: 24, + paddingHorizontal: 16, + }, + title: { + fontSize: 28, + fontWeight: "700", + color: semanticColors.TEXT_NORMAL, + marginBottom: 4, + }, + subtitle: { + fontSize: 16, + fontWeight: "500", + color: semanticColors.TEXT_MUTED, + }, + iconContainer: { + backgroundColor: semanticColors.BACKGROUND_SECONDARY, + borderRadius: 16, + padding: 12, + marginBottom: 16, + }, + icon: { + width: 32, + height: 32, + tintColor: semanticColors.TEXT_NORMAL, + }, + }); - return ( - - - - - Commands - A collection of useful commands - - ); + const handleIconPress = () => { + if (storage.hiddenSettings.enabled) { + storage.hiddenSettings.visible = !storage.hiddenSettings.visible; + showToast( + `Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`, + getAssetIDByName("SettingsIcon"), + ); + onHiddenUnlock?.(); + return; + } + + if (clickTimeout) { + clearTimeout(clickTimeout); + } + + const newTimeout = setTimeout(() => { + setClickCounter(0); + }, 1000); + setClickTimeout(newTimeout); + + const newCounter = clickCounter + 1; + setClickCounter(newCounter); + + if (newCounter < 10) { + return; + } + + showToast( + "🔓 Hidden settings unlocked!", + getAssetIDByName("CheckmarkIcon"), + ); + storage.hiddenSettings.enabled = true; + storage.hiddenSettings.visible = true; + setClickCounter(0); + + onHiddenUnlock?.(); + }; + + return ( + + + + + Commands + A collection of useful commands + + ); } // NekosLife Categories Reference Page - SFW ONLY function NekosLifeCategoriesPage() { - const styles = stylesheet.createThemedStyleSheet({ - container: { - flex: 1, - backgroundColor: semanticColors.BACKGROUND_PRIMARY, - }, - tableHeader: { - flexDirection: "row", - alignItems: "center", - paddingVertical: 12, - paddingHorizontal: 16, - backgroundColor: semanticColors.BACKGROUND_MODIFIER_ACCENT, - borderBottomWidth: 2, - borderBottomColor: semanticColors.BORDER_STRONG, - }, - headerText: { - fontSize: 14, - fontWeight: "700", - color: semanticColors.TEXT_NORMAL, - textTransform: "uppercase", - }, - nameHeader: { - flex: 2, - }, - codeHeader: { - flex: 2, - textAlign: "center", - }, - badgeHeader: { - flex: 1, - textAlign: "center", - }, - categoryItem: { - flexDirection: "row", - alignItems: "center", - paddingVertical: 12, - paddingHorizontal: 16, - borderBottomWidth: 1, - borderBottomColor: semanticColors.BORDER_FAINT, - }, - categoryName: { - fontSize: 16, - fontWeight: "500", - color: semanticColors.TEXT_NORMAL, - flex: 2, - }, - categoryValue: { - fontSize: 14, - color: semanticColors.TEXT_MUTED, - fontFamily: "monospace", - flex: 2, - textAlign: "center", - }, - sfwBadgeContainer: { - flex: 1, - alignItems: "center", - }, - sfwBadge: { - backgroundColor: semanticColors.STATUS_POSITIVE, - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 12, - }, - badgeText: { - fontSize: 12, - fontWeight: "600", - color: semanticColors.WHITE, - }, - infoText: { - fontSize: 14, - color: semanticColors.TEXT_MUTED, - textAlign: "center", - lineHeight: 20, - }, - }); + const styles = stylesheet.createThemedStyleSheet({ + container: { + flex: 1, + backgroundColor: semanticColors.BACKGROUND_PRIMARY, + }, + tableHeader: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: semanticColors.BACKGROUND_MODIFIER_ACCENT, + borderBottomWidth: 2, + borderBottomColor: semanticColors.BORDER_STRONG, + }, + headerText: { + fontSize: 14, + fontWeight: "700", + color: semanticColors.TEXT_NORMAL, + textTransform: "uppercase", + }, + nameHeader: { + flex: 2, + }, + codeHeader: { + flex: 2, + textAlign: "center", + }, + badgeHeader: { + flex: 1, + textAlign: "center", + }, + categoryItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + paddingHorizontal: 16, + borderBottomWidth: 1, + borderBottomColor: semanticColors.BORDER_FAINT, + }, + categoryName: { + fontSize: 16, + fontWeight: "500", + color: semanticColors.TEXT_NORMAL, + flex: 2, + }, + categoryValue: { + fontSize: 14, + color: semanticColors.TEXT_MUTED, + fontFamily: "monospace", + flex: 2, + textAlign: "center", + }, + sfwBadgeContainer: { + flex: 1, + alignItems: "center", + }, + sfwBadge: { + backgroundColor: semanticColors.STATUS_POSITIVE, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + badgeText: { + fontSize: 12, + fontWeight: "600", + color: semanticColors.WHITE, + }, + infoText: { + fontSize: 14, + color: semanticColors.TEXT_MUTED, + textAlign: "center", + lineHeight: 20, + }, + }); - // Only SFW nekos.life categories - const sfwCategories = [ - { name: "Avatar", value: "avatar" }, - { name: "Classic", value: "classic" }, - { name: "Cuddle", value: "cuddle" }, - { name: "Fox Girl", value: "fox_girl" }, - { name: "Gecg", value: "gecg" }, - { name: "Holo", value: "holo" }, - { name: "Kemonomimi", value: "kemonomimi" }, - { name: "Kiss", value: "kiss" }, - { name: "Neko", value: "neko" }, - { name: "Neko GIF", value: "ngif" }, - { name: "Smug", value: "smug" }, - { name: "Spank", value: "spank" }, - { name: "Tickle", value: "tickle" }, - { name: "Waifu", value: "waifu" }, - { name: "Wallpaper", value: "wallpaper" }, - { name: "Woof", value: "woof" }, - ]; + // Only SFW nekos.life categories + const sfwCategories = [ + { name: "Avatar", value: "avatar" }, + { name: "Classic", value: "classic" }, + { name: "Cuddle", value: "cuddle" }, + { name: "Fox Girl", value: "fox_girl" }, + { name: "Gecg", value: "gecg" }, + { name: "Holo", value: "holo" }, + { name: "Kemonomimi", value: "kemonomimi" }, + { name: "Kiss", value: "kiss" }, + { name: "Neko", value: "neko" }, + { name: "Neko GIF", value: "ngif" }, + { name: "Smug", value: "smug" }, + { name: "Spank", value: "spank" }, + { name: "Tickle", value: "tickle" }, + { name: "Waifu", value: "waifu" }, + { name: "Wallpaper", value: "wallpaper" }, + { name: "Woof", value: "woof" }, + ]; - // Sort categories alphabetically - const sortedCategories = sfwCategories.sort((a, b) => - a.name.localeCompare(b.name), - ); + // Sort categories alphabetically + const sortedCategories = sfwCategories.sort((a, b) => + a.name.localeCompare(b.name), + ); - return ( - - - - + return ( + + + + Total SFW Categories: {sfwCategories.length} - {"\n\n"} + {"\n\n"} Use these category names with the /nekoslife command.{"\n"} All categories are Safe For Work (SFW). - - + + - - {/* Table Header */} - - + + {/* Table Header */} + + Category Name - - + + Command Code - - + + Type - - + + - {/* Table Rows */} - {sortedCategories.map((category, index) => ( - - {category.name} - {category.value} - - - SFW - - + {/* Table Rows */} + {sortedCategories.map((category, index) => ( + + {category.name} + {category.value} + + + SFW + + + + ))} + - ))} - - - - ); + + ); } // Hidden Commands Settings Page function HiddenSettingsPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const navigation = NavigationNative.useNavigation(); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + const navigation = NavigationNative.useNavigation(); - const styles = stylesheet.createThemedStyleSheet({ - container: { - flex: 1, - backgroundColor: semanticColors.BACKGROUND_PRIMARY, - }, - warningText: { - fontSize: 14, - color: semanticColors.TEXT_DANGER, - textAlign: "center", - lineHeight: 20, - fontWeight: "600", - }, - infoText: { - fontSize: 14, - color: semanticColors.TEXT_MUTED, - textAlign: "center", - lineHeight: 20, - marginTop: 8, - }, - }); + const styles = stylesheet.createThemedStyleSheet({ + container: { + flex: 1, + backgroundColor: semanticColors.BACKGROUND_PRIMARY, + }, + warningText: { + fontSize: 14, + color: semanticColors.TEXT_DANGER, + textAlign: "center", + lineHeight: 20, + fontWeight: "600", + }, + infoText: { + fontSize: 14, + color: semanticColors.TEXT_MUTED, + textAlign: "center", + lineHeight: 20, + marginTop: 8, + }, + }); - return ( - - - - + return ( + + + + These are hidden commands that may contain mature content or experimental features. - - + + Use at your own discretion. Commands in this section are disabled by default. - - + + - - } - value={storage.enabledCommands.lovefemboys} - onValueChange={(v) => { - storage.enabledCommands.lovefemboys = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - + + } + value={storage.enabledCommands.lovefemboys} + onValueChange={(v) => { + storage.enabledCommands.lovefemboys = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + - - } - value={storage.hiddenSettings.konochanBypassNsfw} - onValueChange={(v) => { - if (v) { - alerts.showConfirmationAlert({ - title: "⚠️ NSFW Bypass Warning", - content: + + } + value={storage.hiddenSettings.konochanBypassNsfw} + onValueChange={(v) => { + if (v) { + alerts.showConfirmationAlert({ + title: "⚠️ NSFW Bypass Warning", + content: "Enabling this allows NSFW content from KonoChan to be sent in any channel, including non-NSFW channels. This could violate server rules or Discord ToS. Use responsibly!", - confirmText: "I Understand", - cancelText: "Cancel", - onConfirm: () => { - storage.hiddenSettings.konochanBypassNsfw = true; - forceUpdate(); - }, - onCancel: () => { - // Do nothing - keep it disabled - }, - }); - } else { - storage.hiddenSettings.konochanBypassNsfw = false; - forceUpdate(); - } - }} - /> - + confirmText: "I Understand", + cancelText: "Cancel", + onConfirm: () => { + storage.hiddenSettings.konochanBypassNsfw = true; + forceUpdate(); + }, + onCancel: () => { + // Do nothing - keep it disabled + }, + }); + } else { + storage.hiddenSettings.konochanBypassNsfw = false; + forceUpdate(); + } + }} + /> + - - } - value={storage.hiddenSettings.visible} - onValueChange={(v) => { - storage.hiddenSettings.visible = v; - }} - /> - } - onPress={() => { - alerts.showConfirmationAlert({ - title: "Reset Hidden Settings", - content: + + } + value={storage.hiddenSettings.visible} + onValueChange={(v) => { + storage.hiddenSettings.visible = v; + }} + /> + } + onPress={() => { + alerts.showConfirmationAlert({ + title: "Reset Hidden Settings", + content: "This will hide the hidden settings section and disable all hidden commands. Are you sure?", - confirmText: "Reset", - onConfirm: () => { - storage.hiddenSettings.enabled = false; - storage.hiddenSettings.visible = false; - storage.hiddenSettings.konochanBypassNsfw = false; - storage.enabledCommands.lovefemboys = false; + confirmText: "Reset", + onConfirm: () => { + storage.hiddenSettings.enabled = false; + storage.hiddenSettings.visible = false; + storage.hiddenSettings.konochanBypassNsfw = false; + storage.enabledCommands.lovefemboys = false; - storage.pendingRestart = true; - showToast( - "Hidden settings reset", - getAssetIDByName("CheckmarkIcon"), - ); - }, - cancelText: "Cancel", - }); - }} - /> - - - - ); + storage.pendingRestart = true; + showToast( + "Hidden settings reset", + getAssetIDByName("CheckmarkIcon"), + ); + }, + cancelText: "Cancel", + }); + }} + /> + + + + ); } // Facts Commands Settings Page function FactsSettingsPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - return ( - - - - - } - value={storage.factSettings.sendAsReply} - onValueChange={(v) => { - storage.factSettings.sendAsReply = v; - }} - /> - } - value={storage.factSettings.includeCitation} - onValueChange={(v) => { - storage.factSettings.includeCitation = v; - }} - /> - + + + + } + value={storage.factSettings.sendAsReply} + onValueChange={(v) => { + storage.factSettings.sendAsReply = v; + }} + /> + } + value={storage.factSettings.includeCitation} + onValueChange={(v) => { + storage.factSettings.includeCitation = v; + }} + /> + - - - } - value={storage.enabledCommands.catfact} - onValueChange={(v) => { - storage.enabledCommands.catfact = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - } - value={storage.enabledCommands.dogfact} - onValueChange={(v) => { - storage.enabledCommands.dogfact = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - } - value={storage.enabledCommands.useless} - onValueChange={(v) => { - storage.enabledCommands.useless = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - - - ); + + + } + value={storage.enabledCommands.catfact} + onValueChange={(v) => { + storage.enabledCommands.catfact = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + } + value={storage.enabledCommands.dogfact} + onValueChange={(v) => { + storage.enabledCommands.dogfact = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + } + value={storage.enabledCommands.useless} + onValueChange={(v) => { + storage.enabledCommands.useless = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + + + ); } // Gary API Settings Page function GaryAPIPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const styles = stylesheet.createThemedStyleSheet({ - container: { - flex: 1, - backgroundColor: semanticColors.BACKGROUND_PRIMARY, - }, - infoText: { - fontSize: 14, - color: semanticColors.TEXT_MUTED, - marginBottom: 16, - textAlign: "center", - }, - currentText: { - fontSize: 14, - color: semanticColors.TEXT_NORMAL, - textAlign: "center", - fontWeight: "600", - }, - }); + const styles = stylesheet.createThemedStyleSheet({ + container: { + flex: 1, + backgroundColor: semanticColors.BACKGROUND_PRIMARY, + }, + infoText: { + fontSize: 14, + color: semanticColors.TEXT_MUTED, + marginBottom: 16, + textAlign: "center", + }, + currentText: { + fontSize: 14, + color: semanticColors.TEXT_NORMAL, + textAlign: "center", + fontWeight: "600", + }, + }); - const currentSource = storage.garySettings.imageSource; + const currentSource = storage.garySettings.imageSource; - return ( - - - - - } - value={storage.enabledCommands.gary} - onValueChange={(v) => { - storage.enabledCommands.gary = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - + return ( + + + + + } + value={storage.enabledCommands.gary} + onValueChange={(v) => { + storage.enabledCommands.gary = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + - - + + Choose which API the /gary command should use to fetch images. - - + + - - } - value={currentSource === "gary"} - onValueChange={(v) => { - if (v) { - storage.garySettings.imageSource = "gary"; - forceUpdate(); - } - }} - /> - } - value={currentSource === "catapi"} - onValueChange={(v) => { - if (v) { - storage.garySettings.imageSource = "catapi"; - forceUpdate(); - } - }} - /> - } - value={currentSource === "minker"} - onValueChange={(v) => { - if (v) { - storage.garySettings.imageSource = "minker"; - forceUpdate(); - } - }} - /> - } - value={currentSource === "goober"} - onValueChange={(v) => { - if (v) { - storage.garySettings.imageSource = "goober"; - forceUpdate(); - } - }} - /> - + + } + value={currentSource === "gary"} + onValueChange={(v) => { + if (v) { + storage.garySettings.imageSource = "gary"; + forceUpdate(); + } + }} + /> + } + value={currentSource === "catapi"} + onValueChange={(v) => { + if (v) { + storage.garySettings.imageSource = "catapi"; + forceUpdate(); + } + }} + /> + } + value={currentSource === "minker"} + onValueChange={(v) => { + if (v) { + storage.garySettings.imageSource = "minker"; + forceUpdate(); + } + }} + /> + } + value={currentSource === "goober"} + onValueChange={(v) => { + if (v) { + storage.garySettings.imageSource = "goober"; + forceUpdate(); + } + }} + /> + - - + + Currently using:{" "} - {currentSource === "gary" - ? "Gary API" - : currentSource === "catapi" - ? "Cat API" - : currentSource === "minker" - ? "Minker API" - : currentSource === "goober" - ? "Goober API" - : "Gary API"} - - - - - ); + {currentSource === "gary" + ? "Gary API" + : currentSource === "catapi" + ? "Cat API" + : currentSource === "minker" + ? "Minker API" + : currentSource === "goober" + ? "Goober API" + : "Gary API"} + + + + + ); } // List Settings Page function ListSettingsPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - return ( - - - - - } - value={storage.listSettings.pluginListAlwaysDetailed} - onValueChange={(v) => { - storage.listSettings.pluginListAlwaysDetailed = v; - }} - /> - - } - value={storage.listSettings.themeListAlwaysDetailed} - onValueChange={(v) => { - storage.listSettings.themeListAlwaysDetailed = v; - }} - /> - + + + + } + value={storage.listSettings.pluginListAlwaysDetailed} + onValueChange={(v) => { + storage.listSettings.pluginListAlwaysDetailed = v; + }} + /> + + } + value={storage.listSettings.themeListAlwaysDetailed} + onValueChange={(v) => { + storage.listSettings.themeListAlwaysDetailed = v; + }} + /> + - - - } - value={storage.enabledCommands.pluginList} - onValueChange={(v) => { - storage.enabledCommands.pluginList = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - } - value={storage.enabledCommands.themeList} - onValueChange={(v) => { - storage.enabledCommands.themeList = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - - - ); + + + } + value={storage.enabledCommands.pluginList} + onValueChange={(v) => { + storage.enabledCommands.pluginList = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + } + value={storage.enabledCommands.themeList} + onValueChange={(v) => { + storage.enabledCommands.themeList = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + + + ); } // Image Settings Page function ImageSettingsPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - return ( - - - - - } - value={storage.enabledCommands.petpet} - onValueChange={(v) => { - storage.enabledCommands.petpet = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - + + + + } + value={storage.enabledCommands.petpet} + onValueChange={(v) => { + storage.enabledCommands.petpet = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + - - } - value={storage.enabledCommands.konoself} - onValueChange={(v) => { - storage.enabledCommands.konoself = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - } - value={storage.enabledCommands.konosend} - onValueChange={(v) => { - storage.enabledCommands.konosend = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - - - ); + + } + value={storage.enabledCommands.konoself} + onValueChange={(v) => { + storage.enabledCommands.konoself = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + } + value={storage.enabledCommands.konosend} + onValueChange={(v) => { + storage.enabledCommands.konosend = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + + + ); } // Spotify Settings Page function SpotifySettingsPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const styles = stylesheet.createThemedStyleSheet({ - infoText: { - fontSize: 14, - color: semanticColors.TEXT_MUTED, - textAlign: "center", - lineHeight: 20, - }, - }); + const styles = stylesheet.createThemedStyleSheet({ + infoText: { + fontSize: 14, + color: semanticColors.TEXT_MUTED, + textAlign: "center", + lineHeight: 20, + }, + }); - return ( - - - - - } - value={storage.enabledCommands.spotifyTrack} - onValueChange={(v) => { - storage.enabledCommands.spotifyTrack = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - } - value={storage.enabledCommands.spotifyAlbum} - onValueChange={(v) => { - storage.enabledCommands.spotifyAlbum = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - } - value={storage.enabledCommands.spotifyArtists} - onValueChange={(v) => { - storage.enabledCommands.spotifyArtists = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - } - value={storage.enabledCommands.spotifyCover} - onValueChange={(v) => { - storage.enabledCommands.spotifyCover = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - + + + + } + value={storage.enabledCommands.spotifyTrack} + onValueChange={(v) => { + storage.enabledCommands.spotifyTrack = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + } + value={storage.enabledCommands.spotifyAlbum} + onValueChange={(v) => { + storage.enabledCommands.spotifyAlbum = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + } + value={storage.enabledCommands.spotifyArtists} + onValueChange={(v) => { + storage.enabledCommands.spotifyArtists = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + } + value={storage.enabledCommands.spotifyCover} + onValueChange={(v) => { + storage.enabledCommands.spotifyCover = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + - - + + These commands allow you to share your current Spotify activity in Discord. Make sure you have Spotify connected to Discord for these commands to work properly. - - - - - ); + + + + + ); } // Other Settings Page - Updated with IP command function OtherSettingsPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - return ( - - - - } - value={storage.enabledCommands.firstmessage} - onValueChange={(v) => { - storage.enabledCommands.firstmessage = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - + + + } + value={storage.enabledCommands.firstmessage} + onValueChange={(v) => { + storage.enabledCommands.firstmessage = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + - - } - value={storage.enabledCommands.sysinfo} - onValueChange={(v) => { - storage.enabledCommands.sysinfo = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - - - - ); + + } + value={storage.enabledCommands.sysinfo} + onValueChange={(v) => { + storage.enabledCommands.sysinfo = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + + + + ); } // Credits Page function AliucordPage() { - useProxy(storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const navigation = NavigationNative.useNavigation(); + useProxy(storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + const navigation = NavigationNative.useNavigation(); - return ( - - - } - value={storage.enabledCommands.ip} - onValueChange={(v) => { - storage.enabledCommands.ip = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - + return ( + + + } + value={storage.enabledCommands.ip} + onValueChange={(v) => { + storage.enabledCommands.ip = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + - - } - value={storage.enabledCommands.nekoslife} - onValueChange={(v) => { - storage.enabledCommands.nekoslife = v; - storage.pendingRestart = true; - forceUpdate(); - }} - /> - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "NekosLife Categories", - render: NekosLifeCategoriesPage, - }) - } - /> - + + } + value={storage.enabledCommands.nekoslife} + onValueChange={(v) => { + storage.enabledCommands.nekoslife = v; + storage.pendingRestart = true; + forceUpdate(); + }} + /> + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "NekosLife Categories", + render: NekosLifeCategoriesPage, + }) + } + /> + - - - ); + + + ); } function CreditsPage() { - const styles = stylesheet.createThemedStyleSheet({ - container: { - flex: 1, - backgroundColor: semanticColors.BACKGROUND_PRIMARY, - }, - creditItem: { - flexDirection: "row", - alignItems: "center", - paddingVertical: 16, - paddingHorizontal: 20, - backgroundColor: semanticColors.CARD_PRIMARY_BG, - borderRadius: 12, - marginHorizontal: 16, - marginVertical: 6, - shadowColor: semanticColors.BLACK, - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.1, - shadowRadius: 2, - elevation: 2, - }, - avatar: { - width: 48, - height: 48, - borderRadius: 24, - marginRight: 16, - backgroundColor: semanticColors.BACKGROUND_SECONDARY, - }, - textContainer: { - flex: 1, - }, - commandText: { - fontSize: 17, - fontWeight: "600", - color: semanticColors.TEXT_NORMAL, - marginBottom: 4, - }, - authorText: { - fontSize: 15, - color: semanticColors.TEXT_MUTED, - marginBottom: 2, - }, - linkText: { - fontSize: 13, - color: semanticColors.TEXT_BRAND, - fontWeight: "500", - }, - infoText: { - fontSize: 14, - color: semanticColors.TEXT_MUTED, - marginBottom: 20, - textAlign: "center", - lineHeight: 20, - }, - versionText: { - fontSize: 15, - color: semanticColors.TEXT_NORMAL, - textAlign: "center", - fontWeight: "600", - lineHeight: 22, - }, - }); + const styles = stylesheet.createThemedStyleSheet({ + container: { + flex: 1, + backgroundColor: semanticColors.BACKGROUND_PRIMARY, + }, + creditItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 16, + paddingHorizontal: 20, + backgroundColor: semanticColors.CARD_PRIMARY_BG, + borderRadius: 12, + marginHorizontal: 16, + marginVertical: 6, + shadowColor: semanticColors.BLACK, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + avatar: { + width: 48, + height: 48, + borderRadius: 24, + marginRight: 16, + backgroundColor: semanticColors.BACKGROUND_SECONDARY, + }, + textContainer: { + flex: 1, + }, + commandText: { + fontSize: 17, + fontWeight: "600", + color: semanticColors.TEXT_NORMAL, + marginBottom: 4, + }, + authorText: { + fontSize: 15, + color: semanticColors.TEXT_MUTED, + marginBottom: 2, + }, + linkText: { + fontSize: 13, + color: semanticColors.TEXT_BRAND, + fontWeight: "500", + }, + infoText: { + fontSize: 14, + color: semanticColors.TEXT_MUTED, + marginBottom: 20, + textAlign: "center", + lineHeight: 20, + }, + versionText: { + fontSize: 15, + color: semanticColors.TEXT_NORMAL, + textAlign: "center", + fontWeight: "600", + lineHeight: 22, + }, + }); - const credits = [ - { - command: "Facts Commands", - author: "jdev082", - avatar: "https://github.com/jdev082.png", - github: "https://github.com/jdev082", - }, - { - command: "List Commands", - author: "Kitomanari", - avatar: "https://github.com/Kitosight.png", - github: "https://github.com/Kitosight", - }, - { - command: "PetPet", - author: "wolfieeee", - avatar: "https://github.com/WolfPlugs.png", - github: "https://github.com/WolfPlugs", - }, - { - command: "KonoChan Commands", - author: "btmc727 & Rico040", - avatar: "https://github.com/OTKUSteyler.png", - github: "https://github.com/OTKUSteyler", - }, - { - command: "FirstMessage Command", - author: "sapphire", - avatar: "https://github.com/aeongdesu.png", - github: "https://github.com/aeongdesu", - }, - { - command: "Sysinfo Command", - author: "mugman", - avatar: "https://github.com/mugman174.png", - github: "https://github.com/mugman174", - }, - { - command: "Spotify Commands", - author: "Kitomanari", - avatar: "https://github.com/Kitosight.png", - github: "https://github.com/Kitosight", - }, - { - command: "Gary Command", - author: "Zach Orange", - avatar: "https://github.com/IAmGaryTheCat.png", - github: "https://github.com/IAmGaryTheCat", - }, - { - command: "IP & NekosLife Commands", - author: "scruzism", - avatar: "https://github.com/scrazzz.png", - github: "https://github.com/scrazzz", - }, - ]; + const credits = [ + { + command: "Facts Commands", + author: "jdev082", + avatar: "https://github.com/jdev082.png", + github: "https://github.com/jdev082", + }, + { + command: "List Commands", + author: "Kitomanari", + avatar: "https://github.com/Kitosight.png", + github: "https://github.com/Kitosight", + }, + { + command: "PetPet", + author: "wolfieeee", + avatar: "https://github.com/WolfPlugs.png", + github: "https://github.com/WolfPlugs", + }, + { + command: "KonoChan Commands", + author: "btmc727 & Rico040", + avatar: "https://github.com/OTKUSteyler.png", + github: "https://github.com/OTKUSteyler", + }, + { + command: "FirstMessage Command", + author: "sapphire", + avatar: "https://github.com/aeongdesu.png", + github: "https://github.com/aeongdesu", + }, + { + command: "Sysinfo Command", + author: "mugman", + avatar: "https://github.com/mugman174.png", + github: "https://github.com/mugman174", + }, + { + command: "Spotify Commands", + author: "Kitomanari", + avatar: "https://github.com/Kitosight.png", + github: "https://github.com/Kitosight", + }, + { + command: "Gary Command", + author: "Zach Orange", + avatar: "https://github.com/IAmGaryTheCat.png", + github: "https://github.com/IAmGaryTheCat", + }, + { + command: "IP & NekosLife Commands", + author: "scruzism", + avatar: "https://github.com/scrazzz.png", + github: "https://github.com/scrazzz", + }, + ]; - const handleProfilePress = (githubUrl: string) => { - RN.Linking.openURL(githubUrl); - }; + const handleProfilePress = (githubUrl: string) => { + RN.Linking.openURL(githubUrl); + }; - return ( - - - - + return ( + + + + Thanks to this developers for creating such a nice plugins!{"\n"} Tap on any developer to visit their GitHub profile. - - + + - {credits.map((credit, index) => ( - handleProfilePress(credit.github)} - android_ripple={{ - color: semanticColors.ANDROID_RIPPLE, - radius: 200, - }} - > - - - {credit.command} - by {credit.author} - - {credit.github.replace("https://github.com/", "@")} - - - - ))} + {credits.map((credit, index) => ( + handleProfilePress(credit.github)} + android_ripple={{ + color: semanticColors.ANDROID_RIPPLE, + radius: 200, + }} + > + + + {credit.command} + by {credit.author} + + {credit.github.replace("https://github.com/", "@")} + + + + ))} - - + + Commands Plugin Collection{"\n"} Version 1.2.0 - - - - - ); + + + + + ); } // MAIN SETTINGS COMPONENT export default function Settings() { - useProxy(storage); - const navigation = NavigationNative.useNavigation(); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(storage); + const navigation = NavigationNative.useNavigation(); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - React.useEffect(() => { - return () => { - if (storage.pendingRestart) { - storage.pendingRestart = false; - alerts.showConfirmationAlert({ - title: "Restart Required", - content: + React.useEffect(() => { + return () => { + if (storage.pendingRestart) { + storage.pendingRestart = false; + alerts.showConfirmationAlert({ + title: "Restart Required", + content: "You have made changes to commands. Please restart Discord to apply these changes.", - confirmText: "Okay", - cancelText: null, - }); - } - }; - }, []); - - return ( - -
- - - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Facts Commands", - render: FactsSettingsPage, - }) - } - /> - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "List Commands", - render: ListSettingsPage, - }) - } - /> - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Image Commands", - render: ImageSettingsPage, - }) - } - /> - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Gary Commands", - render: GaryAPIPage, - }) - } - /> - - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Spotify Commands", - render: SpotifySettingsPage, - }) - } - /> - - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Aliucord", - render: AliucordPage, - }) - } - /> - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Other Commands", - render: OtherSettingsPage, - }) - } - /> - - - {storage.hiddenSettings?.visible && ( - - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Hidden Settings", - render: HiddenSettingsPage, - }) + confirmText: "Okay", + cancelText: null, + }); } - /> - - )} + }; + }, []); - - } - trailing={} - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Credits", - render: CreditsPage, - }) - } - /> - + return ( + +
- - - ); + + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Facts Commands", + render: FactsSettingsPage, + }) + } + /> + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "List Commands", + render: ListSettingsPage, + }) + } + /> + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Image Commands", + render: ImageSettingsPage, + }) + } + /> + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Gary Commands", + render: GaryAPIPage, + }) + } + /> + + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Spotify Commands", + render: SpotifySettingsPage, + }) + } + /> + + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Aliucord", + render: AliucordPage, + }) + } + /> + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Other Commands", + render: OtherSettingsPage, + }) + } + /> + + + {storage.hiddenSettings?.visible && ( + + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Hidden Settings", + render: HiddenSettingsPage, + }) + } + /> + + )} + + + } + trailing={} + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Credits", + render: CreditsPage, + }) + } + /> + + + + + ); } diff --git a/plugins/Commands/src/commands/lovefemboys.ts b/plugins/Commands/src/commands/lovefemboys.ts index 72b7344..7a23050 100644 --- a/plugins/Commands/src/commands/lovefemboys.ts +++ b/plugins/Commands/src/commands/lovefemboys.ts @@ -8,166 +8,166 @@ const BotMessage = findByProps("createBotMessage"); const Avatars = findByProps("BOT_AVATARS"); function sendReply(channelID, content, embed) { - const channel = channelID ?? Channels?.getChannelId?.(); - const msg = BotMessage.createBotMessage({ - channelId: channel, - content: "", - embeds: embed, - }); - msg.author.username = "Astolfo"; - msg.author.avatar = "Astolfo"; - Avatars.BOT_AVATARS.Astolfo = + const channel = channelID ?? Channels?.getChannelId?.(); + const msg = BotMessage.createBotMessage({ + channelId: channel, + content: "", + embeds: embed, + }); + msg.author.username = "Astolfo"; + msg.author.avatar = "Astolfo"; + Avatars.BOT_AVATARS.Astolfo = "https://i.pinimg.com/736x/50/77/1f/50771f45b1c015cfbb8b0853ba7b8521.jpg"; - if (typeof content === "string") { - msg.content = content; - } else { - Object.assign(msg, content); - } - MessageActions.receiveMessage(channel, msg); + if (typeof content === "string") { + msg.content = content; + } else { + Object.assign(msg, content); + } + MessageActions.receiveMessage(channel, msg); } export const lovefemboysCommand = { - name: "lovefemboys", - displayName: "lovefemboys", - description: "Get an image of a femboy", - displayDescription: "Get an image of a femboy", - options: [ - { - name: "nsfw", - displayName: "nsfw", - description: "Get the result from r/femboys instead of r/femboy (NSFW)", - displayDescription: + name: "lovefemboys", + displayName: "lovefemboys", + description: "Get an image of a femboy", + displayDescription: "Get an image of a femboy", + options: [ + { + name: "nsfw", + displayName: "nsfw", + description: "Get the result from r/femboys instead of r/femboy (NSFW)", + displayDescription: "Get the result from r/femboys instead of r/femboy (NSFW)", - required: false, - type: 5, - }, - { - name: "sort", - displayName: "sort", - description: "Changes the way reddit sorts.", - displayDescription: "Changes the way reddit sorts", - required: false, - type: 3, - }, - { - name: "silent", - displayName: "silent", - description: "Makes it so only you can see the message.", - displayDescription: "Makes it so only you can see the message.", - required: false, - type: 5, - }, - ], - applicationId: "-1", - inputType: 1, - type: 1, - execute: async (args, ctx) => { - try { - // Some code taken from enmity gotfemboys plugin by spinfal & was modified with the help of meqativ - let nsfw = args.find((arg) => arg.name === "nsfw")?.value; - let sort = args.find((arg) => arg.name === "sort")?.value; - let silent = args.find((arg) => arg.name === "silent")?.value; + required: false, + type: 5, + }, + { + name: "sort", + displayName: "sort", + description: "Changes the way reddit sorts.", + displayDescription: "Changes the way reddit sorts", + required: false, + type: 3, + }, + { + name: "silent", + displayName: "silent", + description: "Makes it so only you can see the message.", + displayDescription: "Makes it so only you can see the message.", + required: false, + type: 5, + }, + ], + applicationId: "-1", + inputType: 1, + type: 1, + execute: async (args, ctx) => { + try { + // Some code taken from enmity gotfemboys plugin by spinfal & was modified with the help of meqativ + const nsfw = args.find((arg) => arg.name === "nsfw")?.value; + let sort = args.find((arg) => arg.name === "sort")?.value; + const silent = args.find((arg) => arg.name === "silent")?.value; - if (typeof sort === "undefined") sort = storage.sortdefs; - if ( - !["best", "hot", "new", "rising", "top", "controversial"].includes(sort) - ) { - sendReply( - ctx.channel.id, - "Incorrect sorting type. Valid options are\n`best`, `hot`, `new`, `rising`, `top`, `controversial`.", - [], - ); - return; - } + if (typeof sort === "undefined") sort = storage.sortdefs; + if ( + !["best", "hot", "new", "rising", "top", "controversial"].includes(sort) + ) { + sendReply( + ctx.channel.id, + "Incorrect sorting type. Valid options are\n`best`, `hot`, `new`, `rising`, `top`, `controversial`.", + [], + ); + return; + } - // Show NSFW warning popup only when nsfw option is selected - if (nsfw) { - const shouldProceed = await new Promise((resolve) => { - alerts.showConfirmationAlert({ - title: "⚠️ NSFW Content Warning", - content: + // Show NSFW warning popup only when nsfw option is selected + if (nsfw) { + const shouldProceed = await new Promise((resolve) => { + alerts.showConfirmationAlert({ + title: "⚠️ NSFW Content Warning", + content: "This command will send NSFW (Not Safe For Work) content from r/femboys. Are you sure you want to continue?", - confirmText: "Yes, Continue", - onConfirm: () => resolve(true), - cancelText: "Cancel", - onCancel: () => resolve(false), - }); - }); + confirmText: "Yes, Continue", + onConfirm: () => resolve(true), + cancelText: "Cancel", + onCancel: () => resolve(false), + }); + }); - if (!shouldProceed) { - return { type: 4 }; - } - } + if (!shouldProceed) { + return { type: 4 }; + } + } - let response = await fetch( - `https://www.reddit.com/r/femboy/${sort}.json?limit=100`, - ).then((res) => res.json()); + let response = await fetch( + `https://www.reddit.com/r/femboy/${sort}.json?limit=100`, + ).then((res) => res.json()); - if (!ctx.channel.nsfw_ && nsfw && storage.nsfwwarn && !(silent ?? true)) { - sendReply( - ctx.channel.id, - "This channel is not marked as NSFW\n(You can disable this check in plugin settings)", - [], - ); - return; - } + if (!ctx.channel.nsfw_ && nsfw && storage.nsfwwarn && !(silent ?? true)) { + sendReply( + ctx.channel.id, + "This channel is not marked as NSFW\n(You can disable this check in plugin settings)", + [], + ); + return; + } - if (nsfw) { - response = await fetch( - `https://www.reddit.com/r/femboys/${sort}.json?limit=100`, - ).then((res) => res.json()); - } + if (nsfw) { + response = await fetch( + `https://www.reddit.com/r/femboys/${sort}.json?limit=100`, + ).then((res) => res.json()); + } - response = + response = response.data?.children?.[ - Math.floor(Math.random() * response.data?.children?.length) + Math.floor(Math.random() * response.data?.children?.length) ]?.data; - let author = await fetch( - `https://www.reddit.com/u/${response?.author}/about.json`, - ).then((res) => res.json()); + const author = await fetch( + `https://www.reddit.com/u/${response?.author}/about.json`, + ).then((res) => res.json()); - if (silent ?? true) { - sendReply(ctx.channel.id, "", [ - { - type: "rich", - title: response?.title, - url: `https://reddit.com${response?.permalink}`, - author: { - name: `u/${response?.author} • r/${response?.subreddit}`, - proxy_icon_url: author?.data.icon_img.split("?")[0], - icon_url: author?.data.icon_img.split("?")[0], - }, - image: { - proxy_url: + if (silent ?? true) { + sendReply(ctx.channel.id, "", [ + { + type: "rich", + title: response?.title, + url: `https://reddit.com${response?.permalink}`, + author: { + name: `u/${response?.author} • r/${response?.subreddit}`, + proxy_icon_url: author?.data.icon_img.split("?")[0], + icon_url: author?.data.icon_img.split("?")[0], + }, + image: { + proxy_url: response?.url_overridden_by_dest?.replace(/.gifv$/g, ".gif") ?? response?.url.replace(/.gifv$/g, ".gif"), - url: + url: response?.url_overridden_by_dest?.replace(/.gifv$/g, ".gif") ?? response?.url?.replace(/.gifv$/g, ".gif"), - width: response?.preview?.images?.[0]?.source?.width, - height: response?.preview?.images?.[0]?.source?.height, - }, - color: "0xf4b8e4", - }, - ]); - } else { - const fixNonce = Date.now().toString(); - MessageActions.sendMessage( - ctx.channel.id, - { - content: response?.url_overridden_by_dest ?? response?.url, - }, - void 0, - { nonce: fixNonce }, - ); - } - } catch (err) { - console.error("[LoveFemboys] Error:", err); - sendReply( - ctx.channel.id, - "ERROR !!!!!!!!!!!! 😭😭😭 Check debug logs!! 🥺🥺🥺", - [], - ); - } - }, + width: response?.preview?.images?.[0]?.source?.width, + height: response?.preview?.images?.[0]?.source?.height, + }, + color: "0xf4b8e4", + }, + ]); + } else { + const fixNonce = Date.now().toString(); + MessageActions.sendMessage( + ctx.channel.id, + { + content: response?.url_overridden_by_dest ?? response?.url, + }, + void 0, + { nonce: fixNonce }, + ); + } + } catch (err) { + console.error("[LoveFemboys] Error:", err); + sendReply( + ctx.channel.id, + "ERROR !!!!!!!!!!!! 😭😭😭 Check debug logs!! 🥺🥺🥺", + [], + ); + } + }, }; diff --git a/plugins/Commands/src/utils/api.ts b/plugins/Commands/src/utils/api.ts index 2c81fec..63f11cd 100644 --- a/plugins/Commands/src/utils/api.ts +++ b/plugins/Commands/src/utils/api.ts @@ -2,64 +2,71 @@ // Facts APIs export const uselessFact = async () => { - const response = await fetch("https://uselessfacts.jsph.pl/api/v2/facts/random"); - const resp = await response.json(); - return { - text: resp["text"], - source: resp["source"], - language: resp["language"], - }; + const response = await fetch( + "https://uselessfacts.jsph.pl/api/v2/facts/random", + ); + const resp = await response.json(); + return { + text: resp["text"], + source: resp["source"], + language: resp["language"], + }; }; export const dogFact = async () => { - const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1"); - const resp = await response.json(); - return { - text: resp["data"]["0"]["attributes"]["body"], - }; + const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1"); + const resp = await response.json(); + return { + text: resp["data"]["0"]["attributes"]["body"], + }; }; export const catFact = async () => { - const response = await fetch("https://catfact.ninja/fact"); - const resp = await response.json(); - return { - text: resp["fact"], - length: resp["length"], - }; + const response = await fetch("https://catfact.ninja/fact"); + const resp = await response.json(); + return { + text: resp["fact"], + length: resp["length"], + }; }; // Image APIs export const getPetPetData = async (image: string) => { - const data = await fetch( - `https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}` - ); - const body = await data.json(); - return body; + const data = await fetch( + `https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}`, + ); + const body = await data.json(); + return body; }; // Gary API function export const getGaryUrl = async (source = "gary"): Promise => { - try { - switch (source) { - case "gary": - const response = await fetch("https://api.garythe.cat/gary"); - const json = await response.json(); - return json.url; - case "goober": - const gooberResponse = await fetch("https://api.garythe.cat/goober"); - const gooberJson = await gooberResponse.json(); - return gooberJson.url; - case "catapi": - const catResponse = await fetch("https://api.thecatapi.com/v1/images/search"); - const catJson = await catResponse.json(); - return catJson[0].url; - case "minker": - return "https://minky.materii.dev/"; - default: - throw new Error("Invalid Gary image source value"); - } - } catch (error) { - console.error(`[Gary API] Error fetching image from ${source}:`, error); - return null; + try { + switch (source) { + case "gary": { + const response = await fetch("https://api.garythe.cat/gary"); + const json = await response.json(); + return json.url; + } + case "goober": { + const gooberResponse = await fetch("https://api.garythe.cat/goober"); + const gooberJson = await gooberResponse.json(); + return gooberJson.url; + } + case "catapi": { + const catResponse = await fetch( + "https://api.thecatapi.com/v1/images/search", + ); + const catJson = await catResponse.json(); + return catJson[0].url; + } + case "minker": + return "https://minky.materii.dev/"; + default: + throw new Error("Invalid Gary image source value"); } + } catch (error) { + console.error(`[Gary API] Error fetching image from ${source}:`, error); + return null; + } }; diff --git a/plugins/multi-scrobbler/src/constants.ts b/plugins/multi-scrobbler/src/constants.ts index 9fbb96b..b67f6af 100644 --- a/plugins/multi-scrobbler/src/constants.ts +++ b/plugins/multi-scrobbler/src/constants.ts @@ -1,79 +1,79 @@ import { ServiceConfig, ServiceType } from "../../defs"; const Constants = { - DEFAULT_APP_NAME: "Music", - DEFAULT_TIME_INTERVAL: 5, - // Discord application ID - APPLICATION_ID: "1054951789318909972", - // Don't check more than once every 3 seconds to avoid getting rate limited - MIN_UPDATE_INTERVAL: 3, - // How many times to retry failed API calls - MAX_RETRY_ATTEMPTS: 3, - // Wait 5 seconds between retries - RETRY_DELAY: 5000, + DEFAULT_APP_NAME: "Music", + DEFAULT_TIME_INTERVAL: 5, + // Discord application ID + APPLICATION_ID: "1054951789318909972", + // Don't check more than once every 3 seconds to avoid getting rate limited + MIN_UPDATE_INTERVAL: 3, + // How many times to retry failed API calls + MAX_RETRY_ATTEMPTS: 3, + // Wait 5 seconds between retries + RETRY_DELAY: 5000, - // Configuration for each supported service - SERVICES: { - lastfm: { - name: "Last.fm", - baseUrl: "http://ws.audioscrobbler.com/2.0", - requiresApiKey: true, - requiresToken: false, + // Configuration for each supported service + SERVICES: { + lastfm: { + name: "Last.fm", + baseUrl: "http://ws.audioscrobbler.com/2.0", + requiresApiKey: true, + requiresToken: false, + }, + librefm: { + name: "Libre.fm", + baseUrl: "http://libre.fm/api", + requiresApiKey: true, + requiresToken: false, + }, + listenbrainz: { + name: "ListenBrainz", + baseUrl: "http://api.listenbrainz.org/1", + requiresApiKey: false, + requiresToken: true, + }, + } as Record, + + // Last.fm/Libre.fm use these hashes for their generic album covers + DEFAULT_COVER_HASHES: [ + "2a96cbd8b46e442fc41c2b86b821562f", + "c6f59c1e5e7240a4c0d427abd71f3dbb", + ], + + // Plugin defaults + DEFAULT_SETTINGS: { + username: "", + apiKey: "", + appName: "Music", + timeInterval: 5, + showTimestamp: true, + listeningTo: true, + ignoreSpotify: true, + verboseLogging: false, + service: undefined as ServiceType | undefined, + librefmUsername: "", + librefmApiKey: "", + listenbrainzUsername: "", + listenbrainzToken: "", }, - librefm: { - name: "Libre.fm", - baseUrl: "http://libre.fm/api", - requiresApiKey: true, - requiresToken: false, - }, - listenbrainz: { - name: "ListenBrainz", - baseUrl: "http://api.listenbrainz.org/1", - requiresApiKey: false, - requiresToken: true, - }, - } as Record, - // Last.fm/Libre.fm use these hashes for their generic album covers - DEFAULT_COVER_HASHES: [ - "2a96cbd8b46e442fc41c2b86b821562f", - "c6f59c1e5e7240a4c0d427abd71f3dbb", - ], - - // Plugin defaults - DEFAULT_SETTINGS: { - username: "", - apiKey: "", - appName: "Music", - timeInterval: 5, - showTimestamp: true, - listeningTo: true, - ignoreSpotify: true, - verboseLogging: false, - service: undefined as ServiceType | undefined, - librefmUsername: "", - librefmApiKey: "", - listenbrainzUsername: "", - listenbrainzToken: "", - }, - - // Last.fm/Libre.fm API error codes - API_ERROR_CODES: { - 2: "Invalid service", - 3: "Invalid method", - 4: "Invalid format", - 5: "Invalid parameters", - 6: "Invalid resource specified", - 7: "Invalid session key", - 8: "Invalid API key", - 9: "Invalid session", - 10: "Invalid API signature", - 11: "Service offline", - 13: "Invalid method signature supplied", - 16: "Service temporarily unavailable", - 26: "Suspended API key", - 29: "Rate limit exceeded", - } as Record, + // Last.fm/Libre.fm API error codes + API_ERROR_CODES: { + 2: "Invalid service", + 3: "Invalid method", + 4: "Invalid format", + 5: "Invalid parameters", + 6: "Invalid resource specified", + 7: "Invalid session key", + 8: "Invalid API key", + 9: "Invalid session", + 10: "Invalid API signature", + 11: "Service offline", + 13: "Invalid method signature supplied", + 16: "Service temporarily unavailable", + 26: "Suspended API key", + 29: "Rate limit exceeded", + } as Record, } as const; export default Constants; diff --git a/plugins/multi-scrobbler/src/index.tsx b/plugins/multi-scrobbler/src/index.tsx index e024620..896887f 100644 --- a/plugins/multi-scrobbler/src/index.tsx +++ b/plugins/multi-scrobbler/src/index.tsx @@ -11,10 +11,10 @@ import Settings from "./ui/pages/Settings"; import patchSidebar from "./sidebar"; export const pluginState = { - pluginStopped: false, - lastActivity: undefined, - updateInterval: undefined, - lastTrackUrl: undefined, + pluginStopped: false, + lastActivity: undefined, + updateInterval: undefined, + lastTrackUrl: undefined, } as { pluginStopped: boolean; lastActivity?: any; @@ -28,20 +28,20 @@ let sidebarUnpatch: (() => void) | undefined; // Set up default plugin settings const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS; for (const key of Object.keys(defaultSettings)) { - plugin.storage[key] = + plugin.storage[key] = plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings]; } plugin.storage.addToSidebar ??= false; 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; + }, }); // Keep track of connection attempts @@ -51,182 +51,182 @@ const RECONNECT_DELAY = 5000; const log = (...args: any[]) => console.log("[Scrobble Plugin]", ...args); const logError = (...args: any[]) => - console.error("[Scrobble Plugin] Error:", ...args); + console.error("[Scrobble Plugin] Error:", ...args); async function tryInitialize() { - try { - await initialize(); - connectionAttempts = 0; - log("Plugin initialized successfully"); - } catch (error) { - logError("Initialization failed:", error); - connectionAttempts++; + try { + await initialize(); + connectionAttempts = 0; + log("Plugin initialized successfully"); + } catch (error) { + logError("Initialization failed:", error); + connectionAttempts++; - if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) { - log( - `Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`, - ); - setTimeout(tryInitialize, RECONNECT_DELAY); - } else { - logError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`); + if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) { + log( + `Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`, + ); + setTimeout(tryInitialize, RECONNECT_DELAY); + } else { + logError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`); + } } - } } async function validateAndInitialize() { - // Check if user has selected a service first - if (!currentSettings.service) { - log("No service selected. Please configure a service in plugin settings."); - return; - } + // Check if user has selected a service first + if (!currentSettings.service) { + log("No service selected. Please configure a service in plugin settings."); + return; + } - const serviceName = serviceFactory.getCurrentService().getServiceName(); + const serviceName = serviceFactory.getCurrentService().getServiceName(); - // Make sure we have credentials for whatever service is selected - const service = currentSettings.service; - let hasCredentials = false; + // Make sure we have credentials for whatever service is selected + const service = currentSettings.service; + let hasCredentials = false; - switch (service) { - case "lastfm": - hasCredentials = !!(currentSettings.username && currentSettings.apiKey); - break; - case "librefm": - hasCredentials = !!( - currentSettings.librefmUsername && currentSettings.librefmApiKey - ); - break; - case "listenbrainz": - hasCredentials = !!currentSettings.listenbrainzUsername; - break; - } + switch (service) { + case "lastfm": + hasCredentials = !!(currentSettings.username && currentSettings.apiKey); + break; + case "librefm": + hasCredentials = !!( + currentSettings.librefmUsername && currentSettings.librefmApiKey + ); + break; + case "listenbrainz": + hasCredentials = !!currentSettings.listenbrainzUsername; + break; + } - if (!hasCredentials) { - logError( - `Missing credentials for ${serviceName}. Please configure in plugin settings.`, - ); - return; - } + if (!hasCredentials) { + logError( + `Missing credentials for ${serviceName}. Please configure in plugin settings.`, + ); + return; + } - log(`Starting with ${serviceName}...`); + log(`Starting with ${serviceName}...`); - if (UserStore.getCurrentUser()) { - log("Discord is already connected, initializing immediately..."); - await tryInitialize(); - } else { - log("Waiting for Discord connection..."); - const waitForUser = () => { - if (UserStore.getCurrentUser()) { - log("Discord connection established"); - tryInitialize(); - FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser); - } - }; + if (UserStore.getCurrentUser()) { + log("Discord is already connected, initializing immediately..."); + await tryInitialize(); + } else { + log("Waiting for Discord connection..."); + const waitForUser = () => { + if (UserStore.getCurrentUser()) { + log("Discord connection established"); + tryInitialize(); + FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser); + } + }; - FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser); - } + FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser); + } } export default { - onLoad() { - log("Plugin loading..."); - pluginState.pluginStopped = false; + onLoad() { + log("Plugin loading..."); + pluginState.pluginStopped = false; - // Check if service is configured - if (!currentSettings.service) { - log("No service configured. Please select a service in plugin settings."); - } else { - // Show what we're starting with - const serviceName = serviceFactory.getCurrentService().getServiceName(); - log( - `Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`, - ); - } - - // Add to sidebar if user wants it - try { - sidebarUnpatch = patchSidebar(); - cleanupFunctions.push(() => { - if (sidebarUnpatch) { - sidebarUnpatch(); - sidebarUnpatch = undefined; + // Check if service is configured + if (!currentSettings.service) { + log("No service configured. Please select a service in plugin settings."); + } else { + // Show what we're starting with + const serviceName = serviceFactory.getCurrentService().getServiceName(); + log( + `Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`, + ); } - }); - } catch (error) { - log("Sidebar setup failed:", error); - } - validateAndInitialize(); - }, - - onUnload() { - log("Plugin unloading..."); - pluginState.pluginStopped = true; - - // Run all cleanup functions - cleanupFunctions.forEach((cleanup) => { - try { - cleanup(); - } catch (error) { - // ignore cleanup errors - } - }); - cleanupFunctions.length = 0; - - stop(); - log("Plugin unloaded"); - }, - - // When user changes settings - async onSettingsUpdate(newSettings: any) { - const oldService = currentSettings.service; - const newService = newSettings.service; - const oldSidebar = currentSettings.addToSidebar; - const newSidebar = newSettings.addToSidebar; - - // Apply the new settings - Object.assign(currentSettings, newSettings); - - log("Settings updated:", Object.keys(newSettings)); - - // Check if sidebar setting changed - if (oldSidebar !== newSidebar) { - if (newSidebar) { + // Add to sidebar if user wants it try { - sidebarUnpatch = patchSidebar(); - log("Sidebar enabled"); + sidebarUnpatch = patchSidebar(); + cleanupFunctions.push(() => { + if (sidebarUnpatch) { + sidebarUnpatch(); + sidebarUnpatch = undefined; + } + }); } catch (error) { - log("Failed to enable sidebar:", error); + log("Sidebar setup failed:", error); } - } else { - if (sidebarUnpatch) { - sidebarUnpatch(); - sidebarUnpatch = undefined; - log("Sidebar disabled"); + + validateAndInitialize(); + }, + + onUnload() { + log("Plugin unloading..."); + pluginState.pluginStopped = true; + + // Run all cleanup functions + cleanupFunctions.forEach((cleanup) => { + try { + cleanup(); + } catch (error) { + // ignore cleanup errors + } + }); + cleanupFunctions.length = 0; + + stop(); + log("Plugin unloaded"); + }, + + // When user changes settings + async onSettingsUpdate(newSettings: any) { + const oldService = currentSettings.service; + const newService = newSettings.service; + const oldSidebar = currentSettings.addToSidebar; + const newSidebar = newSettings.addToSidebar; + + // Apply the new settings + Object.assign(currentSettings, newSettings); + + log("Settings updated:", Object.keys(newSettings)); + + // Check if sidebar setting changed + if (oldSidebar !== newSidebar) { + if (newSidebar) { + try { + sidebarUnpatch = patchSidebar(); + log("Sidebar enabled"); + } catch (error) { + log("Failed to enable sidebar:", error); + } + } else { + if (sidebarUnpatch) { + sidebarUnpatch(); + sidebarUnpatch = undefined; + log("Sidebar disabled"); + } + } } - } - } - // Switch services if needed - if (oldService !== newService && newService) { - log(`Service changed from ${oldService || "none"} to ${newService}`); - await switchService(newService); - } else if (!pluginState.pluginStopped && currentSettings.service) { - // Just restart with the updated settings (only if service is selected) - log("Restarting with updated settings..."); - await tryInitialize(); - } else if (!currentSettings.service) { - // Service was unselected, stop the plugin - log("Service unselected, stopping plugin..."); - stop(); - } - }, + // Switch services if needed + if (oldService !== newService && newService) { + log(`Service changed from ${oldService || "none"} to ${newService}`); + await switchService(newService); + } else if (!pluginState.pluginStopped && currentSettings.service) { + // Just restart with the updated settings (only if service is selected) + log("Restarting with updated settings..."); + await tryInitialize(); + } else if (!currentSettings.service) { + // Service was unselected, stop the plugin + log("Service unselected, stopping plugin..."); + stop(); + } + }, - onDiscordReconnect() { - if (!pluginState.pluginStopped) { - log("Discord reconnected, reinitializing plugin..."); - tryInitialize(); - } - }, + onDiscordReconnect() { + if (!pluginState.pluginStopped) { + log("Discord reconnected, reinitializing plugin..."); + tryInitialize(); + } + }, - settings: Settings, + settings: Settings, }; diff --git a/plugins/multi-scrobbler/src/manager.ts b/plugins/multi-scrobbler/src/manager.ts index 95a251f..6d27700 100644 --- a/plugins/multi-scrobbler/src/manager.ts +++ b/plugins/multi-scrobbler/src/manager.ts @@ -4,16 +4,16 @@ import Constants from "./constants"; import { SelfPresenceStore } from "./modules"; import { clearActivity, fetchAsset, sendRequest } from "./utils/activity"; import { - setDebugInfo, - recordSuccessfulUpdate, - recordServiceError, - incrementApiCall, + setDebugInfo, + recordSuccessfulUpdate, + recordServiceError, + incrementApiCall, } from "./utils/debug"; import { serviceFactory } from "./services/ServiceFactory"; import { - formatDuration, - validateTimestamps, - getCurrentTimestamp, + formatDuration, + validateTimestamps, + getCurrentTimestamp, } from "./utils/time"; enum ActivityType { @@ -25,382 +25,382 @@ enum ActivityType { const log = (...message: any[]) => console.log("[Scrobble Plugin]", ...message); const logError = (...message: any[]) => - console.error("[Scrobble Plugin] Error:", ...message); + console.error("[Scrobble Plugin] Error:", ...message); const logVerbose = (...message: any[]) => - currentSettings.verboseLogging && + currentSettings.verboseLogging && console.log("[Scrobble Plugin] Verbose:", ...message); class PluginManager { - private static instance: PluginManager; - private updateTimer?: any; - private reconnectTimer?: any; - private consecutiveFailures = 0; - private isReconnecting = false; - private currentActivity?: Activity; - private lastUpdateTime = 0; + private static instance: PluginManager; + private updateTimer?: any; + private reconnectTimer?: any; + private consecutiveFailures = 0; + private isReconnecting = false; + private currentActivity?: Activity; + private lastUpdateTime = 0; - private constructor() { + private constructor() { // singleton pattern - } - - public static getInstance(): PluginManager { - if (!PluginManager.instance) { - PluginManager.instance = new PluginManager(); - } - return PluginManager.instance; - } - - // Check for new tracks and update Discord status when something changes - private async updateActivity() { - if (pluginState.pluginStopped) { - logVerbose("Plugin is stopped, skipping update"); - this.stopUpdates(); - return; } - const serviceName = serviceFactory.getCurrentService().getServiceName(); - logVerbose(`Fetching latest track from ${serviceName}...`); - - // keep track of whether we actually need to update Discord - let willUpdateRPC = false; - - try { - // skip if Spotify is playing and user wants to ignore it - if (currentSettings.ignoreSpotify) { - const spotifyActivity = SelfPresenceStore.findActivity( - (act) => act.sync_id, - ); - if (spotifyActivity) { - logVerbose("Spotify is currently playing, clearing activity"); - setDebugInfo("isSpotifyIgnored", true); - clearActivity(); - return; + public static getInstance(): PluginManager { + if (!PluginManager.instance) { + PluginManager.instance = new PluginManager(); } - setDebugInfo("isSpotifyIgnored", false); - } + return PluginManager.instance; + } - incrementApiCall(); - const lastTrack = await serviceFactory - .getCurrentService() - .fetchLatestScrobble(); - setDebugInfo("lastTrack", lastTrack); + // Check for new tracks and update Discord status when something changes + private async updateActivity() { + if (pluginState.pluginStopped) { + logVerbose("Plugin is stopped, skipping update"); + this.stopUpdates(); + return; + } - if (!lastTrack.nowPlaying) { - logVerbose("Track is not currently playing"); - clearActivity(); - return; - } + const serviceName = serviceFactory.getCurrentService().getServiceName(); + logVerbose(`Fetching latest track from ${serviceName}...`); - // use simple URL comparison like Last.fm plugin - if (pluginState.lastTrackUrl === lastTrack.url) { - logVerbose("Track hasn't changed"); - recordSuccessfulUpdate(); - this.consecutiveFailures = 0; - return; - } + // keep track of whether we actually need to update Discord + let willUpdateRPC = false; - willUpdateRPC = true; + try { + // skip if Spotify is playing and user wants to ignore it + if (currentSettings.ignoreSpotify) { + const spotifyActivity = SelfPresenceStore.findActivity( + (act) => act.sync_id, + ); + if (spotifyActivity) { + logVerbose("Spotify is currently playing, clearing activity"); + setDebugInfo("isSpotifyIgnored", true); + clearActivity(); + return; + } + setDebugInfo("isSpotifyIgnored", false); + } - log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`); + incrementApiCall(); + const lastTrack = await serviceFactory + .getCurrentService() + .fetchLatestScrobble(); + setDebugInfo("lastTrack", lastTrack); - // set up timestamps for the track - let activityTimestamps; - if ( - lastTrack.nowPlaying && + if (!lastTrack.nowPlaying) { + logVerbose("Track is not currently playing"); + clearActivity(); + return; + } + + // use simple URL comparison like Last.fm plugin + if (pluginState.lastTrackUrl === lastTrack.url) { + logVerbose("Track hasn't changed"); + recordSuccessfulUpdate(); + this.consecutiveFailures = 0; + return; + } + + willUpdateRPC = true; + + log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`); + + // set up timestamps for the track + let activityTimestamps; + if ( + lastTrack.nowPlaying && currentSettings.showTimestamp && lastTrack.from - ) { - // figure out when this track actually started - const now = getCurrentTimestamp(); - let startTime = lastTrack.from; + ) { + // figure out when this track actually started + const now = getCurrentTimestamp(); + let startTime = lastTrack.from; - // if the timestamp is way old, estimate when it started - if (startTime < now - 3600) { - // more than an hour old - probably wrong - if (lastTrack.duration && lastTrack.duration > 0) { - // guess we're about 10% in or 30 seconds, whatever's smaller - const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30); - startTime = now - estimatedElapsed; - } else { - startTime = now; - } - logVerbose("had to estimate start time"); - } + // if the timestamp is way old, estimate when it started + if (startTime < now - 3600) { + // more than an hour old - probably wrong + if (lastTrack.duration && lastTrack.duration > 0) { + // guess we're about 10% in or 30 seconds, whatever's smaller + const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30); + startTime = now - estimatedElapsed; + } else { + startTime = now; + } + logVerbose("had to estimate start time"); + } - activityTimestamps = { - start: startTime * 1000, // Discord wants milliseconds - }; + activityTimestamps = { + start: startTime * 1000, // Discord wants milliseconds + }; - if (lastTrack.to) { - activityTimestamps.end = lastTrack.to * 1000; - } - } + if (lastTrack.to) { + activityTimestamps.end = lastTrack.to * 1000; + } + } - logVerbose( - `🎯 Preparing RPC update for: ${lastTrack.artist} - ${lastTrack.name}`, - ); + logVerbose( + `🎯 Preparing RPC update for: ${lastTrack.artist} - ${lastTrack.name}`, + ); - 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 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, + }; - // replace template variables in app name if user is using them - if (activity.name.includes("{{")) { - const variables = { - artist: lastTrack.artist, - name: lastTrack.name, - album: lastTrack.album, - service: serviceName, - }; + // replace template variables in app name if user is using them + if (activity.name.includes("{{")) { + const variables = { + artist: lastTrack.artist, + name: lastTrack.name, + album: lastTrack.album, + service: serviceName, + }; - for (const [key, value] of Object.entries(variables)) { - activity.name = activity.name.replace( - new RegExp(`{{${key}}}`, "g"), - value || "", - ); - } - } + for (const [key, value] of Object.entries(variables)) { + activity.name = activity.name.replace( + new RegExp(`{{${key}}}`, "g"), + value || "", + ); + } + } - // add the timestamps we figured out - if (activityTimestamps) { - activity.timestamps = activityTimestamps; + // add the timestamps we figured out + if (activityTimestamps) { + activity.timestamps = activityTimestamps; - logVerbose("Timestamps set:", { - start: new Date(activityTimestamps.start).toISOString(), - end: activityTimestamps.end - ? new Date(activityTimestamps.end).toISOString() - : "none", - duration: lastTrack.duration - ? formatDuration(lastTrack.duration) - : "unknown", - }); - } + logVerbose("Timestamps set:", { + start: new Date(activityTimestamps.start).toISOString(), + end: activityTimestamps.end + ? new Date(activityTimestamps.end).toISOString() + : "none", + duration: lastTrack.duration + ? formatDuration(lastTrack.duration) + : "unknown", + }); + } - // set up album art and tooltip text - if (lastTrack.album || lastTrack.albumArt) { - const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : []; - const assets = await fetchAsset(assetUrls); + // set up album art and tooltip text + if (lastTrack.album || lastTrack.albumArt) { + const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : []; + const assets = await fetchAsset(assetUrls); - if (assets[0]) { - const largeText = lastTrack.album - ? `on ${lastTrack.album}` - : `${lastTrack.artist} - ${lastTrack.name}`; + if (assets[0]) { + const largeText = lastTrack.album + ? `on ${lastTrack.album}` + : `${lastTrack.artist} - ${lastTrack.name}`; - // include track length in the tooltip if we have it - const durationText = + // include track length in the tooltip if we have it + const durationText = currentSettings.showTimestamp && lastTrack.duration - ? ` • ${formatDuration(lastTrack.duration)}` - : ""; + ? ` • ${formatDuration(lastTrack.duration)}` + : ""; - activity.assets = { - large_image: assets[0], - large_text: largeText + durationText, - }; + activity.assets = { + large_image: assets[0], + large_text: largeText + durationText, + }; - logVerbose("Album art set:", assets[0]); - } else if (lastTrack.album) { - // no album art but we can still show the album name - activity.assets = { - large_text: `on ${lastTrack.album}`, - }; + logVerbose("Album art set:", assets[0]); + } else if (lastTrack.album) { + // no album art but we can still show the album name + activity.assets = { + large_text: `on ${lastTrack.album}`, + }; + } + } + + logVerbose("Setting Discord activity:", activity); + setDebugInfo("lastActivity", activity); + + await sendRequest(activity); + pluginState.lastTrackUrl = lastTrack.url; + this.currentActivity = activity; + pluginState.lastActivity = activity; + this.consecutiveFailures = 0; + this.lastUpdateTime = getCurrentTimestamp(); + recordSuccessfulUpdate(); + + log( + `✅ RPC updated successfully: ${lastTrack.artist} - ${lastTrack.name}`, + ); + } catch (error) { + logError("Update failed:", error); + recordServiceError(currentSettings.service, (error as Error).message); + this.handleError(error as Error); } - } - - logVerbose("Setting Discord activity:", activity); - setDebugInfo("lastActivity", activity); - - await sendRequest(activity); - pluginState.lastTrackUrl = lastTrack.url; - this.currentActivity = activity; - pluginState.lastActivity = activity; - this.consecutiveFailures = 0; - this.lastUpdateTime = getCurrentTimestamp(); - recordSuccessfulUpdate(); - - log( - `✅ RPC updated successfully: ${lastTrack.artist} - ${lastTrack.name}`, - ); - } catch (error) { - logError("Update failed:", error); - recordServiceError(currentSettings.service, (error as Error).message); - this.handleError(error as Error); - } - } - - private handleError(error: Error) { - this.consecutiveFailures++; - setDebugInfo("lastError", error); - - logError( - `Failure ${this.consecutiveFailures}/${Constants.MAX_RETRY_ATTEMPTS}:`, - error.message, - ); - - if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) { - logError("Max retry attempts reached, initiating reconnection..."); - this.startReconnection(); - } - } - - private startReconnection() { - if (this.isReconnecting) return; - - this.isReconnecting = true; - this.stopUpdates(); - - log("Starting reconnection process..."); - this.reconnectTimer = setInterval(() => { - log("Attempting to reconnect..."); - this.initialize() // don't reset state - .then(() => { - log("Reconnection successful!"); - this.stopReconnection(); - }) - .catch((error) => { - logError("Reconnection attempt failed:", error.message); - }); - }, Constants.RETRY_DELAY); - } - - private stopReconnection() { - try { - if (this.reconnectTimer) { - clearInterval(this.reconnectTimer); - this.reconnectTimer = undefined; - } - this.isReconnecting = false; - this.consecutiveFailures = 0; - } catch (error) { - // Silently handle reconnection cleanup errors - console.error("[Scrobble Plugin] Reconnection cleanup error:", error); - } - } - - // clean up all timers - private stopUpdates() { - try { - if (this.updateTimer) { - clearInterval(this.updateTimer); - this.updateTimer = undefined; - } - } catch (error) { - // Silently handle timer cleanup errors - console.error("[Scrobble Plugin] Timer cleanup error:", error); - } - } - - // start everything up - public async initialize() { - if (pluginState.pluginStopped) { - throw new Error("Plugin is stopped"); } - const serviceName = serviceFactory.getCurrentService().getServiceName(); - log(`Initializing with ${serviceName}...`); + private handleError(error: Error) { + this.consecutiveFailures++; + setDebugInfo("lastError", error); - // make sure credentials work before starting - try { - const isValid = await serviceFactory.validateCurrentService(); - if (!isValid) { - throw new Error(`Invalid credentials for ${serviceName}`); - } - log(`${serviceName} credentials validated successfully`); - } catch (error) { - logError(`Failed to validate ${serviceName} credentials:`, error); - throw error; + logError( + `Failure ${this.consecutiveFailures}/${Constants.MAX_RETRY_ATTEMPTS}:`, + error.message, + ); + + if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) { + logError("Max retry attempts reached, initiating reconnection..."); + this.startReconnection(); + } } - this.stopUpdates(); + private startReconnection() { + if (this.isReconnecting) return; - // check right away in case something is already playing - log("🎵 checking for already playing songs..."); - // Don't reset track state - let updateActivity() handle whether to update - await this.updateActivity(); + this.isReconnecting = true; + this.stopUpdates(); - const interval = Math.max( - (Number(currentSettings.timeInterval) || + log("Starting reconnection process..."); + this.reconnectTimer = setInterval(() => { + log("Attempting to reconnect..."); + this.initialize() // don't reset state + .then(() => { + log("Reconnection successful!"); + this.stopReconnection(); + }) + .catch((error) => { + logError("Reconnection attempt failed:", error.message); + }); + }, Constants.RETRY_DELAY); + } + + private stopReconnection() { + try { + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer); + this.reconnectTimer = undefined; + } + this.isReconnecting = false; + this.consecutiveFailures = 0; + } catch (error) { + // Silently handle reconnection cleanup errors + console.error("[Scrobble Plugin] Reconnection cleanup error:", error); + } + } + + // clean up all timers + private stopUpdates() { + try { + if (this.updateTimer) { + clearInterval(this.updateTimer); + this.updateTimer = undefined; + } + } catch (error) { + // Silently handle timer cleanup errors + console.error("[Scrobble Plugin] Timer cleanup error:", error); + } + } + + // start everything up + public async initialize() { + if (pluginState.pluginStopped) { + throw new Error("Plugin is stopped"); + } + + const serviceName = serviceFactory.getCurrentService().getServiceName(); + log(`Initializing with ${serviceName}...`); + + // make sure credentials work before starting + try { + const isValid = await serviceFactory.validateCurrentService(); + if (!isValid) { + throw new Error(`Invalid credentials for ${serviceName}`); + } + log(`${serviceName} credentials validated successfully`); + } catch (error) { + logError(`Failed to validate ${serviceName} credentials:`, error); + throw error; + } + + this.stopUpdates(); + + // check right away in case something is already playing + log("🎵 checking for already playing songs..."); + // Don't reset track state - let updateActivity() handle whether to update + await this.updateActivity(); + + const interval = Math.max( + (Number(currentSettings.timeInterval) || Constants.DEFAULT_SETTINGS.timeInterval) * 1000, - Constants.MIN_UPDATE_INTERVAL * 1000, - ); + Constants.MIN_UPDATE_INTERVAL * 1000, + ); - this.updateTimer = setInterval(() => this.updateActivity(), interval); - log( - `Update timer started with interval: ${interval}ms (${interval / 1000}s)`, - ); - } - - // stop everything and clean up - public stop() { - if (pluginState.pluginStopped) { - return; // already stopped + this.updateTimer = setInterval(() => this.updateActivity(), interval); + log( + `Update timer started with interval: ${interval}ms (${interval / 1000}s)`, + ); } - log("Stopping plugin..."); - pluginState.pluginStopped = true; + // stop everything and clean up + public stop() { + if (pluginState.pluginStopped) { + return; // already stopped + } - try { - this.stopUpdates(); - this.stopReconnection(); - clearActivity(); - // Don't reset track state on stop - only when actually switching services - log("Plugin stopped successfully"); - } catch (error) { - // don't let cleanup errors break things - console.error("[Scrobble Plugin] Stop error:", error); - } - } + log("Stopping plugin..."); + pluginState.pluginStopped = true; - // change to a different scrobble service - public async switchService(newService: string) { - if (pluginState.pluginStopped) { - return; + try { + this.stopUpdates(); + this.stopReconnection(); + clearActivity(); + // Don't reset track state on stop - only when actually switching services + log("Plugin stopped successfully"); + } catch (error) { + // don't let cleanup errors break things + console.error("[Scrobble Plugin] Stop error:", error); + } } - log(`Switching to ${newService}...`); + // change to a different scrobble service + public async switchService(newService: string) { + if (pluginState.pluginStopped) { + return; + } - // stop what we're doing first - const wasRunning = !pluginState.pluginStopped; - this.stop(); + log(`Switching to ${newService}...`); - try { - // clear cache so we get fresh service instances - serviceFactory.clearCache(); + // stop what we're doing first + const wasRunning = !pluginState.pluginStopped; + this.stop(); - // Reset track state when switching services - pluginState.lastTrackUrl = undefined; - this.currentActivity = undefined; - this.lastUpdateTime = 0; + try { + // clear cache so we get fresh service instances + serviceFactory.clearCache(); - // start back up with the new service - if (wasRunning) { - pluginState.pluginStopped = false; - await this.initialize(); - } - } catch (error) { - logError("Failed to switch service:", error); + // Reset track state when switching services + pluginState.lastTrackUrl = undefined; + this.currentActivity = undefined; + this.lastUpdateTime = 0; + + // start back up with the new service + if (wasRunning) { + pluginState.pluginStopped = false; + await this.initialize(); + } + } catch (error) { + logError("Failed to switch service:", error); + } } - } - // get info about what's currently happening - public getStatus() { - const serviceName = serviceFactory.getCurrentService().getServiceName(); - return { - running: !pluginState.pluginStopped, - service: serviceName, - consecutiveFailures: this.consecutiveFailures, - isReconnecting: this.isReconnecting, - lastTrackUrl: pluginState.lastTrackUrl, - updateInterval: this.updateTimer ? "Active" : "Inactive", - }; - } + // get info about what's currently happening + public getStatus() { + const serviceName = serviceFactory.getCurrentService().getServiceName(); + return { + running: !pluginState.pluginStopped, + service: serviceName, + consecutiveFailures: this.consecutiveFailures, + isReconnecting: this.isReconnecting, + lastTrackUrl: pluginState.lastTrackUrl, + updateInterval: this.updateTimer ? "Active" : "Inactive", + }; + } } // expose the manager functions @@ -408,5 +408,5 @@ const manager = PluginManager.getInstance(); export const initialize = () => manager.initialize(); export const stop = () => manager.stop(); export const switchService = (service: string) => - manager.switchService(service); + manager.switchService(service); export const getStatus = () => manager.getStatus(); diff --git a/plugins/multi-scrobbler/src/services/BaseService.ts b/plugins/multi-scrobbler/src/services/BaseService.ts index 3df5845..da49b80 100644 --- a/plugins/multi-scrobbler/src/services/BaseService.ts +++ b/plugins/multi-scrobbler/src/services/BaseService.ts @@ -3,119 +3,119 @@ import Constants from "../constants"; import { incrementApiCall, recordServiceError } from "../utils/debug"; export abstract class BaseService implements ServiceClient { - protected retryCount = 0; - protected lastError = 0; + protected retryCount = 0; + protected lastError = 0; abstract fetchLatestScrobble(): Promise; abstract validateCredentials(): Promise; abstract getServiceName(): string; protected log(...args: any[]): void { - console.log(`[${this.getServiceName()}]`, ...args); + console.log(`[${this.getServiceName()}]`, ...args); } protected logError(...args: any[]): void { - console.error(`[${this.getServiceName()}] Error:`, ...args); + console.error(`[${this.getServiceName()}] Error:`, ...args); } protected logVerbose(...args: any[]): void { - // subclasses will check if verbose logging is enabled - console.log(`[${this.getServiceName()}] Verbose:`, ...args); + // subclasses will check if verbose logging is enabled + console.log(`[${this.getServiceName()}] Verbose:`, ...args); } protected async handleError(error: any): Promise { - this.lastError = error.error || 0; + this.lastError = error.error || 0; - const errorMessage = this.getErrorMessage(error); - this.logError(errorMessage); - recordServiceError( + const errorMessage = this.getErrorMessage(error); + this.logError(errorMessage); + recordServiceError( this.getServiceName().toLowerCase() as any, errorMessage, - ); + ); - throw new Error(`${this.getServiceName()} API Error: ${errorMessage}`); + throw new Error(`${this.getServiceName()} API Error: ${errorMessage}`); } protected getErrorMessage(error: any): string { - if (error.error && Constants.API_ERROR_CODES[error.error]) { - return Constants.API_ERROR_CODES[error.error]; - } - return error.message || error.toString() || "Unknown error"; + if (error.error && Constants.API_ERROR_CODES[error.error]) { + return Constants.API_ERROR_CODES[error.error]; + } + return error.message || error.toString() || "Unknown error"; } protected async makeRequest( - url: string, - options: RequestInit = {}, + url: string, + options: RequestInit = {}, ): Promise { - try { - this.logVerbose(`Making request to: ${url}`); - incrementApiCall(); + try { + this.logVerbose(`Making request to: ${url}`); + incrementApiCall(); - const response = await fetch(url, { - ...options, - headers: { - "User-Agent": "Vendetta Multi-Service Scrobbler/3.0.0", - ...options.headers, - }, - }); + const response = await fetch(url, { + ...options, + headers: { + "User-Agent": "Vendetta Multi-Service Scrobbler/3.0.0", + ...options.headers, + }, + }); - if (!response.ok) { - const error = new Error( - `HTTP ${response.status}: ${response.statusText}`, - ); - recordServiceError( + if (!response.ok) { + const error = new Error( + `HTTP ${response.status}: ${response.statusText}`, + ); + recordServiceError( this.getServiceName().toLowerCase() as any, error.message, - ); - throw error; - } + ); + throw error; + } - const data = await response.json(); + const data = await response.json(); - if (data.error) { - await this.handleError(data); - } + if (data.error) { + await this.handleError(data); + } - this.retryCount = 0; // worked, reset retry counter - return data; - } catch (error) { - this.retryCount++; - if (this.retryCount > Constants.MAX_RETRY_ATTEMPTS) { - this.retryCount = 0; - recordServiceError( + this.retryCount = 0; // worked, reset retry counter + return data; + } catch (error) { + this.retryCount++; + if (this.retryCount > Constants.MAX_RETRY_ATTEMPTS) { + this.retryCount = 0; + recordServiceError( this.getServiceName().toLowerCase() as any, `Max retries exceeded: ${(error as Error).message}`, - ); - throw error; - } + ); + throw error; + } - this.logVerbose( - `Request failed, retrying (${this.retryCount}/${Constants.MAX_RETRY_ATTEMPTS})`, - ); - await new Promise((resolve) => - setTimeout(resolve, Constants.RETRY_DELAY), - ); - return this.makeRequest(url, options); - } + this.logVerbose( + `Request failed, retrying (${this.retryCount}/${Constants.MAX_RETRY_ATTEMPTS})`, + ); + await new Promise((resolve) => + setTimeout(resolve, Constants.RETRY_DELAY), + ); + return this.makeRequest(url, options); + } } protected isDefaultCover(cover?: string): boolean { - if (!cover) return true; - return Constants.DEFAULT_COVER_HASHES.some((hash) => cover.includes(hash)); + if (!cover) return true; + return Constants.DEFAULT_COVER_HASHES.some((hash) => cover.includes(hash)); } protected processAlbumArt(cover?: string): string | null { - if (!cover || this.isDefaultCover(cover)) { - return null; - } - return cover; + if (!cover || this.isDefaultCover(cover)) { + return null; + } + return cover; } public getLastError(): number { - return this.lastError; + return this.lastError; } public resetRetryCount(): void { - this.retryCount = 0; + this.retryCount = 0; } } diff --git a/plugins/multi-scrobbler/src/services/LastFmService.ts b/plugins/multi-scrobbler/src/services/LastFmService.ts index 07ab059..8443eb8 100644 --- a/plugins/multi-scrobbler/src/services/LastFmService.ts +++ b/plugins/multi-scrobbler/src/services/LastFmService.ts @@ -38,134 +38,134 @@ interface LastFMTrack { } export class LastFmService extends BaseService { - getServiceName(): string { - return "Last.fm"; - } - - protected logVerbose(...args: any[]): void { - if (currentSettings.verboseLogging) { - console.log(`[${this.getServiceName()}] Verbose:`, ...args); + getServiceName(): string { + return "Last.fm"; } - } - async validateCredentials(): Promise { - try { - if (!currentSettings.username || !currentSettings.apiKey) { - throw new Error("Username or API key not set"); - } - - const params = new URLSearchParams({ - method: "user.getinfo", - user: currentSettings.username, - api_key: currentSettings.apiKey, - format: "json", - }); - - const url = `${Constants.SERVICES.lastfm.baseUrl}?${params}`; - await this.makeRequest(url); - - this.log("Credentials validation successful"); - return true; - } catch (error) { - this.logError("Credentials validation failed:", error); - return false; + protected logVerbose(...args: any[]): void { + if (currentSettings.verboseLogging) { + console.log(`[${this.getServiceName()}] Verbose:`, ...args); + } } - } - async fetchLatestScrobble(): Promise { - try { - if (!currentSettings.username || !currentSettings.apiKey) { - throw new Error("Username or API key not set"); - } - - this.logVerbose( - "Fetching latest scrobble for user:", - currentSettings.username, - ); - - const params = new URLSearchParams({ - method: "user.getrecenttracks", - user: currentSettings.username, - api_key: currentSettings.apiKey, - limit: "1", - extended: "1", - format: "json", - }); - - const url = `${Constants.SERVICES.lastfm.baseUrl}?${params}`; - const data: LastFMResponse = await this.makeRequest(url); - - const lastTrack = data?.recenttracks?.track?.[0]; - if (!lastTrack) { - throw new Error("No tracks found"); - } - - this.logVerbose("Raw track data:", lastTrack); - - const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying); - const trackTimestamp = lastTrack.date?.uts - ? parseInt(lastTrack.date.uts) - : Math.floor(Date.now() / 1000); - - let duration: number | null = null; - let endTime: number | null = null; - - // get track length if it's currently playing - if (isNowPlaying) { + async validateCredentials(): Promise { try { - const trackInfoParams = new URLSearchParams({ - method: "track.getInfo", - track: lastTrack.name, - artist: lastTrack.artist.name, - api_key: currentSettings.apiKey, - format: "json", - }); + if (!currentSettings.username || !currentSettings.apiKey) { + throw new Error("Username or API key not set"); + } - const trackInfoUrl = `${Constants.SERVICES.lastfm.baseUrl}?${trackInfoParams}`; - const trackInfo: LastFMResponse = + const params = new URLSearchParams({ + method: "user.getinfo", + user: currentSettings.username, + api_key: currentSettings.apiKey, + format: "json", + }); + + const url = `${Constants.SERVICES.lastfm.baseUrl}?${params}`; + await this.makeRequest(url); + + this.log("Credentials validation successful"); + return true; + } catch (error) { + this.logError("Credentials validation failed:", error); + return false; + } + } + + async fetchLatestScrobble(): Promise { + try { + if (!currentSettings.username || !currentSettings.apiKey) { + throw new Error("Username or API key not set"); + } + + this.logVerbose( + "Fetching latest scrobble for user:", + currentSettings.username, + ); + + const params = new URLSearchParams({ + method: "user.getrecenttracks", + user: currentSettings.username, + api_key: currentSettings.apiKey, + limit: "1", + extended: "1", + format: "json", + }); + + const url = `${Constants.SERVICES.lastfm.baseUrl}?${params}`; + const data: LastFMResponse = await this.makeRequest(url); + + const lastTrack = data?.recenttracks?.track?.[0]; + if (!lastTrack) { + throw new Error("No tracks found"); + } + + this.logVerbose("Raw track data:", lastTrack); + + const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying); + const trackTimestamp = lastTrack.date?.uts + ? parseInt(lastTrack.date.uts) + : Math.floor(Date.now() / 1000); + + let duration: number | null = null; + let endTime: number | null = null; + + // get track length if it's currently playing + if (isNowPlaying) { + try { + const trackInfoParams = new URLSearchParams({ + method: "track.getInfo", + track: lastTrack.name, + artist: lastTrack.artist.name, + api_key: currentSettings.apiKey, + format: "json", + }); + + const trackInfoUrl = `${Constants.SERVICES.lastfm.baseUrl}?${trackInfoParams}`; + const trackInfo: LastFMResponse = await this.makeRequest(trackInfoUrl); - if (trackInfo?.track?.duration) { - duration = parseInt(trackInfo.track.duration); - if (duration > 0) { - // Last.fm gives duration in milliseconds, we want seconds - duration = Math.floor(duration / 1000); - endTime = trackTimestamp + duration; + if (trackInfo?.track?.duration) { + duration = parseInt(trackInfo.track.duration); + if (duration > 0) { + // Last.fm gives duration in milliseconds, we want seconds + duration = Math.floor(duration / 1000); + endTime = trackTimestamp + duration; + } + } + } catch (error) { + this.logVerbose("Failed to fetch track duration:", error); + } } - } + + const albumArt = this.processAlbumArt( + lastTrack.image?.find((img) => img.size === "large")?.["#text"], + ); + + const track: Track = { + name: lastTrack.name, + artist: lastTrack.artist.name, + album: lastTrack.album["#text"], + albumArt, + url: lastTrack.url, + date: lastTrack.date?.["#text"] ?? "now", + nowPlaying: isNowPlaying, + loved: lastTrack.loved === "1", + from: trackTimestamp, + to: endTime, + duration, + }; + + this.logVerbose("Processed track:", track); + this.log( + `${isNowPlaying ? "Now playing" : "Last played"}:`, + `${track.artist} - ${track.name}`, + ); + + return track; } catch (error) { - this.logVerbose("Failed to fetch track duration:", error); + this.logError("Failed to fetch latest scrobble:", error); + throw error; } - } - - const albumArt = this.processAlbumArt( - lastTrack.image?.find((img) => img.size === "large")?.["#text"], - ); - - const track: Track = { - name: lastTrack.name, - artist: lastTrack.artist.name, - album: lastTrack.album["#text"], - albumArt, - url: lastTrack.url, - date: lastTrack.date?.["#text"] ?? "now", - nowPlaying: isNowPlaying, - loved: lastTrack.loved === "1", - from: trackTimestamp, - to: endTime, - duration, - }; - - this.logVerbose("Processed track:", track); - this.log( - `${isNowPlaying ? "Now playing" : "Last played"}:`, - `${track.artist} - ${track.name}`, - ); - - return track; - } catch (error) { - this.logError("Failed to fetch latest scrobble:", error); - throw error; } - } } diff --git a/plugins/multi-scrobbler/src/services/LibreFmService.ts b/plugins/multi-scrobbler/src/services/LibreFmService.ts index 60bb69c..5652a17 100644 --- a/plugins/multi-scrobbler/src/services/LibreFmService.ts +++ b/plugins/multi-scrobbler/src/services/LibreFmService.ts @@ -38,137 +38,137 @@ interface LibreFMTrack { } export class LibreFmService extends BaseService { - getServiceName(): string { - return "Libre.fm"; - } - - protected logVerbose(...args: any[]): void { - if (currentSettings.verboseLogging) { - console.log(`[${this.getServiceName()}] Verbose:`, ...args); + getServiceName(): string { + return "Libre.fm"; } - } - async validateCredentials(): Promise { - try { - const username = currentSettings.librefmUsername; - const apiKey = currentSettings.librefmApiKey; - - if (!username || !apiKey) { - throw new Error("Username or API key not set for Libre.fm"); - } - - const params = new URLSearchParams({ - method: "user.getinfo", - user: username, - api_key: apiKey, - format: "json", - }); - - const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`; - await this.makeRequest(url); - - this.log("Credentials validation successful"); - return true; - } catch (error) { - this.logError("Credentials validation failed:", error); - return false; + protected logVerbose(...args: any[]): void { + if (currentSettings.verboseLogging) { + console.log(`[${this.getServiceName()}] Verbose:`, ...args); + } } - } - async fetchLatestScrobble(): Promise { - try { - const username = currentSettings.librefmUsername; - const apiKey = currentSettings.librefmApiKey; - - if (!username || !apiKey) { - throw new Error("Username or API key not set for Libre.fm"); - } - - this.logVerbose("Fetching latest scrobble for user:", username); - - const params = new URLSearchParams({ - method: "user.getrecenttracks", - user: username, - api_key: apiKey, - limit: "1", - extended: "1", - format: "json", - }); - - const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`; - const data: LibreFMResponse = await this.makeRequest(url); - - const lastTrack = data?.recenttracks?.track?.[0]; - if (!lastTrack) { - throw new Error("No tracks found"); - } - - this.logVerbose("Raw track data:", lastTrack); - - const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying); - const trackTimestamp = lastTrack.date?.uts - ? parseInt(lastTrack.date.uts) - : Math.floor(Date.now() / 1000); - - let duration: number | null = null; - let endTime: number | null = null; - - // Fetch track duration if it's currently playing - if (isNowPlaying) { + async validateCredentials(): Promise { try { - const trackInfoParams = new URLSearchParams({ - method: "track.getInfo", - track: lastTrack.name, - artist: lastTrack.artist.name, - api_key: apiKey, - format: "json", - }); + const username = currentSettings.librefmUsername; + const apiKey = currentSettings.librefmApiKey; - const trackInfoUrl = `${Constants.SERVICES.librefm.baseUrl}?${trackInfoParams}`; - const trackInfo: LibreFMResponse = + if (!username || !apiKey) { + throw new Error("Username or API key not set for Libre.fm"); + } + + const params = new URLSearchParams({ + method: "user.getinfo", + user: username, + api_key: apiKey, + format: "json", + }); + + const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`; + await this.makeRequest(url); + + this.log("Credentials validation successful"); + return true; + } catch (error) { + this.logError("Credentials validation failed:", error); + return false; + } + } + + async fetchLatestScrobble(): Promise { + try { + const username = currentSettings.librefmUsername; + const apiKey = currentSettings.librefmApiKey; + + if (!username || !apiKey) { + throw new Error("Username or API key not set for Libre.fm"); + } + + this.logVerbose("Fetching latest scrobble for user:", username); + + const params = new URLSearchParams({ + method: "user.getrecenttracks", + user: username, + api_key: apiKey, + limit: "1", + extended: "1", + format: "json", + }); + + const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`; + const data: LibreFMResponse = await this.makeRequest(url); + + const lastTrack = data?.recenttracks?.track?.[0]; + if (!lastTrack) { + throw new Error("No tracks found"); + } + + this.logVerbose("Raw track data:", lastTrack); + + const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying); + const trackTimestamp = lastTrack.date?.uts + ? parseInt(lastTrack.date.uts) + : Math.floor(Date.now() / 1000); + + let duration: number | null = null; + let endTime: number | null = null; + + // Fetch track duration if it's currently playing + if (isNowPlaying) { + try { + const trackInfoParams = new URLSearchParams({ + method: "track.getInfo", + track: lastTrack.name, + artist: lastTrack.artist.name, + api_key: apiKey, + format: "json", + }); + + const trackInfoUrl = `${Constants.SERVICES.librefm.baseUrl}?${trackInfoParams}`; + const trackInfo: LibreFMResponse = await this.makeRequest(trackInfoUrl); - if (trackInfo?.track?.duration) { - duration = parseInt(trackInfo.track.duration); - if (duration > 0) { - // Duration is in milliseconds, convert to seconds - duration = Math.floor(duration / 1000); - endTime = trackTimestamp + duration; + if (trackInfo?.track?.duration) { + duration = parseInt(trackInfo.track.duration); + if (duration > 0) { + // Duration is in milliseconds, convert to seconds + duration = Math.floor(duration / 1000); + endTime = trackTimestamp + duration; + } + } + } catch (error) { + this.logVerbose("Failed to fetch track duration:", error); + } } - } + + const albumArt = this.processAlbumArt( + lastTrack.image?.find((img) => img.size === "large")?.["#text"], + ); + + const track: Track = { + name: lastTrack.name, + artist: lastTrack.artist.name, + album: lastTrack.album["#text"], + albumArt, + url: lastTrack.url, + date: lastTrack.date?.["#text"] ?? "now", + nowPlaying: isNowPlaying, + loved: lastTrack.loved === "1", + from: trackTimestamp, + to: endTime, + duration, + }; + + this.logVerbose("Processed track:", track); + this.log( + `${isNowPlaying ? "Now playing" : "Last played"}:`, + `${track.artist} - ${track.name}`, + ); + + return track; } catch (error) { - this.logVerbose("Failed to fetch track duration:", error); + this.logError("Failed to fetch latest scrobble:", error); + throw error; } - } - - const albumArt = this.processAlbumArt( - lastTrack.image?.find((img) => img.size === "large")?.["#text"], - ); - - const track: Track = { - name: lastTrack.name, - artist: lastTrack.artist.name, - album: lastTrack.album["#text"], - albumArt, - url: lastTrack.url, - date: lastTrack.date?.["#text"] ?? "now", - nowPlaying: isNowPlaying, - loved: lastTrack.loved === "1", - from: trackTimestamp, - to: endTime, - duration, - }; - - this.logVerbose("Processed track:", track); - this.log( - `${isNowPlaying ? "Now playing" : "Last played"}:`, - `${track.artist} - ${track.name}`, - ); - - return track; - } catch (error) { - this.logError("Failed to fetch latest scrobble:", error); - throw error; } - } } diff --git a/plugins/multi-scrobbler/src/services/ListenBrainzService.ts b/plugins/multi-scrobbler/src/services/ListenBrainzService.ts index fb8e3b5..42da2f5 100644 --- a/plugins/multi-scrobbler/src/services/ListenBrainzService.ts +++ b/plugins/multi-scrobbler/src/services/ListenBrainzService.ts @@ -37,171 +37,171 @@ interface ListenBrainzPlayingNowResponse { } export class ListenBrainzService extends BaseService { - getServiceName(): string { - return "ListenBrainz"; - } - - protected logVerbose(...args: any[]): void { - if (currentSettings.verboseLogging) { - console.log(`[${this.getServiceName()}] Verbose:`, ...args); + getServiceName(): string { + return "ListenBrainz"; } - } - async validateCredentials(): Promise { - try { - const username = currentSettings.listenbrainzUsername; - const token = currentSettings.listenbrainzToken; - - if (!username) { - throw new Error("Username not set for ListenBrainz"); - } - - // For ListenBrainz, we don't need a token to read public data - // But we can test the connection by fetching user listens - const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`; - - const headers: Record = {}; - if (token) { - headers["Authorization"] = `Token ${token}`; - } - - await this.makeRequest(url, { headers }); - - this.log("Credentials validation successful"); - return true; - } catch (error) { - this.logError("Credentials validation failed:", error); - return false; - } - } - - async fetchLatestScrobble(): Promise { - try { - const username = currentSettings.listenbrainzUsername; - const token = currentSettings.listenbrainzToken; - - if (!username) { - throw new Error("Username not set for ListenBrainz"); - } - - this.logVerbose("Fetching latest scrobble for user:", username); - - // First check for currently playing track - let currentlyPlaying: ListenBrainzListen | null = null; - try { - const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`; - const headers: Record = {}; - if (token) { - headers["Authorization"] = `Token ${token}`; + protected logVerbose(...args: any[]): void { + if (currentSettings.verboseLogging) { + console.log(`[${this.getServiceName()}] Verbose:`, ...args); } + } - const playingNowData: ListenBrainzPlayingNowResponse = + async validateCredentials(): Promise { + try { + const username = currentSettings.listenbrainzUsername; + const token = currentSettings.listenbrainzToken; + + if (!username) { + throw new Error("Username not set for ListenBrainz"); + } + + // For ListenBrainz, we don't need a token to read public data + // But we can test the connection by fetching user listens + const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`; + + const headers: Record = {}; + if (token) { + headers["Authorization"] = `Token ${token}`; + } + + await this.makeRequest(url, { headers }); + + this.log("Credentials validation successful"); + return true; + } catch (error) { + this.logError("Credentials validation failed:", error); + return false; + } + } + + async fetchLatestScrobble(): Promise { + try { + const username = currentSettings.listenbrainzUsername; + const token = currentSettings.listenbrainzToken; + + if (!username) { + throw new Error("Username not set for ListenBrainz"); + } + + this.logVerbose("Fetching latest scrobble for user:", username); + + // First check for currently playing track + let currentlyPlaying: ListenBrainzListen | null = null; + try { + const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`; + const headers: Record = {}; + if (token) { + headers["Authorization"] = `Token ${token}`; + } + + const playingNowData: ListenBrainzPlayingNowResponse = await this.makeRequest(playingNowUrl, { headers }); - if (playingNowData.listens && playingNowData.listens.length > 0) { - currentlyPlaying = playingNowData.listens[0]; - currentlyPlaying.playing_now = true; - } - } catch (error) { - this.logVerbose( - "No currently playing track or failed to fetch:", - error, - ); - } + if (playingNowData.listens && playingNowData.listens.length > 0) { + currentlyPlaying = playingNowData.listens[0]; + currentlyPlaying.playing_now = true; + } + } catch (error) { + this.logVerbose( + "No currently playing track or failed to fetch:", + error, + ); + } - let latestListen: ListenBrainzListen; + let latestListen: ListenBrainzListen; - if (currentlyPlaying) { - latestListen = currentlyPlaying; - this.logVerbose("Using currently playing track"); - } else { - // Fetch recent listens - const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`; - const headers: Record = {}; - if (token) { - headers["Authorization"] = `Token ${token}`; - } + if (currentlyPlaying) { + latestListen = currentlyPlaying; + this.logVerbose("Using currently playing track"); + } else { + // Fetch recent listens + const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`; + const headers: Record = {}; + if (token) { + headers["Authorization"] = `Token ${token}`; + } - const data: ListenBrainzResponse = await this.makeRequest(url, { - headers, - }); + const data: ListenBrainzResponse = await this.makeRequest(url, { + headers, + }); - if (!data.listens || data.listens.length === 0) { - throw new Error("No listens found"); - } + if (!data.listens || data.listens.length === 0) { + throw new Error("No listens found"); + } - latestListen = data.listens[0]; - this.logVerbose("Using latest completed listen"); - } + latestListen = data.listens[0]; + this.logVerbose("Using latest completed listen"); + } - this.logVerbose("Raw listen data:", latestListen); + this.logVerbose("Raw listen data:", latestListen); - const isNowPlaying = Boolean(latestListen.playing_now); - const trackTimestamp = + const isNowPlaying = Boolean(latestListen.playing_now); + const trackTimestamp = latestListen.listened_at || Math.floor(Date.now() / 1000); - let duration: number | null = null; - let endTime: number | null = null; + let duration: number | null = null; + let endTime: number | null = null; - // Extract duration from additional_info if available - if (latestListen.track_metadata.additional_info?.duration_ms) { - duration = Math.floor( - latestListen.track_metadata.additional_info.duration_ms / 1000, - ); - if (isNowPlaying && duration > 0) { - endTime = trackTimestamp + duration; - } - } + // Extract duration from additional_info if available + if (latestListen.track_metadata.additional_info?.duration_ms) { + duration = Math.floor( + latestListen.track_metadata.additional_info.duration_ms / 1000, + ); + if (isNowPlaying && duration > 0) { + endTime = trackTimestamp + duration; + } + } - // Try to get album art from MusicBrainz or other sources - const albumArt: string | null = null; - if (latestListen.track_metadata.additional_info?.release_mbid) { - try { - // We could fetch cover art from Cover Art Archive, but for now we'll leave it null - // albumArt = `https://coverartarchive.org/release/${latestListen.track_metadata.additional_info.release_mbid}/front`; - } catch (error) { - this.logVerbose("Failed to fetch album art:", error); - } - } + // Try to get album art from MusicBrainz or other sources + const albumArt: string | null = null; + if (latestListen.track_metadata.additional_info?.release_mbid) { + try { + // We could fetch cover art from Cover Art Archive, but for now we'll leave it null + // albumArt = `https://coverartarchive.org/release/${latestListen.track_metadata.additional_info.release_mbid}/front`; + } catch (error) { + this.logVerbose("Failed to fetch album art:", error); + } + } - const track: Track = { - name: latestListen.track_metadata.track_name, - artist: latestListen.track_metadata.artist_name, - album: latestListen.track_metadata.release_name || "", - albumArt, - url: + const track: Track = { + name: latestListen.track_metadata.track_name, + artist: latestListen.track_metadata.artist_name, + album: latestListen.track_metadata.release_name || "", + albumArt, + url: latestListen.track_metadata.additional_info?.origin_url || `https://listenbrainz.org/user/${username}`, - date: isNowPlaying - ? "now" - : new Date(trackTimestamp * 1000).toISOString(), - nowPlaying: isNowPlaying, - loved: false, // ListenBrainz doesn't have a direct "loved" concept - from: trackTimestamp, - to: endTime, - duration, - }; + date: isNowPlaying + ? "now" + : new Date(trackTimestamp * 1000).toISOString(), + nowPlaying: isNowPlaying, + loved: false, // ListenBrainz doesn't have a direct "loved" concept + from: trackTimestamp, + to: endTime, + duration, + }; - this.logVerbose("Processed track:", track); - this.log( - `${isNowPlaying ? "Now playing" : "Last played"}:`, - `${track.artist} - ${track.name}`, - ); + this.logVerbose("Processed track:", track); + this.log( + `${isNowPlaying ? "Now playing" : "Last played"}:`, + `${track.artist} - ${track.name}`, + ); - return track; - } catch (error) { - this.logError("Failed to fetch latest listen:", error); - throw error; + return track; + } catch (error) { + this.logError("Failed to fetch latest listen:", error); + throw error; + } } - } - protected getErrorMessage(error: any): string { + protected getErrorMessage(error: any): string { // ListenBrainz uses different error format - if (error.error) { - return error.error; + if (error.error) { + return error.error; + } + if (error.message) { + return error.message; + } + return error.toString() || "Unknown error"; } - if (error.message) { - return error.message; - } - return error.toString() || "Unknown error"; - } } diff --git a/plugins/multi-scrobbler/src/services/ServiceFactory.ts b/plugins/multi-scrobbler/src/services/ServiceFactory.ts index 6e64057..4530255 100644 --- a/plugins/multi-scrobbler/src/services/ServiceFactory.ts +++ b/plugins/multi-scrobbler/src/services/ServiceFactory.ts @@ -5,91 +5,91 @@ import { LibreFmService } from "./LibreFmService"; import { ListenBrainzService } from "./ListenBrainzService"; export class ServiceFactory { - private static instance: ServiceFactory; - private serviceInstances: Map; + private static instance: ServiceFactory; + private serviceInstances: Map; - private constructor() { + private constructor() { // singleton pattern - this.serviceInstances = new Map(); - } - - public static getInstance(): ServiceFactory { - if (!ServiceFactory.instance) { - ServiceFactory.instance = new ServiceFactory(); + this.serviceInstances = new Map(); } - return ServiceFactory.instance; - } - public getService(serviceType?: ServiceType): ServiceClient { + public static getInstance(): ServiceFactory { + if (!ServiceFactory.instance) { + ServiceFactory.instance = new ServiceFactory(); + } + return ServiceFactory.instance; + } + + public getService(serviceType?: ServiceType): ServiceClient { // make sure map is set up - if (!this.serviceInstances) { - this.serviceInstances = new Map(); + if (!this.serviceInstances) { + this.serviceInstances = new Map(); + } + + const type = serviceType || currentSettings.service; + + if (!type) { + throw new Error( + "[ServiceFactory] No service type specified and no default service configured", + ); + } + + if (!this.serviceInstances.has(type)) { + this.serviceInstances.set(type, this.createService(type)); + } + + return this.serviceInstances.get(type)!; } - const type = serviceType || currentSettings.service; - - if (!type) { - throw new Error( - "[ServiceFactory] No service type specified and no default service configured", - ); + public getCurrentService(): ServiceClient { + return this.getService(currentSettings.service); } - if (!this.serviceInstances.has(type)) { - this.serviceInstances.set(type, this.createService(type)); + private createService(serviceType: ServiceType): ServiceClient { + switch (serviceType) { + case "lastfm": + return new LastFmService(); + case "librefm": + return new LibreFmService(); + case "listenbrainz": + return new ListenBrainzService(); + default: + throw new Error( + `[ServiceFactory] Unknown service type: ${serviceType}`, + ); + } } - return this.serviceInstances.get(type)!; - } - - public getCurrentService(): ServiceClient { - return this.getService(currentSettings.service); - } - - private createService(serviceType: ServiceType): ServiceClient { - switch (serviceType) { - case "lastfm": - return new LastFmService(); - case "librefm": - return new LibreFmService(); - case "listenbrainz": - return new ListenBrainzService(); - default: - throw new Error( - `[ServiceFactory] Unknown service type: ${serviceType}`, - ); + public clearCache(): void { + if (this.serviceInstances) { + this.serviceInstances.clear(); + } else { + this.serviceInstances = new Map(); + } } - } - public clearCache(): void { - if (this.serviceInstances) { - this.serviceInstances.clear(); - } else { - this.serviceInstances = new Map(); + public validateCurrentService(): Promise { + return this.getCurrentService().validateCredentials(); } - } - public validateCurrentService(): Promise { - return this.getCurrentService().validateCredentials(); - } - - public async testService(serviceType: ServiceType): Promise { - try { - const service = this.getService(serviceType); - return await service.validateCredentials(); - } catch (error) { - console.error(`[ServiceFactory] Failed to test ${serviceType}:`, error); - return false; + public async testService(serviceType: ServiceType): Promise { + try { + const service = this.getService(serviceType); + return await service.validateCredentials(); + } catch (error) { + console.error(`[ServiceFactory] Failed to test ${serviceType}:`, error); + return false; + } } - } - public getSupportedServices(): ServiceType[] { - return ["lastfm", "librefm", "listenbrainz"]; - } + public getSupportedServices(): ServiceType[] { + return ["lastfm", "librefm", "listenbrainz"]; + } - public getServiceDisplayName(serviceType: ServiceType): string { - const service = this.getService(serviceType); - return service.getServiceName(); - } + public getServiceDisplayName(serviceType: ServiceType): string { + const service = this.getService(serviceType); + return service.getServiceName(); + } } // main instance @@ -98,6 +98,6 @@ export const serviceFactory = ServiceFactory.getInstance(); // shortcut functions export const getCurrentService = () => serviceFactory.getCurrentService(); export const getService = (type?: ServiceType) => - serviceFactory.getService(type); + serviceFactory.getService(type); export const validateCurrentService = () => - serviceFactory.validateCurrentService(); + serviceFactory.validateCurrentService(); diff --git a/plugins/multi-scrobbler/src/sidebar.tsx b/plugins/multi-scrobbler/src/sidebar.tsx index 9dfcbe9..84d8f26 100644 --- a/plugins/multi-scrobbler/src/sidebar.tsx +++ b/plugins/multi-scrobbler/src/sidebar.tsx @@ -15,197 +15,197 @@ const bunny = window.bunny; const tabsNavigationRef = bunny?.metro?.findByPropsLazy("getRootNavigationRef"); const settingConstants = bunny?.metro?.findByPropsLazy( - "SETTING_RENDERER_CONFIG", + "SETTING_RENDERER_CONFIG", ); const SettingsOverviewScreen = bunny?.metro?.findByNameLazy( - "SettingsOverviewScreen", - false, + "SettingsOverviewScreen", + false, ); function Section({ tabs }) { - const navigation = NavigationNative.useNavigation(); + const navigation = NavigationNative.useNavigation(); - return React.createElement(FormRow, { - label: tabs.title(), - leading: React.createElement(FormRow.Icon, { source: tabs.icon }), - trailing: React.createElement(React.Fragment, {}, [ - tabs.trailing ? tabs.trailing() : null, - React.createElement(FormRow.Arrow, { key: "arrow" }), - ]), - onPress: () => { - const Component = tabs.page; - navigation.navigate("VendettaCustomPage", { - title: tabs.title(), - render: () => React.createElement(Component), - }); - }, - }); + return React.createElement(FormRow, { + label: tabs.title(), + leading: React.createElement(FormRow.Icon, { source: tabs.icon }), + trailing: React.createElement(React.Fragment, {}, [ + tabs.trailing ? tabs.trailing() : null, + React.createElement(FormRow.Arrow, { key: "arrow" }), + ]), + onPress: () => { + const Component = tabs.page; + navigation.navigate("VendettaCustomPage", { + title: tabs.title(), + render: () => React.createElement(Component), + }); + }, + }); } function patchPanelUI(tabs, patches) { - try { - patches.push( - after( - "default", - bunny?.metro?.findByNameLazy("UserSettingsOverviewWrapper", false), - (_, ret) => { - const UserSettingsOverview = findInReactTree( - ret.props.children, - (n) => n.type?.name === "UserSettingsOverview", - ); - - if (UserSettingsOverview) { - patches.push( - after( - "render", - UserSettingsOverview.type.prototype, - (_args, res) => { - const sections = findInReactTree( - res.props.children, - (n) => n?.children?.[1]?.type === FormSection, - )?.children; - - if (sections) { - const index = sections.findIndex((c) => - ["BILLING_SETTINGS", "PREMIUM_SETTINGS"].includes( - c?.props?.label, - ), + try { + patches.push( + after( + "default", + bunny?.metro?.findByNameLazy("UserSettingsOverviewWrapper", false), + (_, ret) => { + const UserSettingsOverview = findInReactTree( + ret.props.children, + (n) => n.type?.name === "UserSettingsOverview", ); - sections.splice( - -~index || 4, - 0, - React.createElement(Section, { key: tabs.key, tabs }), - ); - } + if (UserSettingsOverview) { + patches.push( + after( + "render", + UserSettingsOverview.type.prototype, + (_args, res) => { + const sections = findInReactTree( + res.props.children, + (n) => n?.children?.[1]?.type === FormSection, + )?.children; + + if (sections) { + const index = sections.findIndex((c) => + ["BILLING_SETTINGS", "PREMIUM_SETTINGS"].includes( + c?.props?.label, + ), + ); + + sections.splice( + -~index || 4, + 0, + React.createElement(Section, { key: tabs.key, tabs }), + ); + } + }, + ), + ); + } }, - ), - ); - } - }, - true, - ), - ); - } catch (error) { - logger.info("Panel UI patch failed graciously 💔", error); - } + true, + ), + ); + } catch (error) { + logger.info("Panel UI patch failed graciously 💔", error); + } } function patchTabsUI(tabs, patches) { - if (!settingConstants || !SettingsOverviewScreen || !tabsNavigationRef) { - console.warn("[LastFm] Missing required constants for tabs UI patch"); - return; - } + if (!settingConstants || !SettingsOverviewScreen || !tabsNavigationRef) { + console.warn("[LastFm] Missing required constants for tabs UI patch"); + return; + } - const row = {}; - row[tabs.key] = { - type: "pressable", - title: tabs.title, - icon: tabs.icon, - IconComponent: + const row = {}; + row[tabs.key] = { + type: "pressable", + title: tabs.title, + icon: tabs.icon, + IconComponent: tabs.icon && (() => { - const actualIconSource = + const actualIconSource = typeof tabs.icon === "object" && tabs.icon.uri !== undefined - ? tabs.icon.uri - : tabs.icon; - return React.createElement(TableRowIcon, { source: actualIconSource }); + ? tabs.icon.uri + : tabs.icon; + return React.createElement(TableRowIcon, { source: actualIconSource }); }), - usePredicate: tabs.predicate, - useTrailing: tabs.trailing, - onPress: () => { - const navigation = tabsNavigationRef.getRootNavigationRef(); - const Component = tabs.page; + usePredicate: tabs.predicate, + useTrailing: tabs.trailing, + onPress: () => { + const navigation = tabsNavigationRef.getRootNavigationRef(); + const Component = tabs.page; - navigation.navigate("VendettaCustomPage", { - title: tabs.title(), - render: () => React.createElement(Component), - }); - }, - withArrow: true, - }; + navigation.navigate("VendettaCustomPage", { + title: tabs.title(), + render: () => React.createElement(Component), + }); + }, + withArrow: true, + }; - let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG; + let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG; - Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", { - enumerable: true, - configurable: true, - get: () => ({ - ...rendererConfigValue, - ...row, - }), - set: (v) => (rendererConfigValue = v), - }); + Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", { + enumerable: true, + configurable: true, + get: () => ({ + ...rendererConfigValue, + ...row, + }), + set: (v) => (rendererConfigValue = v), + }); - const firstRender = Symbol("LastFm-pinToSettings"); + const firstRender = Symbol("LastFm-pinToSettings"); - patches.push( - after("default", SettingsOverviewScreen, (args, ret) => { - if (!args[0][firstRender]) { - args[0][firstRender] = true; + patches.push( + after("default", SettingsOverviewScreen, (args, ret) => { + if (!args[0][firstRender]) { + args[0][firstRender] = true; - const { sections } = findInReactTree( - ret, - (i) => i.props?.sections, - ).props; + const { sections } = findInReactTree( + ret, + (i) => i.props?.sections, + ).props; - const section = sections?.find((x) => - ["Bunny", "Revenge", "Kettu"].some( - (mod) => x.label === mod && x.title === mod, - ), - ); + const section = sections?.find((x) => + ["Bunny", "Revenge", "Kettu"].some( + (mod) => x.label === mod && x.title === mod, + ), + ); - if (section?.settings) { - section.settings = [...section.settings, tabs.key]; - } - } - }), - ); + if (section?.settings) { + section.settings = [...section.settings, tabs.key]; + } + } + }), + ); } function patchSettingsPin(tabs) { - const patches = []; + const patches = []; - let disabled = false; + let disabled = false; - const realPredicate = tabs.predicate || (() => true); - tabs.predicate = () => (disabled ? false : realPredicate()); + const realPredicate = tabs.predicate || (() => true); + tabs.predicate = () => (disabled ? false : realPredicate()); - patchPanelUI(tabs, patches); - patchTabsUI(tabs, patches); - patches.push(() => (disabled = true)); + patchPanelUI(tabs, patches); + patchTabsUI(tabs, patches); + patches.push(() => (disabled = true)); - return () => { - for (const x of patches) { - x(); - } - }; + return () => { + for (const x of patches) { + x(); + } + }; } export default function patchSidebar() { - // Check if sidebar option is enabled in plugin settings - if (!plugin.storage.addToSidebar) { - console.log("[Multi Scrobbler] Sidebar disabled in settings"); - return () => {}; - } + // Check if sidebar option is enabled in plugin settings + if (!plugin.storage.addToSidebar) { + console.log("[Multi Scrobbler] Sidebar disabled in settings"); + return () => {}; + } - console.log( - "[Multi Scrobbler] Patching sidebar using custom patchSettingsPin...", - ); + console.log( + "[Multi Scrobbler] Patching sidebar using custom patchSettingsPin...", + ); - try { - const unpatch = patchSettingsPin({ - key: "LastFmScrobbler", - icon: getAssetIDByName("MusicIcon"), - title: () => "Multi Scrobbler", - predicate: () => plugin.storage.addToSidebar === true, - page: Settings, - }); + try { + const unpatch = patchSettingsPin({ + key: "LastFmScrobbler", + icon: getAssetIDByName("MusicIcon"), + title: () => "Multi Scrobbler", + predicate: () => plugin.storage.addToSidebar === true, + page: Settings, + }); - console.log("[Multi Scrobbler] Successfully patched sidebar"); - return unpatch; - } catch (error) { - console.error("[Multi Scrobbler] Failed to patch sidebar:", error); - return () => {}; - } + console.log("[Multi Scrobbler] Successfully patched sidebar"); + return unpatch; + } catch (error) { + console.error("[Multi Scrobbler] Failed to patch sidebar:", error); + return () => {}; + } } diff --git a/plugins/multi-scrobbler/src/ui/pages/Settings.tsx b/plugins/multi-scrobbler/src/ui/pages/Settings.tsx index b5a43ee..e1cf815 100644 --- a/plugins/multi-scrobbler/src/ui/pages/Settings.tsx +++ b/plugins/multi-scrobbler/src/ui/pages/Settings.tsx @@ -9,10 +9,10 @@ import { getAssetIDByName } from "@vendetta/ui/assets"; const { ScrollView } = findByProps("ScrollView"); const { TableRowGroup, TableSwitchRow, Stack, TableRow } = findByProps( - "TableSwitchRow", - "TableRowGroup", - "Stack", - "TableRow", + "TableSwitchRow", + "TableRowGroup", + "Stack", + "TableRow", ); const { TextInput } = findByProps("TextInput"); @@ -40,526 +40,526 @@ const set = (k: string, v: any) => (plugin.storage[k] = v); // Last.fm Settings Page function LastFmSettingsPage() { - useProxy(plugin.storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(plugin.storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const testConnection = async () => { - showToast("Testing Last.fm connection...", getAssetIDByName("ClockIcon")); - try { - const isValid = await serviceFactory.testService("lastfm"); - if (isValid) { - showToast( - "✅ Last.fm connection successful!", - getAssetIDByName("CheckIcon"), - ); - } else { - showToast("❌ Last.fm connection failed", getAssetIDByName("XIcon")); - } - } catch (error) { - showToast("❌ Connection error", getAssetIDByName("XIcon")); - } - }; - - return ( - - - - - { - set("username", v); - forceUpdate(); - }} - isClearable - /> - { - set("apiKey", v); - forceUpdate(); - }} - secureTextEntry={true} - isClearable - /> - - - - - } - onPress={testConnection} - /> - } - onPress={async () => { - try { - await Linking.openURL("https://www.last.fm/api/account/create"); - } catch (error) { - console.error("Failed to open Last.fm API URL:", error); + const testConnection = async () => { + showToast("Testing Last.fm connection...", getAssetIDByName("ClockIcon")); + try { + const isValid = await serviceFactory.testService("lastfm"); + if (isValid) { showToast( - "Failed to open web browser. Please visit: https://www.last.fm/api/account/create", - getAssetIDByName("XIcon"), + "✅ Last.fm connection successful!", + getAssetIDByName("CheckIcon"), ); - } - }} - /> - - - - - ); + } else { + showToast("❌ Last.fm connection failed", getAssetIDByName("XIcon")); + } + } catch (error) { + showToast("❌ Connection error", getAssetIDByName("XIcon")); + } + }; + + return ( + + + + + { + set("username", v); + forceUpdate(); + }} + isClearable + /> + { + set("apiKey", v); + forceUpdate(); + }} + secureTextEntry={true} + isClearable + /> + + + + + } + onPress={testConnection} + /> + } + onPress={async () => { + try { + await Linking.openURL("https://www.last.fm/api/account/create"); + } catch (error) { + console.error("Failed to open Last.fm API URL:", error); + showToast( + "Failed to open web browser. Please visit: https://www.last.fm/api/account/create", + getAssetIDByName("XIcon"), + ); + } + }} + /> + + + + + ); } // Libre.fm Settings Page function LibreFmSettingsPage() { - useProxy(plugin.storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(plugin.storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const testConnection = async () => { - showToast("Testing Libre.fm connection...", getAssetIDByName("ClockIcon")); - try { - const isValid = await serviceFactory.testService("librefm"); - if (isValid) { - showToast( - "✅ Libre.fm connection successful!", - getAssetIDByName("CheckIcon"), - ); - } else { - showToast("❌ Libre.fm connection failed", getAssetIDByName("XIcon")); - } - } catch (error) { - showToast("❌ Connection error", getAssetIDByName("XIcon")); - } - }; - - return ( - - - - - { - set("librefmUsername", v); - forceUpdate(); - }} - isClearable - /> - { - set("librefmApiKey", v); - forceUpdate(); - }} - secureTextEntry={true} - isClearable - /> - - - - - } - onPress={testConnection} - /> - } - onPress={async () => { - try { - await Linking.openURL("https://www.last.fm/api/account/create"); - } catch (error) { - console.error("Failed to open Last.fm API URL:", error); + const testConnection = async () => { + showToast("Testing Libre.fm connection...", getAssetIDByName("ClockIcon")); + try { + const isValid = await serviceFactory.testService("librefm"); + if (isValid) { showToast( - "Failed to open web browser. Please visit: https://www.last.fm/api/account/create", - getAssetIDByName("XIcon"), + "✅ Libre.fm connection successful!", + getAssetIDByName("CheckIcon"), ); - } - }} - /> - - - - - ); + } else { + showToast("❌ Libre.fm connection failed", getAssetIDByName("XIcon")); + } + } catch (error) { + showToast("❌ Connection error", getAssetIDByName("XIcon")); + } + }; + + return ( + + + + + { + set("librefmUsername", v); + forceUpdate(); + }} + isClearable + /> + { + set("librefmApiKey", v); + forceUpdate(); + }} + secureTextEntry={true} + isClearable + /> + + + + + } + onPress={testConnection} + /> + } + onPress={async () => { + try { + await Linking.openURL("https://www.last.fm/api/account/create"); + } catch (error) { + console.error("Failed to open Last.fm API URL:", error); + showToast( + "Failed to open web browser. Please visit: https://www.last.fm/api/account/create", + getAssetIDByName("XIcon"), + ); + } + }} + /> + + + + + ); } // ListenBrainz Settings Page function ListenBrainzSettingsPage() { - useProxy(plugin.storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(plugin.storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const testConnection = async () => { - showToast( - "Testing ListenBrainz connection...", - getAssetIDByName("ClockIcon"), - ); - try { - const isValid = await serviceFactory.testService("listenbrainz"); - if (isValid) { + const testConnection = async () => { showToast( - "✅ ListenBrainz connection successful!", - getAssetIDByName("CheckIcon"), + "Testing ListenBrainz connection...", + getAssetIDByName("ClockIcon"), ); - } else { - showToast( - "❌ ListenBrainz connection failed", - getAssetIDByName("XIcon"), - ); - } - } catch (error) { - showToast("❌ Connection error", getAssetIDByName("XIcon")); - } - }; - - return ( - - - - - { - set("listenbrainzUsername", v); - forceUpdate(); - }} - isClearable - /> - { - set("listenbrainzToken", v); - forceUpdate(); - }} - secureTextEntry={true} - isClearable - /> - - - - - } - onPress={testConnection} - /> - } - onPress={async () => { - try { - await Linking.openURL("https://listenbrainz.org/settings/"); - } catch (error) { - console.error( - "Failed to open ListenBrainz settings URL:", - error, - ); + try { + const isValid = await serviceFactory.testService("listenbrainz"); + if (isValid) { showToast( - "Failed to open web browser. Please visit: https://listenbrainz.org/settings/", - getAssetIDByName("XIcon"), + "✅ ListenBrainz connection successful!", + getAssetIDByName("CheckIcon"), ); - } - }} - /> - - - - - ); + } else { + showToast( + "❌ ListenBrainz connection failed", + getAssetIDByName("XIcon"), + ); + } + } catch (error) { + showToast("❌ Connection error", getAssetIDByName("XIcon")); + } + }; + + return ( + + + + + { + set("listenbrainzUsername", v); + forceUpdate(); + }} + isClearable + /> + { + set("listenbrainzToken", v); + forceUpdate(); + }} + secureTextEntry={true} + isClearable + /> + + + + + } + onPress={testConnection} + /> + } + onPress={async () => { + try { + await Linking.openURL("https://listenbrainz.org/settings/"); + } catch (error) { + console.error( + "Failed to open ListenBrainz settings URL:", + error, + ); + showToast( + "Failed to open web browser. Please visit: https://listenbrainz.org/settings/", + getAssetIDByName("XIcon"), + ); + } + }} + /> + + + + + ); } // Display Settings Page function DisplaySettingsPage() { - useProxy(plugin.storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(plugin.storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - return ( - - - - - { - set("appName", v); - forceUpdate(); - }} - isClearable - /> - { - const interval = Number(v); - if (interval >= Constants.MIN_UPDATE_INTERVAL) { - set("timeInterval", interval); - forceUpdate(); - } - }} - keyboardType="numeric" - isClearable - /> - - + return ( + + + + + { + set("appName", v); + forceUpdate(); + }} + isClearable + /> + { + const interval = Number(v); + if (interval >= Constants.MIN_UPDATE_INTERVAL) { + set("timeInterval", interval); + forceUpdate(); + } + }} + keyboardType="numeric" + isClearable + /> + + - - { - set("listeningTo", value); - forceUpdate(); - }} - /> - { - set("showTimestamp", value); - forceUpdate(); - }} - /> - { - set("ignoreSpotify", value); - forceUpdate(); - }} - /> - - - - - ); + + { + set("listeningTo", value); + forceUpdate(); + }} + /> + { + set("showTimestamp", value); + forceUpdate(); + }} + /> + { + set("ignoreSpotify", value); + forceUpdate(); + }} + /> + + + + + ); } // Logging Settings Page function LoggingSettingsPage() { - useProxy(plugin.storage); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(plugin.storage); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - return ( - - - - { - set("verboseLogging", value); - forceUpdate(); - }} - /> - + return ( + + + + { + set("verboseLogging", value); + forceUpdate(); + }} + /> + - - - - + + + + - - - - - - - - - ); + + + + + + + + + ); } // Main Settings Page export default function Settings() { - useProxy(plugin.storage); - const navigation = NavigationNative.useNavigation(); - const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + useProxy(plugin.storage); + const navigation = NavigationNative.useNavigation(); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const currentService = get("service") as ServiceType; + const currentService = get("service") as ServiceType; - const getCredentialStatus = (service: ServiceType) => { - switch (service) { - case "lastfm": { - return get("username") && get("apiKey") - ? "✅ Configured" - : "❌ Missing credentials"; - } - case "librefm": { - return get("librefmUsername") && get("librefmApiKey") - ? "✅ Configured" - : "❌ Missing credentials"; - } - case "listenbrainz": { - return get("listenbrainzUsername") - ? "✅ Configured" - : "❌ Missing username"; - } - default: - return "❓ Unknown"; - } - }; + const getCredentialStatus = (service: ServiceType) => { + switch (service) { + case "lastfm": { + return get("username") && get("apiKey") + ? "✅ Configured" + : "❌ Missing credentials"; + } + case "librefm": { + return get("librefmUsername") && get("librefmApiKey") + ? "✅ Configured" + : "❌ Missing credentials"; + } + case "listenbrainz": { + return get("listenbrainzUsername") + ? "✅ Configured" + : "❌ Missing username"; + } + default: + return "❓ Unknown"; + } + }; - return ( - - - {/* Service Selection */} - - - {(["lastfm", "librefm", "listenbrainz"] as ServiceType[]).map( - (service) => ( - { - if (value && service !== currentService) { - set("service", service); - forceUpdate(); - } - }} - /> - ), - )} - + return ( + + + {/* Service Selection */} + + + {(["lastfm", "librefm", "listenbrainz"] as ServiceType[]).map( + (service) => ( + { + if (value && service !== currentService) { + set("service", service); + forceUpdate(); + } + }} + /> + ), + )} + - {/* Service Configuration */} - - } - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Last.fm Settings", - render: LastFmSettingsPage, - }) - } - /> - } - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Libre.fm Settings", - render: LibreFmSettingsPage, - }) - } - /> - } - onPress={() => - navigation.push("VendettaCustomPage", { - title: "ListenBrainz Settings", - render: ListenBrainzSettingsPage, - }) - } - /> - + {/* Service Configuration */} + + } + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Last.fm Settings", + render: LastFmSettingsPage, + }) + } + /> + } + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Libre.fm Settings", + render: LibreFmSettingsPage, + }) + } + /> + } + onPress={() => + navigation.push("VendettaCustomPage", { + title: "ListenBrainz Settings", + render: ListenBrainzSettingsPage, + }) + } + /> + - {/* Plugin Configuration */} - - } - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Display Settings", - render: DisplaySettingsPage, - }) - } - /> - } - onPress={() => - navigation.push("VendettaCustomPage", { - title: "Logging Settings", - render: LoggingSettingsPage, - }) - } - /> - { - set("addToSidebar", value); - forceUpdate(); - }} - /> - + {/* Plugin Configuration */} + + } + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Display Settings", + render: DisplaySettingsPage, + }) + } + /> + } + onPress={() => + navigation.push("VendettaCustomPage", { + title: "Logging Settings", + render: LoggingSettingsPage, + }) + } + /> + { + set("addToSidebar", value); + forceUpdate(); + }} + /> + - {/* About */} - - - - - - - - - ); + {/* About */} + + + + + + + + + ); } diff --git a/plugins/multi-scrobbler/src/utils/activity.ts b/plugins/multi-scrobbler/src/utils/activity.ts index 2612ab6..518ff18 100644 --- a/plugins/multi-scrobbler/src/utils/activity.ts +++ b/plugins/multi-scrobbler/src/utils/activity.ts @@ -8,68 +8,68 @@ import { AssetManager } from "../modules"; const log = (...args: any[]) => console.log("[Activity]", ...args); const logError = (...args: any[]) => - console.error("[Activity] Error:", ...args); + console.error("[Activity] Error:", ...args); const logVerbose = (...args: any[]) => { - // just log everything for now since checking the setting is tricky here - try { - console.log("[Activity] Verbose:", ...args); - } catch { - console.log("[Activity] Verbose:", ...args); - } + // just log everything for now since checking the setting is tricky here + try { + console.log("[Activity] Verbose:", ...args); + } catch { + console.log("[Activity] Verbose:", ...args); + } }; // Remove Discord activity export function clearActivity() { - log("Clearing Discord activity"); - return sendRequest(null); + log("Clearing Discord activity"); + return sendRequest(null); } // Send activity to Discord or clear it export function sendRequest(activity: Activity | null) { - if (pluginState.pluginStopped) { - logVerbose("Plugin is stopped, clearing activity"); - stop(); - activity = null; - } + if (pluginState.pluginStopped) { + logVerbose("Plugin is stopped, clearing activity"); + stop(); + activity = null; + } - pluginState.lastActivity = activity; + pluginState.lastActivity = activity; - logVerbose( - "Dispatching activity update:", - activity ? "Setting activity" : "Clearing activity", - ); + logVerbose( + "Dispatching activity update:", + activity ? "Setting activity" : "Clearing activity", + ); - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity: activity, - pid: 2312, - socketId: "Multi-Service-Scrobbler@Vendetta", - }); + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity: activity, + pid: 2312, + socketId: "Multi-Service-Scrobbler@Vendetta", + }); - if (activity) { - log(`Activity set: ${activity.details} - ${activity.state}`); - } else { - log("Activity cleared"); - } + if (activity) { + log(`Activity set: ${activity.details} - ${activity.state}`); + } else { + log("Activity cleared"); + } } // Get Discord asset for album art export async function fetchAsset( - asset: string[], - appId: string = Constants.APPLICATION_ID, + asset: string[], + appId: string = Constants.APPLICATION_ID, ): Promise { - if (!asset?.length) { - logVerbose("No assets to fetch"); - return []; - } + if (!asset?.length) { + logVerbose("No assets to fetch"); + return []; + } - try { - logVerbose(`Fetching ${asset.length} asset(s):`, asset); - const result = await AssetManager.fetchAssetIds(appId, asset); - logVerbose(`Successfully fetched ${result.length} asset(s)`); - return result; - } catch (error) { - logError("Failed to fetch assets:", error); - return []; - } + try { + logVerbose(`Fetching ${asset.length} asset(s):`, asset); + const result = await AssetManager.fetchAssetIds(appId, asset); + logVerbose(`Successfully fetched ${result.length} asset(s)`); + return result; + } catch (error) { + logError("Failed to fetch assets:", error); + return []; + } } diff --git a/plugins/multi-scrobbler/src/utils/debug.ts b/plugins/multi-scrobbler/src/utils/debug.ts index de3ed30..c5d4d9b 100644 --- a/plugins/multi-scrobbler/src/utils/debug.ts +++ b/plugins/multi-scrobbler/src/utils/debug.ts @@ -30,181 +30,181 @@ debugInfo.serviceErrors = { lastfm: [], librefm: [], listenbrainz: [] }; debugInfo.apiCallCount = 0; debugInfo.connectionAttempts = 0; debugInfo.lastCredentialValidation = { - lastfm: false, - librefm: false, - listenbrainz: false, + lastfm: false, + librefm: false, + listenbrainz: false, }; const log = (...args: any[]) => console.log("[Debug]", ...args); const logError = (...args: any[]) => console.error("[Debug] Error:", ...args); export function setDebugInfo( - key: keyof typeof debugInfo, - value: (typeof debugInfo)[typeof key], + key: keyof typeof debugInfo, + value: (typeof debugInfo)[typeof key], ) { - debugInfo[key] = value; + debugInfo[key] = value; - // log the important stuff - if (key === "lastError" && value) { - logError("Error recorded:", (value as Error).message); - } else if (key === "lastTrack" && value) { - log( - "Track updated:", - `${(value as Track).artist} - ${(value as Track).name}`, - ); - } else if (key === "currentService" && value) { - log("Service changed to:", value); - } + // log the important stuff + if (key === "lastError" && value) { + logError("Error recorded:", (value as Error).message); + } else if (key === "lastTrack" && value) { + log( + "Track updated:", + `${(value as Track).artist} - ${(value as Track).name}`, + ); + } else if (key === "currentService" && value) { + log("Service changed to:", value); + } - __forceUpdate?.(); + __forceUpdate?.(); } export function incrementApiCall() { - debugInfo.apiCallCount = (debugInfo.apiCallCount || 0) + 1; - log(`API call count: ${debugInfo.apiCallCount}`); + debugInfo.apiCallCount = (debugInfo.apiCallCount || 0) + 1; + log(`API call count: ${debugInfo.apiCallCount}`); } export function recordServiceError(service: ServiceType, error: string) { - debugInfo.serviceErrors = debugInfo.serviceErrors || { - lastfm: [], - librefm: [], - listenbrainz: [], - }; - debugInfo.serviceErrors[service] = debugInfo.serviceErrors[service] || []; - debugInfo.serviceErrors[service].push( - `${new Date().toISOString()}: ${error}`, - ); + debugInfo.serviceErrors = debugInfo.serviceErrors || { + lastfm: [], + librefm: [], + listenbrainz: [], + }; + debugInfo.serviceErrors[service] = debugInfo.serviceErrors[service] || []; + debugInfo.serviceErrors[service].push( + `${new Date().toISOString()}: ${error}`, + ); - // don't let error logs get too long - if (debugInfo.serviceErrors[service].length > 10) { - debugInfo.serviceErrors[service] = + // don't let error logs get too long + if (debugInfo.serviceErrors[service].length > 10) { + debugInfo.serviceErrors[service] = debugInfo.serviceErrors[service].slice(-10); - } + } - logError(`${service} error:`, error); - __forceUpdate?.(); + logError(`${service} error:`, error); + __forceUpdate?.(); } export function recordCredentialValidation( - service: ServiceType, - isValid: boolean, + service: ServiceType, + isValid: boolean, ) { - debugInfo.lastCredentialValidation = debugInfo.lastCredentialValidation || { - lastfm: false, - librefm: false, - listenbrainz: false, - }; - debugInfo.lastCredentialValidation[service] = isValid; - log( - `${service} credentials validation:`, - isValid ? "✅ Valid" : "❌ Invalid", - ); - __forceUpdate?.(); + debugInfo.lastCredentialValidation = debugInfo.lastCredentialValidation || { + lastfm: false, + librefm: false, + listenbrainz: false, + }; + debugInfo.lastCredentialValidation[service] = isValid; + log( + `${service} credentials validation:`, + isValid ? "✅ Valid" : "❌ Invalid", + ); + __forceUpdate?.(); } export function recordSuccessfulUpdate() { - debugInfo.lastSuccessfulUpdate = new Date().toISOString(); - log("Successful update recorded at:", debugInfo.lastSuccessfulUpdate); - __forceUpdate?.(); + debugInfo.lastSuccessfulUpdate = new Date().toISOString(); + log("Successful update recorded at:", debugInfo.lastSuccessfulUpdate); + __forceUpdate?.(); } export function incrementConnectionAttempt() { - debugInfo.connectionAttempts = (debugInfo.connectionAttempts || 0) + 1; - log(`Connection attempt: ${debugInfo.connectionAttempts}`); + debugInfo.connectionAttempts = (debugInfo.connectionAttempts || 0) + 1; + log(`Connection attempt: ${debugInfo.connectionAttempts}`); } export function resetConnectionAttempts() { - debugInfo.connectionAttempts = 0; - log("Connection attempts reset"); + debugInfo.connectionAttempts = 0; + log("Connection attempts reset"); } export function logComponentMount(componentName: string) { - debugInfo.componentMountCount = (debugInfo.componentMountCount || 0) + 1; - log( - `Component mounted: ${componentName} (count: ${debugInfo.componentMountCount})`, - ); + debugInfo.componentMountCount = (debugInfo.componentMountCount || 0) + 1; + log( + `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; + } - logError(`Component error in ${componentName}:`, error); - __forceUpdate?.(); + logError(`Component error in ${componentName}:`, error); + __forceUpdate?.(); } export function logNavigationError(error: any) { - debugInfo.lastNavigationError = String(error); - logError("Navigation error:", error); - __forceUpdate?.(); + debugInfo.lastNavigationError = String(error); + logError("Navigation error:", error); + __forceUpdate?.(); } export function incrementSettingsLoad() { - debugInfo.settingsLoadAttempts = (debugInfo.settingsLoadAttempts || 0) + 1; - log(`Settings load attempt: ${debugInfo.settingsLoadAttempts}`); + debugInfo.settingsLoadAttempts = (debugInfo.settingsLoadAttempts || 0) + 1; + log(`Settings load attempt: ${debugInfo.settingsLoadAttempts}`); } export function getDebugSummary(): string { - const summary = { - status: { - currentService: debugInfo.currentService || "unknown", - lastSuccessfulUpdate: debugInfo.lastSuccessfulUpdate || "never", - connectionAttempts: debugInfo.connectionAttempts || 0, - apiCallCount: debugInfo.apiCallCount || 0, - }, - validation: debugInfo.lastCredentialValidation, - errors: { - totalComponentErrors: debugInfo.componentMountErrors?.length || 0, - lastError: debugInfo.lastError?.message || "none", - serviceErrors: Object.fromEntries( - Object.entries(debugInfo.serviceErrors || {}).map( - ([service, errors]) => [service, errors.length], - ), - ), - }, - lastTrack: debugInfo.lastTrack - ? { - artist: debugInfo.lastTrack.artist, - name: debugInfo.lastTrack.name, - nowPlaying: debugInfo.lastTrack.nowPlaying, - service: debugInfo.currentService, - } - : null, - }; + const summary = { + status: { + currentService: debugInfo.currentService || "unknown", + lastSuccessfulUpdate: debugInfo.lastSuccessfulUpdate || "never", + connectionAttempts: debugInfo.connectionAttempts || 0, + apiCallCount: debugInfo.apiCallCount || 0, + }, + validation: debugInfo.lastCredentialValidation, + errors: { + totalComponentErrors: debugInfo.componentMountErrors?.length || 0, + lastError: debugInfo.lastError?.message || "none", + serviceErrors: Object.fromEntries( + Object.entries(debugInfo.serviceErrors || {}).map( + ([service, errors]) => [service, errors.length], + ), + ), + }, + lastTrack: debugInfo.lastTrack + ? { + artist: debugInfo.lastTrack.artist, + name: debugInfo.lastTrack.name, + nowPlaying: debugInfo.lastTrack.nowPlaying, + service: debugInfo.currentService, + } + : null, + }; - return JSON.stringify(summary, null, 2); + return JSON.stringify(summary, null, 2); } export function useDebugInfo(): string { - try { - [, __forceUpdate] = useReducer((x) => ~x, 0); + try { + [, __forceUpdate] = useReducer((x) => ~x, 0); - // add some extra runtime info - const runtimeInfo = { - ...debugInfo, - timestamp: new Date().toISOString(), - reactVersion: "18.0.0", // we can't check the real version here - hasReanimatedError: !!debugInfo.lastReanimatedError, - totalErrors: debugInfo.componentMountErrors?.length || 0, - summary: getDebugSummary(), - }; + // add some extra runtime info + const runtimeInfo = { + ...debugInfo, + timestamp: new Date().toISOString(), + reactVersion: "18.0.0", // we can't check the real version here + hasReanimatedError: !!debugInfo.lastReanimatedError, + totalErrors: debugInfo.componentMountErrors?.length || 0, + summary: getDebugSummary(), + }; - return JSON.stringify(runtimeInfo, null, 4); - } catch (error) { - logError("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) { + logError("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/multi-scrobbler/src/utils/time.ts b/plugins/multi-scrobbler/src/utils/time.ts index 4e28257..d212834 100644 --- a/plugins/multi-scrobbler/src/utils/time.ts +++ b/plugins/multi-scrobbler/src/utils/time.ts @@ -2,126 +2,126 @@ // Convert seconds to MM:SS format export function formatTime(seconds: number): string { - if (!seconds || seconds < 0) return "0:00"; + if (!seconds || seconds < 0) return "0:00"; - 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")}`; } // Convert milliseconds to MM:SS format export function formatTimeFromMs(milliseconds: number): string { - return formatTime(Math.floor(milliseconds / 1000)); + return formatTime(Math.floor(milliseconds / 1000)); } // Format duration with hours if it's a long track export function formatDuration(seconds: number): string { - if (!seconds || seconds < 0) return "0:00"; + if (!seconds || seconds < 0) return "0:00"; - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = Math.floor(seconds % 60); + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = Math.floor(seconds % 60); - if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`; - } + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`; + } - return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; + return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; } // Convert timestamp to ISO date string export function timestampToISO(timestamp: number): string { - return new Date(timestamp * 1000).toISOString(); + return new Date(timestamp * 1000).toISOString(); } // Get current time as Unix timestamp export function getCurrentTimestamp(): number { - return Math.floor(Date.now() / 1000); + return Math.floor(Date.now() / 1000); } // Figure out how far through a track we are (0-100%) export function calculateProgress( - startTime: number, - endTime?: number, - currentTime: number = getCurrentTimestamp(), + startTime: number, + endTime?: number, + currentTime: number = getCurrentTimestamp(), ): number { - if (!endTime || endTime <= startTime) return 0; + if (!endTime || endTime <= startTime) return 0; - const elapsed = currentTime - startTime; - const total = endTime - startTime; + const elapsed = currentTime - startTime; + const total = endTime - startTime; - if (elapsed <= 0) return 0; - if (elapsed >= total) return 100; + if (elapsed <= 0) return 0; + if (elapsed >= total) return 100; - return Math.round((elapsed / total) * 100); + return Math.round((elapsed / total) * 100); } // How much time is left in the track export function formatTimeRemaining( - startTime: number, - endTime: number, - currentTime: number = getCurrentTimestamp(), + startTime: number, + endTime: number, + currentTime: number = getCurrentTimestamp(), ): string { - const remaining = endTime - currentTime; - if (remaining <= 0) return "0:00"; + const remaining = endTime - currentTime; + if (remaining <= 0) return "0:00"; - return formatTime(remaining); + return formatTime(remaining); } // How much time has passed since the track started export function formatElapsedTime( - startTime: number, - currentTime: number = getCurrentTimestamp(), + startTime: number, + currentTime: number = getCurrentTimestamp(), ): string { - const elapsed = currentTime - startTime; - if (elapsed <= 0) return "0:00"; + const elapsed = currentTime - startTime; + if (elapsed <= 0) return "0:00"; - return formatTime(elapsed); + return formatTime(elapsed); } // Check if timestamps make sense export function validateTimestamps( - startTime: number, - endTime?: number, + startTime: number, + endTime?: number, ): boolean { - const now = getCurrentTimestamp(); - const oneWeekAgo = now - 7 * 24 * 60 * 60; // 7 days ago - const twoHoursFuture = now + 2 * 60 * 60; // 2 hours in future (more lenient) + const now = getCurrentTimestamp(); + const oneWeekAgo = now - 7 * 24 * 60 * 60; // 7 days ago + const twoHoursFuture = now + 2 * 60 * 60; // 2 hours in future (more lenient) - // Make sure start time isn't crazy old or in the future - // Allow some wiggle room for tracks that just started - const twoHoursAgo = now - 2 * 60 * 60; - if (startTime < oneWeekAgo || startTime > twoHoursFuture) { + // Make sure start time isn't crazy old or in the future + // Allow some wiggle room for tracks that just started + const twoHoursAgo = now - 2 * 60 * 60; + if (startTime < oneWeekAgo || startTime > twoHoursFuture) { // give recent tracks some slack - if (startTime >= twoHoursAgo && startTime <= now + 300) { - // allow 5 minutes in the future for "now playing" - return true; + if (startTime >= twoHoursAgo && startTime <= now + 300) { + // allow 5 minutes in the future for "now playing" + return true; + } + return false; } - return false; - } - // if we have an end time, make sure it makes sense - if (endTime !== undefined) { - if (endTime <= startTime) return false; + // if we have an end time, make sure it makes sense + if (endTime !== undefined) { + if (endTime <= startTime) return false; - const duration = endTime - startTime; - const maxDuration = 2 * 60 * 60; // 2 hours max - pretty generous + const duration = endTime - startTime; + const maxDuration = 2 * 60 * 60; // 2 hours max - pretty generous - if (duration > maxDuration) return false; - } + if (duration > maxDuration) return false; + } - return true; + return true; } // Turn timestamp into "2 minutes ago" style text export function formatRelativeTime(timestamp: number): string { - const now = getCurrentTimestamp(); - const diff = now - timestamp; + const now = getCurrentTimestamp(); + const diff = now - timestamp; - if (diff < 60) return "just now"; - if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`; - if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`; - if (diff < 604800) return `${Math.floor(diff / 86400)} days ago`; + if (diff < 60) return "just now"; + if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`; + if (diff < 604800) return `${Math.floor(diff / 86400)} days ago`; - return new Date(timestamp * 1000).toLocaleDateString(); + return new Date(timestamp * 1000).toLocaleDateString(); }