diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa19d2..c761061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 5.4.0 +- Added [Songwhip](https://songwhip.com/) integration - [DEV]: - added a logger to log into STDout - added "watchStart" which will automatically restart electron when it detects a source code change diff --git a/README.md b/README.md index 6fc5c6a..454c664 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect - Notifications - Custom [theming](./docs/theming.md) - Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts)) +- Songwhip.com integration (hotkey `ctrl + w`) - API for status and playback - Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495)) - Custom [integrations](#integrations) diff --git a/package-lock.json b/package-lock.json index 30b3e85..24c9479 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@electron/remote": "^2.0.9", + "axios": "^1.4.0", "discord-rpc": "^4.0.1", "electron-store": "^8.1.0", "express": "^4.18.2", @@ -1834,6 +1835,16 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3426,6 +3437,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -4843,6 +4855,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -6886,6 +6904,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/ps-tree": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", diff --git a/package.json b/package.json index aef9825..af70750 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "license": "MIT", "dependencies": { "@electron/remote": "^2.0.9", + "axios": "^1.4.0", "discord-rpc": "^4.0.1", "electron-store": "^8.1.0", "express": "^4.18.2", diff --git a/src/constants/globalEvents.ts b/src/constants/globalEvents.ts index 0929023..35f568c 100644 --- a/src/constants/globalEvents.ts +++ b/src/constants/globalEvents.ts @@ -10,5 +10,6 @@ export const globalEvents = { showSettings: "showSettings", storeChanged: "storeChanged", error: "error", + whip: "whip", log: "log", }; diff --git a/src/features/songwhip/models/Artist.ts b/src/features/songwhip/models/Artist.ts new file mode 100644 index 0000000..e605f8a --- /dev/null +++ b/src/features/songwhip/models/Artist.ts @@ -0,0 +1,21 @@ +import { ServiceLinks } from "./ServiceLinks"; + +export interface Artist { + type: string; + id: number; + path: string; + name: string; + sourceUrl: string; + sourceCountry: string; + url: string; + image: string; + createdAt: string; + updatedAt: string; + refreshedAt: string; + serviceIds: { [key: string]: string }; + orchardId: string; + spotifyId: string; + links: { [key: string]: ServiceLinks[] }; + linksCountries: string[]; + description: string; +} diff --git a/src/features/songwhip/models/ServiceLinks.ts b/src/features/songwhip/models/ServiceLinks.ts new file mode 100644 index 0000000..bb09716 --- /dev/null +++ b/src/features/songwhip/models/ServiceLinks.ts @@ -0,0 +1,4 @@ +export interface ServiceLinks { + link: string; + countries: string[]; +} diff --git a/src/features/songwhip/models/whip.ts b/src/features/songwhip/models/whip.ts new file mode 100644 index 0000000..2433a12 --- /dev/null +++ b/src/features/songwhip/models/whip.ts @@ -0,0 +1,27 @@ +import { Artist } from "./Artist"; +import { ServiceLinks } from "./ServiceLinks"; + +export interface WhippedResult { + status: string; + data: { + item: { + type: string; + id: number; + path: string; + name: string; + url: string; + sourceUrl: string; + sourceCountry: string; + releaseDate: string; + createdAt: string; + updatedAt: string; + refreshedAt: string; + image: string; + isrc: string; + isExplicit: boolean; + links: { [key: string]: ServiceLinks[] }; + linksCountries: string[]; + artists: Artist[]; + }; + }; +} diff --git a/src/features/songwhip/songwhip.ts b/src/features/songwhip/songwhip.ts new file mode 100644 index 0000000..6ac2aa8 --- /dev/null +++ b/src/features/songwhip/songwhip.ts @@ -0,0 +1,32 @@ +import { WhippedResult } from "./models/whip"; +import axios from "axios"; + +export class Songwhip { + /** + * Call the songwhip API and create a shareable songwhip page + * @param currentUrl + * @returns + */ + public static async whip(currentUrl: string): Promise { + try { + const response = await axios.post("https://songwhip.com/api/songwhip/create", { + url: currentUrl, + // doesn't actually matter.. returns everything the same way anyway + country: "NL", + }); + + return response.data; + } catch (error) { + console.log(JSON.stringify(error)); + } + } + + /** + * Transform a songwhip response into a shareable url + * @param response + * @returns + */ + public static getWhipUrl(response: WhippedResult) { + return `https://songwhip.com${response.data.item.url}`; + } +} diff --git a/src/main.ts b/src/main.ts index 8d73feb..7de3b94 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,6 +26,7 @@ import { import { settings } from "./constants/settings"; import { addTray, refreshTray } from "./scripts/tray"; import { MediaInfo } from "./models/mediaInfo"; +import { Songwhip } from "./features/songwhip/songwhip"; import { Logger } from "./features/logger"; const tidalUrl = "https://listen.tidal.com"; @@ -222,4 +223,8 @@ ipcMain.on(globalEvents.error, (event) => { console.log(event); }); +ipcMain.handle(globalEvents.whip, async (event, url) => { + return await Songwhip.whip(url); +}); + Logger.watch(ipcMain); diff --git a/src/preload.ts b/src/preload.ts index 91144dd..88ee765 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,16 +1,17 @@ -import { Notification, app, dialog } from "@electron/remote"; -import { ipcRenderer } from "electron"; +import { app, dialog, Notification } from "@electron/remote"; +import { clipboard, ipcRenderer } from "electron"; import fs from "fs"; import Player from "mpris-service"; import { globalEvents } from "./constants/globalEvents"; import { settings } from "./constants/settings"; import { statuses } from "./constants/statuses"; +import { Songwhip } from "./features/songwhip/songwhip"; import { Options } from "./models/options"; import { downloadFile } from "./scripts/download"; import { addHotkey } from "./scripts/hotkeys"; - import { settingsStore } from "./scripts/settings"; import { setTitle } from "./scripts/window-functions"; + const notificationPath = `${app.getPath("userData")}/notification.jpg`; const appName = "Tidal Hifi"; let currentSong = ""; @@ -232,6 +233,15 @@ function addHotKeys() { addHotkey("control+r", function () { elements.click("repeat"); }); + addHotkey("control+w", async function () { + const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL()); + const url = Songwhip.getWhipUrl(result); + clipboard.writeText(url); + new Notification({ + title: `Successfully whipped: `, + body: `URL copied to clipboard: ${url}`, + }).show(); + }); } // always add the hotkey for the settings window