From 01c5ec7ebbf7603801a207c33118c21e439f1137 Mon Sep 17 00:00:00 2001 From: Rick van Lieshout Date: Sun, 27 Oct 2024 23:05:55 +0100 Subject: [PATCH] PoC: separated tidal interface controller into separate controllers --- src/TidalControllers/DomTidalController.ts | 168 ++++++++++++++++++ src/TidalControllers/MediaController.ts | 13 ++ .../MediaSessionTidalController.ts | 19 ++ src/preload.ts | 32 ++-- 4 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 src/TidalControllers/DomTidalController.ts create mode 100644 src/TidalControllers/MediaController.ts create mode 100644 src/TidalControllers/MediaSessionTidalController.ts diff --git a/src/TidalControllers/DomTidalController.ts b/src/TidalControllers/DomTidalController.ts new file mode 100644 index 0000000..d86bc67 --- /dev/null +++ b/src/TidalControllers/DomTidalController.ts @@ -0,0 +1,168 @@ +import { TidalController } from "./MediaController"; + +export class DomTidalController implements TidalController { + public elements = { + play: '*[data-test="play"]', + pause: '*[data-test="pause"]', + next: '*[data-test="next"]', + previous: 'button[data-test="previous"]', + title: '*[data-test^="footer-track-title"]', + artists: '*[data-test^="grid-item-detail-text-title-artist"]', + home: '*[data-test="menu--home"]', + back: '[title^="Back"]', + forward: '[title^="Next"]', + search: '[class^="searchField"]', + shuffle: '*[data-test="shuffle"]', + repeat: '*[data-test="repeat"]', + account: '*[data-test^="profile-image-button"]', + settings: '*[data-test^="sidebar-menu-button"]', + openSettings: '*[data-test^="open-settings"]', + media: '*[data-test="current-media-imagery"]', + image: "img", + current: '*[data-test="current-time"]', + duration: '*[class^=playbackControlsContainer] *[data-test="duration"]', + bar: '*[data-test="progress-bar"]', + footer: "#footerPlayer", + mediaItem: "[data-type='mediaItem']", + album_header_title: '*[class^="playingFrom"] span:nth-child(2)', + playing_from: '*[class^="playingFrom"] span:nth-child(2)', + queue_album: "*[class^=playQueueItemsContainer] *[class^=groupTitle] span:nth-child(2)", + currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']", + album_name_cell: '[class^="album"]', + tracklist_row: '[data-test="tracklist-row"]', + volume: '*[data-test="volume"]', + favorite: '*[data-test="footer-favorite-button"]', + /** + * Get an element from the dom + * @param {*} key key in elements object to fetch + */ + get: function (key: string) { + return globalThis.document.querySelector(this[key.toLowerCase()]); + }, + + /** + * Get the icon of the current media + */ + getSongIcon: function () { + const figure = this.get("media"); + + if (figure) { + const mediaElement = figure.querySelector(this["image"]); + if (mediaElement) { + return mediaElement.src.replace("80x80", "640x640"); + } + } + + return ""; + }, + + /** + * returns an array of all artists in the current media + * @returns {Array} artists + */ + getArtistsArray: function () { + const footer = this.get("footer"); + + if (footer) { + const artists = footer.querySelectorAll(this.artists); + if (artists) + return Array.from(artists).map((artist) => (artist as HTMLElement).textContent); + } + return []; + }, + + /** + * unify the artists array into a string separated by commas + * @param {Array} artistsArray + * @returns {String} artists + */ + getArtistsString: function (artistsArray: string[]) { + if (artistsArray.length > 0) return artistsArray.join(", "); + return "unknown artist(s)"; + }, + + getAlbumName: function () { + //If listening to an album, get its name from the header title + if (globalThis.location.href.includes("/album/")) { + const albumName = globalThis.document.querySelector(this.album_header_title); + if (albumName) { + return albumName.textContent; + } + //If listening to a playlist or a mix, get album name from the list + } else if ( + globalThis.location.href.includes("/playlist/") || + globalThis.location.href.includes("/mix/") + ) { + // TODO: fix + // if (currentPlayStatus === MediaStatus.playing) { + // // find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell. + // // document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent + // const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem); + // if (row) { + // return row.querySelector(this.album_name_cell).textContent; + // } + // } + } + + // see whether we're on the queue page and get it from there + const queueAlbumName = this.getText("queue_album"); + if (queueAlbumName) { + return queueAlbumName; + } + + return ""; + }, + + isMuted: function () { + return this.get("volume").getAttribute("aria-checked") === "false"; // it's muted if aria-checked is false + }, + + isFavorite: function () { + return this.get("favorite").getAttribute("aria-checked") === "true"; + }, + + /** + * Shorthand function to get the text of a dom element + * @param {*} key key in elements object to fetch + */ + getText: function (key: string) { + const element = this.get(key); + return element ? element.textContent : ""; + }, + + /** + * Shorthand function to click a dom element + * @param {*} key key in elements object to fetch + */ + click: function (key: string) { + this.get(key).click(); + return this; + }, + + /** + * Shorthand function to focus a dom element + * @param {*} key key in elements object to fetch + */ + focus: function (key: string) { + return this.get(key).focus(); + }, + }; + + playPause = (): void => { + const play = this.elements.get("play"); + + if (play) { + this.elements.click("play"); + } else { + this.elements.click("pause"); + } + }; + + goToHome(): void { + this.elements.click("home"); + } + + hookup = (): void => { + throw new Error("Method not implemented."); + }; +} diff --git a/src/TidalControllers/MediaController.ts b/src/TidalControllers/MediaController.ts new file mode 100644 index 0000000..3d68e3e --- /dev/null +++ b/src/TidalControllers/MediaController.ts @@ -0,0 +1,13 @@ +export interface TidalController { + /** + * Play or pause the current media + */ + playPause(): void; + + /** + * Hook up the controller to the current web instance + */ + hookup(): void; + + goToHome(): void; +} diff --git a/src/TidalControllers/MediaSessionTidalController.ts b/src/TidalControllers/MediaSessionTidalController.ts new file mode 100644 index 0000000..addc90d --- /dev/null +++ b/src/TidalControllers/MediaSessionTidalController.ts @@ -0,0 +1,19 @@ +import { DomTidalController } from "./DomTidalController"; +import { TidalController } from "./MediaController"; + +export class MediaSessionTidalController implements TidalController { + public domMediaController: TidalController; + + constructor() { + this.domMediaController = new DomTidalController(); + } + goToHome(): void { + this.domMediaController.goToHome(); + } + playPause(): void { + globalThis.alert("Method not implemented"); + } + hookup(): void { + globalThis.alert("Method not implemented"); + } +} diff --git a/src/preload.ts b/src/preload.ts index d00d900..dbbcc8c 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -21,6 +21,9 @@ import { addHotkey } from "./scripts/hotkeys"; import { ObjectToDotNotation } from "./scripts/objectUtilities"; import { settingsStore } from "./scripts/settings"; import { setTitle } from "./scripts/window-functions"; +import { DomTidalController } from "./TidalControllers/DomTidalController"; +import { MediaSessionTidalController } from "./TidalControllers/MediaSessionTidalController"; +import { TidalController } from "./TidalControllers/MediaController"; const notificationPath = `${app.getPath("userData")}/notification.jpg`; let currentSong = ""; @@ -35,6 +38,16 @@ let currentShuffleState = false; let currentMediaInfo: MediaInfo; let currentNotification: Electron.Notification; +let tidalController: TidalController; + +// TODO: replace with setting +// eslint-disable-next-line no-constant-condition +if (true) { + tidalController = new DomTidalController(); +} else { + tidalController = new MediaSessionTidalController(); +} + const elements = { play: '*[data-test="play"]', pause: '*[data-test="pause"]', @@ -195,19 +208,6 @@ function getUpdateFrequency() { } } -/** - * Play or pause the current media - */ -function playPause() { - const play = elements.get("play"); - - if (play) { - elements.click("play"); - } else { - elements.click("pause"); - } -} - /** * Clears the old listenbrainz data on launch */ @@ -235,7 +235,7 @@ function addHotKeys() { }); addHotkey("Control+h", function () { - elements.click("home"); + tidalController.goToHome(); }); addHotkey("backspace", function () { @@ -319,7 +319,7 @@ function addIPCEventListeners() { case globalEvents.playPause: case globalEvents.play: case globalEvents.pause: - playPause(); + tidalController.playPause(); break; case globalEvents.next: elements.click("next"); @@ -455,7 +455,7 @@ function addMPRIS() { const eventValue = events[eventName]; switch (events[eventValue]) { case events.playpause: - playPause(); + tidalController.playPause(); break; default: elements.click(eventValue);