Skip to content

Commit

Permalink
Merge pull request #258 from Mar0xy/master2
Browse files Browse the repository at this point in the history
Add ListenBrainz implementation
  • Loading branch information
Mastermindzh authored Jul 31, 2023
2 parents 11cc209 + 586f7b5 commit ec82aa8
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 33 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
{
"cSpell.words": [
"Brainz",
"flac",
"geqnfr",
"hifi",
"listenbrainz",
"playpause",
"rescrobbler",
"scrobble",
"scrobbling",
"Songwhip",
"trackid",
"tracklist",
Expand Down
6 changes: 6 additions & 0 deletions src/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export const settings = {
disableHardwareMediaKeys: "disableHardwareMediaKeys",
enableCustomHotkeys: "enableCustomHotkeys",
enableDiscord: "enableDiscord",
ListenBrainz: {
root: "ListenBrainz",
enabled: "ListenBrainz.enabled",
api: "ListenBrainz.api",
token: "ListenBrainz.token",
},
flags: {
root: "flags",
disableHardwareMediaKeys: "flags.disableHardwareMediaKeys",
Expand Down
4 changes: 0 additions & 4 deletions src/constants/statuses.ts

This file was deleted.

134 changes: 134 additions & 0 deletions src/features/listenbrainz/listenbrainz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import axios from "axios";
import { ipcRenderer } from "electron";
import Store from "electron-store";
import { settings } from "../../constants/settings";
import { MediaStatus } from "../../models/mediaStatus";
import { settingsStore } from "../../scripts/settings";
import { Logger } from "../logger";
import { StoreData } from "./models/storeData";

const ListenBrainzStore = new Store({ name: "listenbrainz" });

export const ListenBrainzConstants = {
oldData: "oldData",
};

export class ListenBrainz {
/**
* Create the object to store old information in the Store :)
* @param title
* @param artists
* @param duration
* @returns data passed along in an object + a "listenedAt" key with the current time
*/
private static constructStoreData(title: string, artists: string, duration: number): StoreData {
return {
listenedAt: Math.floor(new Date().getTime() / 1000),
title,
artists,
duration,
};
}

/**
* Call the ListenBrainz API and create playing now payload and scrobble old song
* @param title
* @param artists
* @param status
* @param duration
*/
public static async scrobble(
title: string,
artists: string,
status: string,
duration: number
): Promise<void> {
try {
if (status === MediaStatus.paused) {
return;
} else {
// Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
const playing_data = {
listen_type: "playing_now",
payload: [
{
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "tidal.com",
duration: duration,
},
artist_name: artists,
track_name: title,
},
},
],
};

await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
playing_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
if (!oldData) {
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
} else {
if (oldData.title !== title) {
// This constructs the data required to scrobble the data after the song finishes
const scrobble_data = {
listen_type: "single",
payload: [
{
listened_at: oldData.listenedAt,
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "listen.tidal.com",
duration: oldData.duration,
},
artist_name: oldData.artists,
track_name: oldData.title,
},
},
],
};
await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
scrobble_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
}
}
}
} catch (error) {
const logger = new Logger(ipcRenderer);
logger.log(JSON.stringify(error));
}
}
}

export { ListenBrainzStore };
9 changes: 9 additions & 0 deletions src/features/listenbrainz/models/storeData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Data saved for ListenBrainz
*/
export interface StoreData {
listenedAt: number;
title: string;
artists: string;
duration: number;
}
3 changes: 1 addition & 2 deletions src/features/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ export class Logger {
public log(content: string, object: object = {}) {
if (this.ipcRenderer) {
this.ipcRenderer.send(globalEvents.log, { content, object });
} else {
console.log(`${content} \n ${JSON.stringify(object, null, 2)}`);
}
console.log(`${content} \n ${JSON.stringify(object, null, 2)}`);
}
}
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function setFlags() {
}

/**
* Update the menuBarVisbility according to the store value
* Update the menuBarVisibility according to the store value
*
*/
function syncMenuBarWithStore() {
Expand Down
20 changes: 19 additions & 1 deletion src/pages/settings/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ let adBlock: HTMLInputElement,
skippedArtists: HTMLInputElement,
theme: HTMLSelectElement,
trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement;
updateFrequency: HTMLInputElement,
enableListenBrainz: HTMLInputElement,
ListenBrainzAPI: HTMLInputElement,
ListenBrainzToken: HTMLInputElement;

function getThemeFiles() {
const selectElement = document.getElementById("themesList") as HTMLSelectElement;
const builtInThemes = getThemeListFromDirectory(process.resourcesPath);
Expand Down Expand Up @@ -87,6 +91,9 @@ function refreshSettings() {
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled);
ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api);
ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token);
}

/**
Expand Down Expand Up @@ -137,6 +144,10 @@ window.addEventListener("DOMContentLoaded", () => {
} else {
settingsStore.set(key, source.value);
}
// Live update the view for ListenBrainz input, hide if disabled/show if enabled
if (source.value === "on" && source.id === "enableListenBrainz") {
source.checked ? document.getElementById("listenbrainz__options").removeAttribute("hidden") : document.getElementById("listenbrainz__options").setAttribute("hidden", "true");
}
ipcRenderer.send(globalEvents.storeChanged);
});
}
Expand Down Expand Up @@ -183,8 +194,12 @@ window.addEventListener("DOMContentLoaded", () => {
skippedArtists = get("skippedArtists");
singleInstance = get("singleInstance");
updateFrequency = get("updateFrequency");
enableListenBrainz = get("enableListenBrainz");
ListenBrainzAPI = get("ListenBrainzAPI");
ListenBrainzToken = get("ListenBrainzToken");

refreshSettings();
enableListenBrainz.checked ? document.getElementById("listenbrainz__options").removeAttribute("hidden") : document.getElementById("listenbrainz__options").setAttribute("hidden", "true");

addInputListener(adBlock, settings.adBlock);
addInputListener(api, settings.api);
Expand All @@ -206,4 +221,7 @@ window.addEventListener("DOMContentLoaded", () => {
addSelectListener(theme, settings.theme);
addInputListener(trayIcon, settings.trayIcon);
addInputListener(updateFrequency, settings.updateFrequency);
addInputListener(enableListenBrainz, settings.ListenBrainz.enabled);
addTextAreaListener(ListenBrainzAPI, settings.ListenBrainz.api);
addTextAreaListener(ListenBrainzToken, settings.ListenBrainz.token);
});
29 changes: 29 additions & 0 deletions src/pages/settings/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,35 @@ <h4>Discord RPC</h4>
</label>
</div>
</div>
<div class="group">
<p class="group__title">ListenBrainz</p>
<div class="group__option">
<div class="group__description">
<h4>Enable ListenBrainz</h4>
<p>Scrobble your listens directly to ListenBrainz.</p>
</div>
<label class="switch">
<input id="enableListenBrainz" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div id="listenbrainz__options" hidden="true">
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz API Url</h4>
<p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p>
</div>
</div>
<textarea id="ListenBrainzAPI" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz User Token</h4>
<p>Provide the user token you can get from the settings page.</p>
</div>
</div>
<textarea id="ListenBrainzToken" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
</div>
</div>
</section>

<section id="advanced-section" class="tabs__section">
Expand Down
Loading

0 comments on commit ec82aa8

Please sign in to comment.