Skip to content

Commit d2a3faa

Browse files
committed
feat: Add slack scrobbler
1 parent 5853523 commit d2a3faa

File tree

7 files changed

+435
-0
lines changed

7 files changed

+435
-0
lines changed

Diff for: package.json

+3
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@
256256
"@skyra/jaro-winkler": "1.1.1",
257257
"@xhayper/discord-rpc": "1.2.1",
258258
"async-mutex": "0.5.0",
259+
"axios": "^1.8.4",
259260
"bgutils-js": "3.2.0",
260261
"butterchurn": "3.0.0-beta.4",
261262
"butterchurn-presets": "3.0.0-beta.4",
@@ -273,6 +274,7 @@
273274
"fast-average-color": "9.5.0",
274275
"fast-equals": "5.2.2",
275276
"filenamify": "6.0.0",
277+
"form-data": "^4.0.2",
276278
"hanja": "1.1.4",
277279
"happy-dom": "17.4.4",
278280
"hono": "4.7.6",
@@ -311,6 +313,7 @@
311313
"@stylistic/eslint-plugin-js": "4.2.0",
312314
"@total-typescript/ts-reset": "0.6.1",
313315
"@types/electron-localshortcut": "3.1.3",
316+
"@types/form-data": "^2.5.2",
314317
"@types/howler": "2.2.12",
315318
"@types/html-to-text": "9.0.4",
316319
"@types/semver": "7.7.0",

Diff for: pnpm-lock.yaml

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/plugins/scrobbler/index.ts

+28
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ export interface ScrobblerPluginConfig {
7171
*/
7272
apiRoot: string;
7373
};
74+
slack: {
75+
/**
76+
* Enable Slack scrobbling
77+
*
78+
* @default false
79+
*/
80+
enabled: boolean;
81+
/**
82+
* Slack OAuth token
83+
*/
84+
token: string | undefined;
85+
/**
86+
* Slack cookie token (d cookie value)
87+
*/
88+
cookieToken: string | undefined;
89+
/**
90+
* Name to use for the custom emoji in Slack
91+
*
92+
* @default 'my-album-art'
93+
*/
94+
emojiName: string;
95+
};
7496
};
7597
}
7698

@@ -92,6 +114,12 @@ export const defaultConfig: ScrobblerPluginConfig = {
92114
token: undefined,
93115
apiRoot: 'https://api.listenbrainz.org/1/',
94116
},
117+
slack: {
118+
enabled: false,
119+
token: undefined,
120+
cookieToken: undefined,
121+
emojiName: 'my-album-art',
122+
},
95123
},
96124
};
97125

Diff for: src/plugins/scrobbler/main.ts

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createBackend } from '@/utils';
99

1010
import { LastFmScrobbler } from './services/lastfm';
1111
import { ListenbrainzScrobbler } from './services/listenbrainz';
12+
import { SlackScrobbler } from './services/slack';
1213

1314
import type { ScrobblerPluginConfig } from './index';
1415
import type { ScrobblerBase } from './services/base';
@@ -51,6 +52,12 @@ export const backend = createBackend<
5152
} else {
5253
this.enabledScrobblers.delete('listenbrainz');
5354
}
55+
56+
if (config.scrobblers.slack && config.scrobblers.slack.enabled) {
57+
this.enabledScrobblers.set('slack', new SlackScrobbler(window));
58+
} else {
59+
this.enabledScrobblers.delete('slack');
60+
}
5461
},
5562

5663
async createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType) {

Diff for: src/plugins/scrobbler/menu.ts

+78
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,63 @@ async function promptListenbrainzOptions(
7979
}
8080
}
8181

