diff --git a/.gitignore b/.gitignore index f8e23c5..c50dd34 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Dependencies /node_modules +package-lock.json # Environment variables config.js diff --git a/art_cache.json b/art_cache.json new file mode 100644 index 0000000..014939c --- /dev/null +++ b/art_cache.json @@ -0,0 +1,6 @@ +{ + "b0e14b847e862ba4872e85211fcc7734": "https://files.catbox.moe/w9sqhb.jpg", + "cebf9979b121c2a246b517b5bc0ade5e": "https://files.catbox.moe/44ta2m.jpg", + "6aeacb78f346ceaf130b3ee9672bc9ab": "https://files.catbox.moe/p9bk6r.jpg", + "159c2bdcb53fe7d19a0c03106ecd55f4": "https://files.catbox.moe/qoj4oi.jpg" +} \ No newline at end of file diff --git a/index.js b/index.js index 31d0956..53e702b 100644 --- a/index.js +++ b/index.js @@ -3,13 +3,105 @@ import rpc from '@xhayper/discord-rpc'; const { Client, Presence } = rpc; import axios from 'axios'; import config from './config.js'; +import fs from 'fs/promises'; const client = new Client({ clientId: config.discordClientId, }); +const albumArtCache = new Map(); // Cache for uploaded album art URLs +const CACHE_FILE = 'art_cache.json'; // The file to store our cache let lastTrackId = null; +async function loadCache() { + try { + const data = await fs.readFile(CACHE_FILE, 'utf-8'); + + if (!data) { + console.log(`[Cache] ${CACHE_FILE} is empty. A new cache will be created.`); + return; + } + + const parsed = JSON.parse(data); + + // --- THE FIX --- + // Instead of reassigning albumArtCache, clear the existing map + // and add the loaded entries to it. This preserves the global reference. + albumArtCache.clear(); + for (const [key, value] of Object.entries(parsed)) { + albumArtCache.set(key, value); + } + + console.log(`[Cache] Loaded ${albumArtCache.size} items from ${CACHE_FILE}`); + } catch (error) { + if (error.code === 'ENOENT') { + console.log(`[Cache] ${CACHE_FILE} not found. A new cache will be created.`); + } else { + console.warn(`[Cache] Warning: Failed to read or parse ${CACHE_FILE}. Starting with a fresh cache. Error: ${error.message}`); + } + } +} + +async function saveCache() { + try { + const dataToSave = Object.fromEntries(albumArtCache); + await fs.writeFile(CACHE_FILE, JSON.stringify(dataToSave, null, 2)); + // This is a useful one-time log, so we keep it. + console.log(`[Cache] Successfully saved cache with ${albumArtCache.size} items.`); + } catch (error) { + console.error('[Cache] Failed to save cache:', error); + } +} + +/** + * Uploads an image to Catbox.moe by manually constructing the multipart/form-data request. + * This avoids using the 'form-data' library which was causing conflicts. + * @param {string} imageUrl The local URL of the image to upload. + * @returns {Promise} The public URL of the uploaded image, or null on failure. + */ +async function uploadImageToHost(imageUrl) { + try { + const imageResponse = await axios.get(imageUrl, { + responseType: 'arraybuffer', + }); + const imageBuffer = Buffer.from(imageResponse.data); + + if (imageBuffer.length === 0) { + console.error('[Album Art] Downloaded image is empty. Aborting upload.'); + return null; + } + + const contentType = imageResponse.headers['content-type'] || 'image/jpeg'; + const filename = 'albumart.jpg'; + + const boundary = `----WebKitFormBoundary${Math.random().toString(16).slice(2)}`; + const bodyParts = [ + `--${boundary}`, 'Content-Disposition: form-data; name="reqtype"', '', 'fileupload', + `--${boundary}`, `Content-Disposition: form-data; name="fileToUpload"; filename="${filename}"`, `Content-Type: ${contentType}`, '', '', + ]; + const bodyPrefix = Buffer.from(bodyParts.join('\r\n')); + const bodySuffix = Buffer.from(`\r\n--${boundary}--`); + const requestBody = Buffer.concat([bodyPrefix, imageBuffer, bodySuffix]); + + const uploadResponse = await axios.post('https://catbox.moe/user/api.php', requestBody, { + headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': requestBody.length, }, + validateStatus: () => true, + }); + + if (uploadResponse.status === 200 && uploadResponse.data.startsWith('http')) { + console.log(`[Album Art] Uploaded to: ${uploadResponse.data}`); + return uploadResponse.data; + } else { + console.error('[Album Art] Upload failed. Catbox.moe responded with:'); + console.error(` - Status: ${uploadResponse.status}, Data: ${uploadResponse.data}`); + return null; + } + } catch (error) { + console.error(`[Album Art] A network error occurred during upload: ${error.message}`); + return null; + } +} + async function getJellyfinSession() { try { const response = await axios.get(`${config.jellyfinServerUrl}/Sessions`, { @@ -28,18 +120,44 @@ async function getJellyfinSession() { } } +async function getAlbumArtUrl(albumId, trackId) { + const artId = albumId || trackId; + if (!artId) return 'jellyfin_logo'; + + if (albumArtCache.has(artId)) { + return albumArtCache.get(artId); + } + + // This log is important because it only happens once per new album/single. + console.log(`[Album Art] New item detected (ID: ${artId}). Proceeding to upload...`); + const localArtUrl = `${config.jellyfinServerUrl}/Items/${artId}/Images/Primary`; + const publicUrl = await uploadImageToHost(localArtUrl); + + if (publicUrl) { + albumArtCache.set(artId, publicUrl); + await saveCache(); + return publicUrl; + } + + return 'jellyfin_logo'; +} + async function updatePresence() { const session = await getJellyfinSession(); if (session && session.NowPlayingItem) { const track = session.NowPlayingItem; const trackId = track.Id; + const albumId = track.AlbumId; if (trackId !== lastTrackId) { lastTrackId = trackId; console.log(`[Discord] Now listening to: ${track.Artists.join(', ')} - ${track.Name}`); } + // pass trackid for singles + const largeImageUrl = await getAlbumArtUrl(albumId, trackId); + /*const presence = new Presence() .setDetails(`${track.Name}`) .setState(`by ${track.Artists.join(', ')}`) @@ -55,7 +173,7 @@ async function updatePresence() { client.user?.setActivity({ details: `${track.Name}`, state: `by ${track.Artists.join(', ')}`, - largeImageKey: `${config.jellyfinServerUrl}/Items/${track.AlbumId}/Images/Primary`, + largeImageKey: largeImageUrl, largeImageText: `on ${track.Album}`, smallImageKey: 'jellyfin_logo', smallImageText: 'Jellyfin', @@ -82,7 +200,7 @@ client.on('ready', () => { console.log(`[Discord] RPC connected for user ${client.user.username}`); console.log('[Jellyfin] Monitoring for listening activity...'); updatePresence(); - setInterval(updatePresence, 15 * 1000); + setInterval(updatePresence, 2 * 1000); }); client.on('disconnected', () => { @@ -91,6 +209,7 @@ client.on('disconnected', () => { // --- NEW: Connection logic with a 15-second timeout --- async function connectToDiscord() { + await loadCache(); // --- MODIFIED: Load cache before starting --- console.log('[Discord] Connecting to RPC...'); try { // Create a timeout promise diff --git a/package-lock.json b/package-lock.json index dc40ea0..24da555 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "dependencies": { "@xhayper/discord-rpc": "^1.1.2", - "axios": "^1.7.2" + "axios": "^1.7.2", + "form-data": "^4.0.4" } }, "node_modules/@discordjs/collection": { diff --git a/package.json b/package.json index 3aab60f..59d99fd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "license": "MIT", "dependencies": { "@xhayper/discord-rpc": "^1.1.2", - "axios": "^1.7.2" + "axios": "^1.7.2", + "form-data": "^4.0.4" } -} \ No newline at end of file +}