vd-plugins/multi-scrobbler/index.js
2025-10-04 14:25:48 +00:00

1 line
37 KiB
JavaScript

(function(B,f,r,$,tt,D,W,S,v,J,Le,Q){"use strict";const l={DEFAULT_APP_NAME:"Music",DEFAULT_TIME_INTERVAL:5,APPLICATION_ID:"1054951789318909972",MIN_UPDATE_INTERVAL:3,MAX_RETRY_ATTEMPTS:3,RETRY_DELAY:5e3,SERVICES:{lastfm:{name:"Last.fm",baseUrl:"http://ws.audioscrobbler.com/2.0",requiresApiKey:!0,requiresToken:!1},librefm:{name:"Libre.fm",baseUrl:"http://libre.fm/api",requiresApiKey:!0,requiresToken:!1},listenbrainz:{name:"ListenBrainz",baseUrl:"http://api.listenbrainz.org/1",requiresApiKey:!1,requiresToken:!0}},DEFAULT_COVER_HASHES:["2a96cbd8b46e442fc41c2b86b821562f","c6f59c1e5e7240a4c0d427abd71f3dbb"],DEFAULT_SETTINGS:{username:"",apiKey:"",appName:"Music",timeInterval:5,showTimestamp:!0,listeningTo:!0,ignoreSpotify:!0,verboseLogging:!1,service:"lastfm",librefmUsername:"",librefmApiKey:"",listenbrainzUsername:"",listenbrainzToken:""},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"}};function V(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function ue(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}function F(e,t,n){return t&&ue(e.prototype,t),n&&ue(e,n),e}$.findByProps("SET_ACTIVITY");const Ce=$.findByProps("getAssetIds"),Pe=$.findByStoreName("SelfPresenceStore"),ge=$.findByStoreName("UserStore"),fe=function(...e){return console.log("[Activity]",...e)},Ne=function(...e){return console.error("[Activity] Error:",...e)},x=function(...e){try{console.log("[Activity] Verbose:",...e)}catch{console.log("[Activity] Verbose:",...e)}};function Z(){return fe("Clearing Discord activity"),de(null)}function de(e){h.pluginStopped&&(x("Plugin is stopped, clearing activity"),Ee(),e=null),h.lastActivity=e,x("Dispatching activity update:",e?"Setting activity":"Clearing activity"),r.FluxDispatcher.dispatch({type:"LOCAL_ACTIVITY_UPDATE",activity:e,pid:2312,socketId:"Multi-Service-Scrobbler@Vendetta"}),fe(e?`Activity set: ${e.details} - ${e.state}`:"Activity cleared")}async function $e(e,t=l.APPLICATION_ID){if(!e?.length)return x("No assets to fetch"),[];try{x(`Fetching ${e.length} asset(s):`,e);const n=await Ce.fetchAssetIds(t,e);return x(`Successfully fetched ${n.length} asset(s)`),n}catch(n){return Ne("Failed to fetch assets:",n),[]}}const b={};b.componentMountErrors=[],b.componentMountCount=0,b.settingsLoadAttempts=0,b.serviceErrors={lastfm:[],librefm:[],listenbrainz:[]},b.apiCallCount=0,b.connectionAttempts=0,b.lastCredentialValidation={lastfm:!1,librefm:!1,listenbrainz:!1};const q=function(...e){return console.log("[Debug]",...e)},pe=function(...e){return console.error("[Debug] Error:",...e)};function G(e,t){b[e]=t,e==="lastError"&&t?pe("Error recorded:",t.message):e==="lastTrack"&&t?q("Track updated:",`${t.artist} - ${t.name}`):e==="currentService"&&t&&q("Service changed to:",t)}function be(){b.apiCallCount=(b.apiCallCount||0)+1,q(`API call count: ${b.apiCallCount}`)}function j(e,t){b.serviceErrors=b.serviceErrors||{lastfm:[],librefm:[],listenbrainz:[]},b.serviceErrors[e]=b.serviceErrors[e]||[],b.serviceErrors[e].push(`${new Date().toISOString()}: ${t}`),b.serviceErrors[e].length>10&&(b.serviceErrors[e]=b.serviceErrors[e].slice(-10)),pe(`${e} error:`,t)}function he(){b.lastSuccessfulUpdate=new Date().toISOString(),q("Successful update recorded at:",b.lastSuccessfulUpdate)}function Y(e){return Y=Object.setPrototypeOf?Object.getPrototypeOf:function(n){return n.__proto__||Object.getPrototypeOf(n)},Y(e)}function ye(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(ye=function(){return!!e})()}function Ue(e){if(e===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function _e(e){"@swc/helpers - typeof";return e&&typeof Symbol<"u"&&e.constructor===Symbol?"symbol":typeof e}function De(e,t){return t&&(_e(t)==="object"||typeof t=="function")?t:Ue(e)}function ee(e,t,n){return t=Y(t),De(e,ye()?Reflect.construct(t,n||[],Y(e).constructor):t.apply(e,n))}function te(e,t){return te=Object.setPrototypeOf||function(i,a){return i.__proto__=a,i},te(e,t)}function ne(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&te(e,t)}let re=function(){function e(){V(this,e)}return F(e,[{key:"log",value:function(...t){console.log(`[${this.getServiceName()}]`,...t)}},{key:"logError",value:function(...t){console.error(`[${this.getServiceName()}] Error:`,...t)}},{key:"logVerbose",value:function(...t){console.log(`[${this.getServiceName()}] Verbose:`,...t)}},{key:"handleError",value:async function(t){this.lastError=t.error||0;const n=this.getErrorMessage(t);throw this.logError(n),j(this.getServiceName().toLowerCase(),n),new Error(`${this.getServiceName()} API Error: ${n}`)}},{key:"getErrorMessage",value:function(t){return t.error&&l.API_ERROR_CODES[t.error]?l.API_ERROR_CODES[t.error]:t.message||t.toString()||"Unknown error"}},{key:"makeRequest",value:async function(t,n={}){try{this.logVerbose(`Making request to: ${t}`),be();const i=await fetch(t,{...n,headers:{"User-Agent":"Vendetta Multi-Service Scrobbler/3.0.0",...n.headers}});if(!i.ok){const s=new Error(`HTTP ${i.status}: ${i.statusText}`);throw j(this.getServiceName().toLowerCase(),s.message),s}const a=await i.json();return a.error&&await this.handleError(a),this.retryCount=0,a}catch(i){if(this.retryCount++,this.retryCount>l.MAX_RETRY_ATTEMPTS)throw this.retryCount=0,j(this.getServiceName().toLowerCase(),`Max retries exceeded: ${i.message}`),i;return this.logVerbose(`Request failed, retrying (${this.retryCount}/${l.MAX_RETRY_ATTEMPTS})`),await new Promise(function(a){return setTimeout(a,l.RETRY_DELAY)}),this.makeRequest(t,n)}}},{key:"isDefaultCover",value:function(t){return t?l.DEFAULT_COVER_HASHES.some(function(n){return t.includes(n)}):!0}},{key:"processAlbumArt",value:function(t){return!t||this.isDefaultCover(t)?null:t}},{key:"getLastError",value:function(){return this.lastError}},{key:"resetRetryCount",value:function(){this.retryCount=0}}]),e}(),me=function(e){ne(t,e);function t(){return V(this,t),ee(this,t,arguments)}return F(t,[{key:"getServiceName",value:function(){return"Last.fm"}},{key:"logVerbose",value:function(...n){o.verboseLogging&&console.log(`[${this.getServiceName()}] Verbose:`,...n)}},{key:"validateCredentials",value:async function(){try{if(!o.username||!o.apiKey)throw new Error("Username or API key not set");const n=new URLSearchParams({method:"user.getinfo",user:o.username,api_key:o.apiKey,format:"json"}),i=`${l.SERVICES.lastfm.baseUrl}?${n}`;return await this.makeRequest(i),this.log("Credentials validation successful"),!0}catch(n){return this.logError("Credentials validation failed:",n),!1}}},{key:"fetchLatestScrobble",value:async function(){try{if(!o.username||!o.apiKey)throw new Error("Username or API key not set");this.logVerbose("Fetching latest scrobble for user:",o.username);const n=new URLSearchParams({method:"user.getrecenttracks",user:o.username,api_key:o.apiKey,limit:"1",extended:"1",format:"json"}),i=`${l.SERVICES.lastfm.baseUrl}?${n}`,a=(await this.makeRequest(i))?.recenttracks?.track?.[0];if(!a)throw new Error("No tracks found");this.logVerbose("Raw track data:",a);const s=!!a["@attr"]?.nowplaying,c=a.date?.uts?parseInt(a.date.uts):Math.floor(Date.now()/1e3);let g=null,d=null;if(s)try{const L=new URLSearchParams({method:"track.getInfo",track:a.name,artist:a.artist.name,api_key:o.apiKey,format:"json"}),w=`${l.SERVICES.lastfm.baseUrl}?${L}`,T=await this.makeRequest(w);T?.track?.duration&&(g=parseInt(T.track.duration),g>0&&(g=Math.floor(g/1e3),d=c+g))}catch(L){this.logVerbose("Failed to fetch track duration:",L)}const y=this.processAlbumArt(a.image?.find(function(L){return L.size==="large"})?.["#text"]),k={name:a.name,artist:a.artist.name,album:a.album["#text"],albumArt:y,url:a.url,date:a.date?.["#text"]??"now",nowPlaying:s,loved:a.loved==="1",from:c,to:d,duration:g};return this.logVerbose("Processed track:",k),this.log(`${s?"Now playing":"Last played"}:`,`${k.artist} - ${k.name}`),k}catch(n){throw this.logError("Failed to fetch latest scrobble:",n),n}}}]),t}(re),Ve=function(e){ne(t,e);function t(){return V(this,t),ee(this,t,arguments)}return F(t,[{key:"getServiceName",value:function(){return"Libre.fm"}},{key:"logVerbose",value:function(...n){o.verboseLogging&&console.log(`[${this.getServiceName()}] Verbose:`,...n)}},{key:"validateCredentials",value:async function(){try{const n=o.librefmUsername||o.username,i=o.librefmApiKey||o.apiKey;if(!n||!i)throw new Error("Username or API key not set for Libre.fm");const a=new URLSearchParams({method:"user.getinfo",user:n,api_key:i,format:"json"}),s=`${l.SERVICES.librefm.baseUrl}?${a}`;return await this.makeRequest(s),this.log("Credentials validation successful"),!0}catch(n){return this.logError("Credentials validation failed:",n),!1}}},{key:"fetchLatestScrobble",value:async function(){try{const n=o.librefmUsername||o.username,i=o.librefmApiKey||o.apiKey;if(!n||!i)throw new Error("Username or API key not set for Libre.fm");this.logVerbose("Fetching latest scrobble for user:",n);const a=new URLSearchParams({method:"user.getrecenttracks",user:n,api_key:i,limit:"1",extended:"1",format:"json"}),s=`${l.SERVICES.librefm.baseUrl}?${a}`,c=(await this.makeRequest(s))?.recenttracks?.track?.[0];if(!c)throw new Error("No tracks found");this.logVerbose("Raw track data:",c);const g=!!c["@attr"]?.nowplaying,d=c.date?.uts?parseInt(c.date.uts):Math.floor(Date.now()/1e3);let y=null,k=null;if(g)try{const T=new URLSearchParams({method:"track.getInfo",track:c.name,artist:c.artist.name,api_key:i,format:"json"}),N=`${l.SERVICES.librefm.baseUrl}?${T}`,ke=await this.makeRequest(N);ke?.track?.duration&&(y=parseInt(ke.track.duration),y>0&&(y=Math.floor(y/1e3),k=d+y))}catch(T){this.logVerbose("Failed to fetch track duration:",T)}const L=this.processAlbumArt(c.image?.find(function(T){return T.size==="large"})?.["#text"]),w={name:c.name,artist:c.artist.name,album:c.album["#text"],albumArt:L,url:c.url,date:c.date?.["#text"]??"now",nowPlaying:g,loved:c.loved==="1",from:d,to:k,duration:y};return this.logVerbose("Processed track:",w),this.log(`${g?"Now playing":"Last played"}:`,`${w.artist} - ${w.name}`),w}catch(n){throw this.logError("Failed to fetch latest scrobble:",n),n}}}]),t}(re),Fe=function(e){ne(t,e);function t(){return V(this,t),ee(this,t,arguments)}return F(t,[{key:"getServiceName",value:function(){return"ListenBrainz"}},{key:"logVerbose",value:function(...n){o.verboseLogging&&console.log(`[${this.getServiceName()}] Verbose:`,...n)}},{key:"validateCredentials",value:async function(){try{const n=o.listenbrainzUsername||o.username,i=o.listenbrainzToken;if(!n)throw new Error("Username not set for ListenBrainz");const a=`${l.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(n)}/listens?count=1`,s={};return i&&(s.Authorization=`Token ${i}`),await this.makeRequest(a,{headers:s}),this.log("Credentials validation successful"),!0}catch(n){return this.logError("Credentials validation failed:",n),!1}}},{key:"fetchLatestScrobble",value:async function(){try{const n=o.listenbrainzUsername||o.username,i=o.listenbrainzToken;if(!n)throw new Error("Username not set for ListenBrainz");this.logVerbose("Fetching latest scrobble for user:",n);let a=null;try{const w=`${l.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(n)}/playing-now`,T={};i&&(T.Authorization=`Token ${i}`);const N=await this.makeRequest(w,{headers:T});N.listens&&N.listens.length>0&&(a=N.listens[0],a.playing_now=!0)}catch(w){this.logVerbose("No currently playing track or failed to fetch:",w)}let s;if(a)s=a,this.logVerbose("Using currently playing track");else{const w=`${l.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(n)}/listens?count=1`,T={};i&&(T.Authorization=`Token ${i}`);const N=await this.makeRequest(w,{headers:T});if(!N.listens||N.listens.length===0)throw new Error("No listens found");s=N.listens[0],this.logVerbose("Using latest completed listen")}this.logVerbose("Raw listen data:",s);const c=!!s.playing_now,g=s.listened_at||Math.floor(Date.now()/1e3);let d=null,y=null;s.track_metadata.additional_info?.duration_ms&&(d=Math.floor(s.track_metadata.additional_info.duration_ms/1e3),c&&d>0&&(y=g+d));const k=null;if(s.track_metadata.additional_info?.release_mbid)try{}catch(w){this.logVerbose("Failed to fetch album art:",w)}const L={name:s.track_metadata.track_name,artist:s.track_metadata.artist_name,album:s.track_metadata.release_name||"",albumArt:k,url:s.track_metadata.additional_info?.origin_url||`https://listenbrainz.org/user/${n}`,date:c?"now":new Date(g*1e3).toISOString(),nowPlaying:c,loved:!1,from:g,to:y,duration:d};return this.logVerbose("Processed track:",L),this.log(`${c?"Now playing":"Last played"}:`,`${L.artist} - ${L.name}`),L}catch(n){throw this.logError("Failed to fetch latest listen:",n),n}}},{key:"getErrorMessage",value:function(n){return n.error?n.error:n.message?n.message:n.toString()||"Unknown error"}}]),t}(re);const I=function(){function e(){V(this,e),this.serviceInstances=new Map}return F(e,[{key:"getService",value:function(t){this.serviceInstances||(this.serviceInstances=new Map);const n=t||o.service||"lastfm";return this.serviceInstances.has(n)||this.serviceInstances.set(n,this.createService(n)),this.serviceInstances.get(n)}},{key:"getCurrentService",value:function(){return this.getService(o.service)}},{key:"createService",value:function(t){switch(t){case"lastfm":return new me;case"librefm":return new Ve;case"listenbrainz":return new Fe;default:return console.warn(`[ServiceFactory] Unknown service type: ${t}, falling back to Last.fm`),new me}}},{key:"clearCache",value:function(){this.serviceInstances?this.serviceInstances.clear():this.serviceInstances=new Map}},{key:"validateCurrentService",value:function(){return this.getCurrentService().validateCredentials()}},{key:"testService",value:async function(t){try{return await this.getService(t).validateCredentials()}catch(n){return console.error(`[ServiceFactory] Failed to test ${t}:`,n),!1}}},{key:"getSupportedServices",value:function(){return["lastfm","librefm","listenbrainz"]}},{key:"getServiceDisplayName",value:function(t){return this.getService(t).getServiceName()}}],[{key:"getInstance",value:function(){return e.instance||(e.instance=new e),e.instance}}]),e}().getInstance();function ve(e){if(!e||e<0)return"0:00";const t=Math.floor(e/3600),n=Math.floor(e%3600/60),i=Math.floor(e%60);return t>0?`${t}:${n.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${n}:${i.toString().padStart(2,"0")}`}function Se(){return Math.floor(Date.now()/1e3)}var ze=function(e){return e[e.PLAYING=0]="PLAYING",e[e.STREAMING=1]="STREAMING",e[e.LISTENING=2]="LISTENING",e[e.COMPETING=5]="COMPETING",e}(ze||{});const A=function(...e){return console.log("[Scrobble Plugin]",...e)},z=function(...e){return console.error("[Scrobble Plugin] Error:",...e)},C=function(...e){return o.verboseLogging&&console.log("[Scrobble Plugin] Verbose:",...e)},ie=function(){function e(){V(this,e)}return F(e,[{key:"updateActivity",value:async function(){if(h.pluginStopped){C("Plugin is stopped, skipping update"),this.stopUpdates();return}const t=I.getCurrentService().getServiceName();C(`Fetching latest track from ${t}...`);let n=!1;try{if(o.ignoreSpotify){if(Pe.findActivity(function(c){return c.sync_id})){C("Spotify is currently playing, clearing activity"),G("isSpotifyIgnored",!0),Z();return}G("isSpotifyIgnored",!1)}be();const i=await I.getCurrentService().fetchLatestScrobble();if(G("lastTrack",i),!i.nowPlaying){C("Track is not currently playing"),Z();return}if(h.lastTrackUrl===i.url){C("Track hasn't changed"),he(),this.consecutiveFailures=0;return}n=!0,A(`\u{1F3B5} Track changed: ${i.artist} - ${i.name}`);let a;if(i.nowPlaying&&o.showTimestamp&&i.from){const c=Se();let g=i.from;if(g<c-3600){if(i.duration&&i.duration>0){const d=Math.min(i.duration*.1,30);g=c-d}else g=c;C("had to estimate start time")}a={start:g*1e3},i.to&&(a.end=i.to*1e3)}C(`\u{1F3AF} Preparing RPC update for: ${i.artist} - ${i.name}`);const s={name:o.appName||l.DEFAULT_APP_NAME,flags:0,type:o.listeningTo?2:0,details:i.name,state:`by ${i.artist}`,status_display_type:1,application_id:l.APPLICATION_ID};if(s.name.includes("{{")){const c={artist:i.artist,name:i.name,album:i.album,service:t};for(const[g,d]of Object.entries(c))s.name=s.name.replace(new RegExp(`{{${g}}}`,"g"),d||"")}if(a&&(s.timestamps=a,C("Timestamps set:",{start:new Date(a.start).toISOString(),end:a.end?new Date(a.end).toISOString():"none",duration:i.duration?ve(i.duration):"unknown"})),i.album||i.albumArt){const c=i.albumArt?[i.albumArt]:[],g=await $e(c);if(g[0]){const d=i.album?`on ${i.album}`:`${i.artist} - ${i.name}`,y=o.showTimestamp&&i.duration?` \u2022 ${ve(i.duration)}`:"";s.assets={large_image:g[0],large_text:d+y},C("Album art set:",g[0])}else i.album&&(s.assets={large_text:`on ${i.album}`})}C("Setting Discord activity:",s),G("lastActivity",s),await de(s),h.lastTrackUrl=i.url,this.currentActivity=s,h.lastActivity=s,this.consecutiveFailures=0,this.lastUpdateTime=Se(),he(),A(`\u2705 RPC updated successfully: ${i.artist} - ${i.name}`)}catch(i){z("Update failed:",i),j(o.service,i.message),this.handleError(i)}}},{key:"handleError",value:function(t){this.consecutiveFailures++,G("lastError",t),z(`Failure ${this.consecutiveFailures}/${l.MAX_RETRY_ATTEMPTS}:`,t.message),this.consecutiveFailures>=l.MAX_RETRY_ATTEMPTS&&(z("Max retry attempts reached, initiating reconnection..."),this.startReconnection())}},{key:"startReconnection",value:function(){var t=this;this.isReconnecting||(this.isReconnecting=!0,this.stopUpdates(),A("Starting reconnection process..."),this.reconnectTimer=setInterval(function(){A("Attempting to reconnect..."),t.initialize().then(function(){A("Reconnection successful!"),t.stopReconnection()}).catch(function(n){z("Reconnection attempt failed:",n.message)})},l.RETRY_DELAY))}},{key:"stopReconnection",value:function(){try{this.reconnectTimer&&(clearInterval(this.reconnectTimer),this.reconnectTimer=void 0),this.isReconnecting=!1,this.consecutiveFailures=0}catch(t){console.error("[Scrobble Plugin] Reconnection cleanup error:",t)}}},{key:"stopUpdates",value:function(){try{this.updateTimer&&(clearInterval(this.updateTimer),this.updateTimer=void 0)}catch(t){console.error("[Scrobble Plugin] Timer cleanup error:",t)}}},{key:"initialize",value:async function(){var t=this;if(h.pluginStopped)throw new Error("Plugin is stopped");const n=I.getCurrentService().getServiceName();A(`Initializing with ${n}...`);try{if(!await I.validateCurrentService())throw new Error(`Invalid credentials for ${n}`);A(`${n} credentials validated successfully`)}catch(a){throw z(`Failed to validate ${n} credentials:`,a),a}this.stopUpdates(),A("\u{1F3B5} checking for already playing songs..."),await this.updateActivity();const i=Math.max((Number(o.timeInterval)||l.DEFAULT_SETTINGS.timeInterval)*1e3,l.MIN_UPDATE_INTERVAL*1e3);this.updateTimer=setInterval(function(){return t.updateActivity()},i),A(`Update timer started with interval: ${i}ms (${i/1e3}s)`)}},{key:"stop",value:function(){if(!h.pluginStopped){A("Stopping plugin..."),h.pluginStopped=!0;try{this.stopUpdates(),this.stopReconnection(),Z(),A("Plugin stopped successfully")}catch(t){console.error("[Scrobble Plugin] Stop error:",t)}}}},{key:"switchService",value:async function(t){if(h.pluginStopped)return;A(`Switching to ${t}...`);const n=!h.pluginStopped;this.stop();try{I.clearCache(),h.lastTrackUrl=void 0,this.currentActivity=void 0,this.lastUpdateTime=0,n&&(h.pluginStopped=!1,await this.initialize())}catch(i){z("Failed to switch service:",i)}}},{key:"getStatus",value:function(){const t=I.getCurrentService().getServiceName();return{running:!h.pluginStopped,service:t,consecutiveFailures:this.consecutiveFailures,isReconnecting:this.isReconnecting,lastTrackUrl:h.lastTrackUrl,updateInterval:this.updateTimer?"Active":"Inactive"}}}],[{key:"getInstance",value:function(){return e.instance||(e.instance=new e),e.instance}}]),e}().getInstance(),Me=function(){return ie.initialize()},Ee=function(){return ie.stop()},Oe=function(e){return ie.switchService(e)},{ScrollView:M}=$.findByProps("ScrollView"),{TableRowGroup:E,TableSwitchRow:O,Stack:P,TableRow:u}=$.findByProps("TableSwitchRow","TableRowGroup","Stack","TableRow"),{TextInput:U}=$.findByProps("TextInput");f.plugin.storage.username??="",f.plugin.storage.apiKey??="",f.plugin.storage.appName??="Music",f.plugin.storage.timeInterval??=5,f.plugin.storage.showTimestamp??=!0,f.plugin.storage.listeningTo??=!0,f.plugin.storage.ignoreSpotify??=!0,f.plugin.storage.verboseLogging??=!1,f.plugin.storage.service??="lastfm",f.plugin.storage.librefmUsername??="",f.plugin.storage.librefmApiKey??="",f.plugin.storage.listenbrainzUsername??="",f.plugin.storage.listenbrainzToken??="",f.plugin.storage.addToSidebar??=!0;const p=function(e,t=""){return f.plugin.storage[e]??t},R=function(e,t){return f.plugin.storage[e]=t};function Be(){D.useProxy(f.plugin.storage);const[,e]=r.React.useReducer(function(n){return n+1},0),t=async function(){S.showToast("Testing Last.fm connection...",v.getAssetIDByName("ClockIcon"));try{await I.testService("lastfm")?S.showToast("\u2705 Last.fm connection successful!",v.getAssetIDByName("CheckIcon")):S.showToast("\u274C Last.fm connection failed",v.getAssetIDByName("XIcon"))}catch{S.showToast("\u274C Connection error",v.getAssetIDByName("XIcon"))}};return r.React.createElement(M,{style:{flex:1},contentContainerStyle:{padding:10}},r.React.createElement(P,{spacing:8},r.React.createElement(E,{title:"Credentials"},r.React.createElement(P,{spacing:4},r.React.createElement(U,{placeholder:"Last.fm Username",value:p("username"),onChange:function(n){R("username",n),e()},isClearable:!0}),r.React.createElement(U,{placeholder:"Last.fm API Key",value:p("apiKey"),onChange:function(n){R("apiKey",n),e()},secureTextEntry:!0,isClearable:!0}))),r.React.createElement(E,{title:"Actions"},r.React.createElement(u,{label:"Test Connection",subLabel:"Verify your Last.fm credentials",trailing:r.React.createElement(u.Arrow,null),onPress:t}),r.React.createElement(u,{label:"Get API Key",subLabel:"Create a Last.fm API key at last.fm/api/account/create",trailing:r.React.createElement(u.Arrow,null),onPress:async function(){try{await W.Linking.openURL("https://www.last.fm/api/account/create")}catch(n){console.error("Failed to open Last.fm API URL:",n),S.showToast("Failed to open web browser. Please visit: https://www.last.fm/api/account/create",v.getAssetIDByName("XIcon"))}}}))),r.React.createElement(r.ReactNative.View,{style:{height:64}}))}function xe(){D.useProxy(f.plugin.storage);const[,e]=r.React.useReducer(function(n){return n+1},0),t=async function(){S.showToast("Testing Libre.fm connection...",v.getAssetIDByName("ClockIcon"));try{await I.testService("librefm")?S.showToast("\u2705 Libre.fm connection successful!",v.getAssetIDByName("CheckIcon")):S.showToast("\u274C Libre.fm connection failed",v.getAssetIDByName("XIcon"))}catch{S.showToast("\u274C Connection error",v.getAssetIDByName("XIcon"))}};return r.React.createElement(M,{style:{flex:1},contentContainerStyle:{padding:10}},r.React.createElement(P,{spacing:8},r.React.createElement(E,{title:"Credentials"},r.React.createElement(P,{spacing:4},r.React.createElement(U,{placeholder:"Libre.fm Username (optional, uses Last.fm if empty)",value:p("librefmUsername"),onChange:function(n){R("librefmUsername",n),e()},isClearable:!0}),r.React.createElement(U,{placeholder:"Libre.fm API Key (optional, uses Last.fm if empty)",value:p("librefmApiKey"),onChange:function(n){R("librefmApiKey",n),e()},secureTextEntry:!0,isClearable:!0}))),r.React.createElement(E,{title:"Actions"},r.React.createElement(u,{label:"Test Connection",subLabel:"Verify your Libre.fm credentials",trailing:r.React.createElement(u.Arrow,null),onPress:t}),r.React.createElement(u,{label:"Get API Key",subLabel:"Create a Last.fm API key (compatible with Libre.fm)",trailing:r.React.createElement(u.Arrow,null),onPress:async function(){try{await W.Linking.openURL("https://www.last.fm/api/account/create")}catch(n){console.error("Failed to open Last.fm API URL:",n),S.showToast("Failed to open web browser. Please visit: https://www.last.fm/api/account/create",v.getAssetIDByName("XIcon"))}}}))),r.React.createElement(r.ReactNative.View,{style:{height:64}}))}function Ge(){D.useProxy(f.plugin.storage);const[,e]=r.React.useReducer(function(n){return n+1},0),t=async function(){S.showToast("Testing ListenBrainz connection...",v.getAssetIDByName("ClockIcon"));try{await I.testService("listenbrainz")?S.showToast("\u2705 ListenBrainz connection successful!",v.getAssetIDByName("CheckIcon")):S.showToast("\u274C ListenBrainz connection failed",v.getAssetIDByName("XIcon"))}catch{S.showToast("\u274C Connection error",v.getAssetIDByName("XIcon"))}};return r.React.createElement(M,{style:{flex:1},contentContainerStyle:{padding:10}},r.React.createElement(P,{spacing:8},r.React.createElement(E,{title:"Credentials"},r.React.createElement(P,{spacing:4},r.React.createElement(U,{placeholder:"ListenBrainz Username (optional, uses Last.fm if empty)",value:p("listenbrainzUsername"),onChange:function(n){R("listenbrainzUsername",n),e()},isClearable:!0}),r.React.createElement(U,{placeholder:"ListenBrainz Token (optional, for private profiles)",value:p("listenbrainzToken"),onChange:function(n){R("listenbrainzToken",n),e()},secureTextEntry:!0,isClearable:!0}))),r.React.createElement(E,{title:"Actions"},r.React.createElement(u,{label:"Test Connection",subLabel:"Verify your ListenBrainz credentials",trailing:r.React.createElement(u.Arrow,null),onPress:t}),r.React.createElement(u,{label:"Get User Token",subLabel:"Get your ListenBrainz user token at listenbrainz.org/settings/",trailing:r.React.createElement(u.Arrow,null),onPress:async function(){try{await W.Linking.openURL("https://listenbrainz.org/settings/")}catch(n){console.error("Failed to open ListenBrainz settings URL:",n),S.showToast("Failed to open web browser. Please visit: https://listenbrainz.org/settings/",v.getAssetIDByName("XIcon"))}}}))),r.React.createElement(r.ReactNative.View,{style:{height:64}}))}function Ke(){D.useProxy(f.plugin.storage);const[,e]=r.React.useReducer(function(t){return t+1},0);return r.React.createElement(M,{style:{flex:1},contentContainerStyle:{padding:10}},r.React.createElement(P,{spacing:8},r.React.createElement(E,{title:"Activity Display"},r.React.createElement(P,{spacing:4},r.React.createElement(U,{placeholder:`App Name (Default: ${l.DEFAULT_SETTINGS.appName})`,value:p("appName",l.DEFAULT_SETTINGS.appName),onChange:function(t){R("appName",t),e()},isClearable:!0}),r.React.createElement(U,{placeholder:`Update Interval (Default: ${l.DEFAULT_SETTINGS.timeInterval}s)`,value:String(p("timeInterval",l.DEFAULT_SETTINGS.timeInterval)),onChange:function(t){const n=Number(t);n>=l.MIN_UPDATE_INTERVAL&&(R("timeInterval",n),e())},keyboardType:"numeric",isClearable:!0}))),r.React.createElement(E,{title:"Activity Options"},r.React.createElement(O,{label:"Show as Listening",subLabel:"Display as 'Listening to' instead of 'Playing'",value:p("listeningTo",l.DEFAULT_SETTINGS.listeningTo),onValueChange:function(t){R("listeningTo",t),e()}}),r.React.createElement(O,{label:"Show Timestamp",subLabel:"Display track progress and duration",value:p("showTimestamp",l.DEFAULT_SETTINGS.showTimestamp),onValueChange:function(t){R("showTimestamp",t),e()}}),r.React.createElement(O,{label:"Ignore Spotify",subLabel:"Don't show activity when Spotify is playing",value:p("ignoreSpotify",l.DEFAULT_SETTINGS.ignoreSpotify),onValueChange:function(t){R("ignoreSpotify",t),e()}}))),r.React.createElement(r.ReactNative.View,{style:{height:64}}))}function qe(){D.useProxy(f.plugin.storage);const[,e]=r.React.useReducer(function(t){return t+1},0);return r.React.createElement(M,{style:{flex:1},contentContainerStyle:{padding:10}},r.React.createElement(P,{spacing:8},r.React.createElement(E,{title:"Logging Options"},r.React.createElement(O,{label:"Verbose Logging",subLabel:"Enable detailed console logging for debugging",value:p("verboseLogging",l.DEFAULT_SETTINGS.verboseLogging),onValueChange:function(t){R("verboseLogging",t),e()}})),r.React.createElement(E,{title:"Debug Information"},r.React.createElement(u,{label:"Console Logging",subLabel:"Logs are written to the browser/app console when verbose is enabled"}),r.React.createElement(u,{label:"Error Tracking",subLabel:"Connection errors and API failures are automatically logged"})),r.React.createElement(E,{title:"Log Information"},r.React.createElement(u,{label:"API Calls",subLabel:"All API requests are logged when verbose is enabled"}),r.React.createElement(u,{label:"Track Updates",subLabel:"Song changes and RPC updates are logged"}),r.React.createElement(u,{label:"Error Details",subLabel:"Connection errors and retries are logged"}))),r.React.createElement(r.ReactNative.View,{style:{height:64}}))}function Re(){D.useProxy(f.plugin.storage);const e=r.NavigationNative.useNavigation(),[,t]=r.React.useReducer(function(a){return a+1},0),n=p("service","lastfm"),i=function(a){switch(a){case"lastfm":return p("username")&&p("apiKey")?"\u2705 Configured":"\u274C Missing credentials";case"librefm":{const s=p("librefmUsername")||p("username"),c=p("librefmApiKey")||p("apiKey");return s&&c?"\u2705 Configured":"\u274C Missing credentials"}case"listenbrainz":return p("listenbrainzUsername")||p("username")?"\u2705 Configured":"\u274C Missing username";default:return"\u2753 Unknown"}};return r.React.createElement(M,{style:{flex:1},contentContainerStyle:{padding:10}},r.React.createElement(P,{spacing:8},r.React.createElement(E,{title:"Active Service"},r.React.createElement(u,{label:"Current Service",subLabel:`Using: ${I.getServiceDisplayName(n)}`}),["lastfm","librefm","listenbrainz"].map(function(a){return r.React.createElement(O,{key:a,label:I.getServiceDisplayName(a),subLabel:i(a),value:n===a,onValueChange:function(s){s&&a!==n&&(R("service",a),t())}})})),r.React.createElement(E,{title:"Service Configuration"},r.React.createElement(u,{label:"Last.fm Settings",subLabel:"Configure Last.fm credentials and options",trailing:r.React.createElement(u.Arrow,null),onPress:function(){return e.push("VendettaCustomPage",{title:"Last.fm Settings",render:Be})}}),r.React.createElement(u,{label:"Libre.fm Settings",subLabel:"Configure Libre.fm credentials and options",trailing:r.React.createElement(u.Arrow,null),onPress:function(){return e.push("VendettaCustomPage",{title:"Libre.fm Settings",render:xe})}}),r.React.createElement(u,{label:"ListenBrainz Settings",subLabel:"Configure ListenBrainz credentials and options",trailing:r.React.createElement(u.Arrow,null),onPress:function(){return e.push("VendettaCustomPage",{title:"ListenBrainz Settings",render:Ge})}})),r.React.createElement(E,{title:"Plugin Configuration"},r.React.createElement(u,{label:"Display Settings",subLabel:"Customize how music activity appears",trailing:r.React.createElement(u.Arrow,null),onPress:function(){return e.push("VendettaCustomPage",{title:"Display Settings",render:Ke})}}),r.React.createElement(u,{label:"Logging Settings",subLabel:"Configure logging and debugging options",trailing:r.React.createElement(u.Arrow,null),onPress:function(){return e.push("VendettaCustomPage",{title:"Logging Settings",render:qe})}}),r.React.createElement(O,{label:"Add to Sidebar",subLabel:"Show plugin in Discord settings",value:p("addToSidebar",!1),onValueChange:function(a){R("addToSidebar",a),t()}})),r.React.createElement(E,{title:"About"},r.React.createElement(u,{label:"Multi Scrobbler",subLabel:"Show off your music status from multiple services"}),r.React.createElement(u,{label:"Author",subLabel:"kmmiio99o"}),r.React.createElement(u,{label:"Version",subLabel:"1.0.0"}))),r.React.createElement(r.ReactNative.View,{style:{height:64}}))}const{FormSection:je,FormRow:ae}=Le.Forms,{TableRowIcon:Ye}=$.findByProps("TableRowIcon"),X=window.bunny,we=X?.metro?.findByPropsLazy("getRootNavigationRef"),se=X?.metro?.findByPropsLazy("SETTING_RENDERER_CONFIG"),Te=X?.metro?.findByNameLazy("SettingsOverviewScreen",!1);function Xe({tabs:e}){const t=r.NavigationNative.useNavigation();return r.React.createElement(ae,{label:e.title(),leading:r.React.createElement(ae.Icon,{source:e.icon}),trailing:r.React.createElement(r.React.Fragment,{},[e.trailing?e.trailing():null,r.React.createElement(ae.Arrow,{key:"arrow"})]),onPress:function(){const n=e.page;t.navigate("VendettaCustomPage",{title:e.title(),render:function(){return r.React.createElement(n)}})}})}function He(e,t){try{t.push(J.after("default",X?.metro?.findByNameLazy("UserSettingsOverviewWrapper",!1),function(n,i){const a=Q.findInReactTree(i.props.children,function(s){return s.type?.name==="UserSettingsOverview"});a&&t.push(J.after("render",a.type.prototype,function(s,c){const g=Q.findInReactTree(c.props.children,function(d){return d?.children?.[1]?.type===je})?.children;if(g){const d=g.findIndex(function(y){return["BILLING_SETTINGS","PREMIUM_SETTINGS"].includes(y?.props?.label)});g.splice(-~d||4,0,r.React.createElement(Xe,{key:e.key,tabs:e}))}}))},!0))}catch(n){f.logger.info("Panel UI patch failed graciously \u{1F494}",n)}}function We(e,t){if(!se||!Te||!we){console.warn("[LastFm] Missing required constants for tabs UI patch");return}const n={};n[e.key]={type:"pressable",title:e.title,icon:e.icon,IconComponent:e.icon&&function(){const s=typeof e.icon=="object"&&e.icon.uri!==void 0?e.icon.uri:e.icon;return r.React.createElement(Ye,{source:s})},usePredicate:e.predicate,useTrailing:e.trailing,onPress:function(){const s=we.getRootNavigationRef(),c=e.page;s.navigate("VendettaCustomPage",{title:e.title(),render:function(){return r.React.createElement(c)}})},withArrow:!0};let i=se.SETTING_RENDERER_CONFIG;Object.defineProperty(se,"SETTING_RENDERER_CONFIG",{enumerable:!0,configurable:!0,get:function(){return{...i,...n}},set:function(s){return i=s}});const a=Symbol("LastFm-pinToSettings");t.push(J.after("default",Te,function(s,c){if(!s[0][a]){s[0][a]=!0;const{sections:g}=Q.findInReactTree(c,function(y){return y.props?.sections}).props,d=g?.find(function(y){return["Bunny","Revenge","Kettu"].some(function(k){return y.label===k&&y.title===k})});d?.settings&&(d.settings=[...d.settings,e.key])}}))}function Je(e){const t=[];let n=!1;const i=e.predicate||function(){return!0};return e.predicate=function(){return n?!1:i()},He(e,t),We(e,t),t.push(function(){return n=!0}),function(){for(const a of t)a()}}function Ie(){if(!f.plugin.storage.addToSidebar)return console.log("[Multi Scrobbler] Sidebar disabled in settings"),function(){};console.log("[Multi Scrobbler] Patching sidebar using custom patchSettingsPin...");try{const e=Je({key:"LastFmScrobbler",icon:v.getAssetIDByName("MusicIcon"),title:function(){return"Multi Scrobbler"},predicate:function(){return f.plugin.storage.addToSidebar===!0},page:Re});return console.log("[Multi Scrobbler] Successfully patched sidebar"),e}catch(e){return console.error("[Multi Scrobbler] Failed to patch sidebar:",e),function(){}}}const h={pluginStopped:!1,lastActivity:void 0,updateInterval:void 0,lastTrackUrl:void 0},oe=[];let _;const Ae=l.DEFAULT_SETTINGS;for(const e of Object.keys(Ae))f.plugin.storage[e]=f.plugin.storage[e]??Ae[e];f.plugin.storage.addToSidebar??=!1;const o=new Proxy(f.plugin.storage,{get(e,t){return e[t]},set(e,t,n){return e[t]=n,!0}});let H=0;const ce=3,Qe=5e3,m=function(...e){return console.log("[Scrobble Plugin]",...e)},le=function(...e){return console.error("[Scrobble Plugin] Error:",...e)};async function K(){try{await Me(),H=0,m("Plugin initialized successfully")}catch(e){le("Initialization failed:",e),H++,H<ce?(m(`Retrying connection (${H}/${ce})...`),setTimeout(K,Qe)):le(`Failed to connect after ${ce} attempts`)}}async function Ze(){const e=I.getCurrentService().getServiceName(),t=o.service||"lastfm";let n=!1;switch(t){case"lastfm":n=!!(o.username&&o.apiKey);break;case"librefm":n=!!((o.librefmUsername||o.username)&&(o.librefmApiKey||o.apiKey));break;case"listenbrainz":n=!!(o.listenbrainzUsername||o.username);break}if(!n){le(`Missing credentials for ${e}. Please configure in plugin settings.`);return}if(m(`Starting with ${e}...`),ge.getCurrentUser())m("Discord is already connected, initializing immediately..."),await K();else{m("Waiting for Discord connection...");const i=function(){ge.getCurrentUser()&&(m("Discord connection established"),K(),r.FluxDispatcher.unsubscribe("CONNECTION_OPEN",i))};r.FluxDispatcher.subscribe("CONNECTION_OPEN",i)}}var et={onLoad(){m("Plugin loading..."),h.pluginStopped=!1;const e=I.getCurrentService().getServiceName();m(`Configuration: Service=${e}, Update Interval=${o.timeInterval}s, Verbose=${o.verboseLogging}`);try{_=Ie(),oe.push(function(){_&&(_(),_=void 0)})}catch(t){m("Sidebar setup failed:",t)}Ze()},onUnload(){m("Plugin unloading..."),h.pluginStopped=!0,oe.forEach(function(e){try{e()}catch{}}),oe.length=0,Ee(),m("Plugin unloaded")},async onSettingsUpdate(e){const t=o.service,n=e.service,i=o.addToSidebar,a=e.addToSidebar;if(Object.assign(o,e),m("Settings updated:",Object.keys(e)),i!==a)if(a)try{_=Ie(),m("Sidebar enabled")}catch(s){m("Failed to enable sidebar:",s)}else _&&(_(),_=void 0,m("Sidebar disabled"));t!==n&&n?(m(`Service changed from ${t} to ${n}`),await Oe(n)):h.pluginStopped||(m("Restarting with updated settings..."),await K())},onDiscordReconnect(){h.pluginStopped||(m("Discord reconnected, reinitializing plugin..."),K())},settings:Re};return B.currentSettings=o,B.default=et,B.pluginState=h,Object.defineProperty(B,"__esModule",{value:!0}),B})({},vendetta,vendetta.metro.common,vendetta.metro,window.React,vendetta.storage,vendetta.metro.common.ReactNative,vendetta.ui.toasts,vendetta.ui.assets,vendetta.patcher,vendetta.ui.components,vendetta.utils);