82+
async function promptSlackOptions(
83+
options: ScrobblerPluginConfig,
84+
setConfig: SetConfType,
85+
window: BrowserWindow,
86+
) {
87+
const output = await prompt(
88+
{
89+
title: 'Slack Settings',
90+
label: 'Slack Settings',
91+
type: 'multiInput',
92+
multiInputOptions: [
93+
{
94+
label: 'Slack OAuth Token',
95+
value: options.scrobblers.slack?.token,
96+
inputAttrs: {
97+
type: 'text',
98+
},
99+
},
100+
{
101+
label: 'Slack Cookie Token (d cookie value)',
102+
value: options.scrobblers.slack?.cookieToken,
103+
inputAttrs: {
104+
type: 'text',
105+
},
106+
},
107+
{
108+
label: 'Emoji Name (for album art upload)',
109+
value: options.scrobblers.slack?.emojiName,
110+
inputAttrs: {
111+
type: 'text',
112+
},
113+
},
114+
],
115+
resizable: true,
116+
height: 360,
117+
...promptOptions(),
118+
},
119+
window,
120+
);
121+
122+
if (output) {
123+
if (output[0]) {
124+
options.scrobblers.slack.token = output[0];
125+
}
126+
127+
if (output[1]) {
128+
options.scrobblers.slack.cookieToken = output[1];
129+
}
130+
131+
if (output[2]) {
132+
options.scrobblers.slack.emojiName = output[2];
133+
}
134+
135+
setConfig(options);
136+
}
137+
}
138+
82139
export const onMenu = async ({
83140
window,
84141
getConfig,
@@ -147,5 +204,26 @@ export const onMenu = async ({
147204
},
148205
],
149206
},
207+
{
208+
label: 'Slack',
209+
submenu: [
210+
{
211+
label: t('main.menu.plugins.enabled'),
212+
type: 'checkbox',
213+
checked: Boolean(config.scrobblers.slack?.enabled),
214+
click(item) {
215+
backend.toggleScrobblers(config, window);
216+
config.scrobblers.slack.enabled = item.checked;
217+
setConfig(config);
218+
},
219+
},
220+
{
221+
label: 'Slack Settings',
222+
click() {
223+
promptSlackOptions(config, setConfig, window);
224+
},
225+
},
226+
],
227+
},
150228
];
151229
};

Diff for: src/plugins/scrobbler/services/slack-api-client.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import axios, { AxiosResponse } from 'axios';
2+
3+
/**
4+
* Centralized Slack API client for all requests
5+
*/
6+
export class SlackApiClient {
7+
readonly token: string;
8+
readonly cookie: string;
9+
10+
constructor(token: string, cookie: string) {
11+
this.token = token;
12+
this.cookie = cookie;
13+
}
14+
15+
private getBaseHeaders(): Record<string, string> {
16+
return {
17+
'Cookie': `d=${this.cookie}`,
18+
};
19+
}
20+
21+
/**
22+
* POST to a Slack API endpoint
23+
*/
24+
async post(endpoint: string, data: any, formData = false): Promise<AxiosResponse> {
25+
const url = `https://slack.com/api/${endpoint}`;
26+
let headers = this.getBaseHeaders();
27+
let payload = data;
28+
if (formData) {
29+
headers = { ...headers, ...data.getHeaders() };
30+
} else {
31+
headers['Content-Type'] = 'application/x-www-form-urlencoded';
32+
payload = new URLSearchParams(data).toString();
33+
}
34+
return axios.post(url, payload, { headers, maxBodyLength: Infinity, validateStatus: () => true });
35+
}
36+
37+
/**
38+
* GET from a Slack API endpoint
39+
*/
40+
async get(endpoint: string, params: Record<string, any> = {}): Promise<AxiosResponse> {
41+
const url = `https://slack.com/api/${endpoint}`;
42+
const headers = this.getBaseHeaders();
43+
return axios.get(url, { headers, params, validateStatus: () => true });
44+
}
45+
}
46+
47+
export interface SlackApiResponse {
48+
ok: boolean;
49+
error?: string;
50+
[key: string]: any;
51+
}

0 commit comments

Comments
 (0)