Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions native/injector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import electron from "electron";
import os from "os";

import { readFile, rm, writeFile } from "fs/promises";
import mime from "mime";
Expand Down Expand Up @@ -209,11 +210,11 @@ require(startPath);
const requirePrefix = `import { createRequire } from 'module';const require = createRequire(${JSON.stringify(pathToFileURL(process.resourcesPath + "/").href)});`;
// Call to register native module
ipcHandle("__Luna.registerNative", async (_, name: string, code: string) => {
const tempPath = path.join(bundleDir, Math.random().toString() + ".mjs");
const tempFile = path.join(os.tmpdir(), Math.random().toString() + ".mjs")
try {
await writeFile(tempPath, requirePrefix + code, "utf8");
await writeFile(tempFile, requirePrefix + code, "utf8");
// Load module
const exports = (globalThis.luna.modules[name] = await import(pathToFileURL(tempPath).href));
const exports = (globalThis.luna.modules[name] = await import(pathToFileURL(tempFile).href));
const channel = `__LunaNative.${name}`;
// Register handler for calling module exports
ipcHandle(channel, async (_, exportName, ...args) => {
Expand All @@ -227,7 +228,7 @@ ipcHandle("__Luna.registerNative", async (_, name: string, code: string) => {
});
return channel;
} finally {
await rm(tempPath, { force: true });
await rm(tempFile, { force: true });
}
});
// Literally just to log if preload fails
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "luna",
"version": "1.2.6-alpha",
"version": "1.2.9-alpha",
"description": "A client mod for the Tidal music app for plugins",
"author": {
"name": "Inrixia",
Expand Down
4 changes: 2 additions & 2 deletions plugins/lib/src/classes/Album.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export class Album extends ContentBase implements MediaCollection {
return this.tidalAlbum.numberOfTracks!;
}
public tMediaItems: () => Promise<redux.MediaItem[]> = memoize(async () => {
const playlistIitems = await TidalApi.albumItems(this.id);
return playlistIitems?.items ?? [];
const albumItems = await TidalApi.albumItems(this.id);
return albumItems ?? [];
});
public async mediaItems() {
return MediaItem.fromTMediaItems(await this.tMediaItems());
Expand Down
2 changes: 1 addition & 1 deletion plugins/lib/src/classes/ContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class ContextMenu {
* Will return null if the element is not found (usually means no context menu is open)
*/
public static async getCurrent() {
const contextMenu = await observePromise<ContextMenuElem>(`[data-type="list-container__context-menu"]`, 1000);
const contextMenu = await observePromise<ContextMenuElem>(unloads, `[data-type="list-container__context-menu"]`, 1000);
if (contextMenu !== null) {
const templateButton = contextMenu.querySelector(`div[data-type="contextmenu-item"]`) as Element | undefined;
contextMenu.addButton = (text, onClick) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { PlaybackInfo } from "../../helpers";
import type { MetaTags } from "./MediaItem.tags";

const downloads: Record<redux.ItemId, { progress: FetchProgress; promise: Promise<void> } | undefined> = {};
export const downloadProgress = async (trackId: redux.ItemId) => downloads[trackId];
export const downloadProgress = async (trackId: redux.ItemId) => downloads[trackId]?.progress;
export const download = async (playbackInfo: PlaybackInfo, path: string, tags?: MetaTags): Promise<void> => {
if (downloads[playbackInfo.trackId] !== undefined) return downloads[playbackInfo.trackId]!.promise;
try {
Expand Down
32 changes: 17 additions & 15 deletions plugins/lib/src/classes/MediaItem/MediaItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { IRecording, ITrack } from "musicbrainz-api";

import { ftch, ReactiveStore, type Tracer } from "@luna/core";

import { getPlaybackInfo, type PlaybackInfo } from "../../helpers";
import { getPlaybackInfo, parseDate, type PlaybackInfo } from "../../helpers";
import { libTrace, unloads } from "../../index.safe";
import * as redux from "../../redux";
import { Album } from "../Album";
Expand All @@ -13,7 +13,7 @@ import { PlayState } from "../PlayState";
import { Quality } from "../Quality";
import { TidalApi } from "../TidalApi";
import { download, downloadProgress } from "./MediaItem.download.native";
import { makeTags, MetaTags } from "./MediaItem.tags";
import { availableTags, makeTags, MetaTags } from "./MediaItem.tags";

type MediaFormat = {
bitDepth?: number;
Expand All @@ -29,6 +29,7 @@ type MediaItemCache = {

export class MediaItem extends ContentBase {
public static readonly trace: Tracer = libTrace.withSource(".MediaItem").trace;
public static readonly availableTags = availableTags;

private static cache = ReactiveStore.getStore("@luna/MediaItemCache");

Expand All @@ -55,7 +56,7 @@ export class MediaItem extends ContentBase {
return super.fromStore(itemId, "mediaItems", async (mediaItem) => {
mediaItem = mediaItem ??= await this.fetchMediaItem(itemId, contentType);
if (mediaItem === undefined) return;
return new MediaItem(itemId, mediaItem, await mediaItemCache);
return new MediaItem(itemId, mediaItem, contentType, await mediaItemCache);
});
}
public static fromIsrc: (isrc: string) => Promise<MediaItem | undefined> = memoize(async (isrc) => {
Expand Down Expand Up @@ -145,6 +146,7 @@ export class MediaItem extends ContentBase {
constructor(
public readonly id: redux.ItemId,
tidalMediaItem: redux.MediaItem,
public readonly contentType: redux.ContentType,
private readonly cache: MediaItemCache,
) {
super();
Expand Down Expand Up @@ -230,7 +232,7 @@ export class MediaItem extends ContentBase {
});

public async *isrcs(): AsyncIterable<string> {
if (this.tidalItem.contentType !== "track") return;
if (this.contentType !== "track") return;
const seen = new Set<string>();
if (this.tidalItem.isrc) {
yield this.tidalItem.isrc;
Expand Down Expand Up @@ -259,17 +261,20 @@ export class MediaItem extends ContentBase {
});

public releaseDate: () => Promise<Date | undefined> = memoize(async () => {
let releaseDate = this.tidalItem.releaseDate ?? this.tidalItem.streamStartDate;
let releaseDate = parseDate(this.tidalItem.releaseDate) ?? parseDate(this.tidalItem.streamStartDate);
if (releaseDate === undefined) {
const brainzItem = await this.brainzItem();
releaseDate = brainzItem?.recording?.["first-release-date"] ?? releaseDate;
releaseDate = parseDate(brainzItem?.recording?.["first-release-date"]);
}
if (releaseDate === undefined) {
const album = await this.album();
releaseDate = album?.releaseDate ?? releaseDate;
releaseDate ??= (await album?.brainzAlbum())?.date ?? releaseDate;
releaseDate = parseDate(album?.releaseDate);
if (releaseDate === undefined) {
const brainzAlbum = await album?.brainzAlbum();
releaseDate ??= parseDate(brainzAlbum?.date);
}
}
if (releaseDate) return new Date(releaseDate);
return releaseDate;
});

/**
Expand Down Expand Up @@ -298,9 +303,6 @@ export class MediaItem extends ContentBase {
// #endregion

// #region Properties
public get contentType() {
return this.tidalItem.contentType;
}
public get trackNumber() {
return this.tidalItem.trackNumber;
}
Expand All @@ -311,18 +313,18 @@ export class MediaItem extends ContentBase {
return this.tidalItem.peak;
}
public get replayGain(): number {
if (this.tidalItem.contentType !== "track") return 0;
if (this.contentType !== "track") return 0;
return this.tidalItem.replayGain;
}
public get url(): string {
return this.tidalItem.url;
}
public get qualityTags(): Quality[] {
if (this.tidalItem.contentType !== "track") return [];
if (this.contentType !== "track") return [];
return Quality.fromMetaTags(this.tidalItem.mediaMetadata?.tags);
}
public get bestQuality(): Quality {
if (this.tidalItem.contentType !== "track") {
if (this.contentType !== "track") {
this.trace.warn("MediaItem quality called on non-track!", this);
return Quality.High;
}
Expand Down
12 changes: 10 additions & 2 deletions plugins/lib/src/classes/TidalApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getCredentials } from "../../helpers";
import { libTrace } from "../../index.safe";
import * as redux from "../../redux";

import type { AlbumPage } from "./types/AlbumPage";
import { PlaybackInfoResponse } from "./types/PlaybackInfo";

export type * from "./types";
Expand Down Expand Up @@ -61,9 +62,16 @@ export class TidalApi {
return this.fetch<redux.Album>(`https://desktop.tidal.com/v1/albums/${albumId}?${this.queryArgs()}`);
}
public static async albumItems(albumId: redux.ItemId) {
return this.fetch<{ items: redux.MediaItem[]; totalNumberOfItems: number; offset: number; limit: -1 }>(
`https://desktop.tidal.com/v1/albums/${albumId}/items?${this.queryArgs()}&limit=-1`,
const albumPage = await this.fetch<AlbumPage>(
`https://desktop.tidal.com/v1/pages/album?albumId=${albumId}&countryCode=NZ&locale=en_US&deviceType=DESKTOP`,
);
for (const row of albumPage?.rows ?? []) {
for (const module of row.modules) {
if (module.type === "ALBUM_ITEMS" && module.pagedList) {
return module.pagedList.items;
}
}
}
}

public static playlist(playlistUUID: redux.ItemId) {
Expand Down
15 changes: 15 additions & 0 deletions plugins/lib/src/classes/TidalApi/types/AlbumPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { redux } from "@luna/lib";

export type AlbumPage = {
id: string;
title: string;
rows: {
modules: {
/** ALBUM_ITEMS is what we want */
type: string;
pagedList?: {
items: redux.MediaItem[];
};
}[];
}[];
};
1 change: 1 addition & 0 deletions plugins/lib/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from "./getCredentials";
export * from "./getPlaybackInfo";
export * from "./getPlaybackInfo.dasha.native";
export * from "./observable";
export * from "./parseDate";
export * from "./safeTimeout";
11 changes: 7 additions & 4 deletions plugins/lib/src/helpers/observable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// based on: https://github.com/KaiHax/kaihax/blob/master/src/patcher.ts

import type { VoidFn } from "@inrixia/helpers";
import type { LunaUnloads } from "@luna/core";
import { unloads } from "../index.safe";

export type ObserveCallback<E extends Element = Element> = (elem: E) => unknown;
Expand Down Expand Up @@ -31,18 +32,20 @@ const observer = new MutationObserver((records) => {
* @param cb The callback function to execute when a matching element is found cast to type T.
* @returns An `Unload` function that, when called, will stop observing for this selector and callback pair.
*/
export const observe = <T extends Element = Element>(selector: string, cb: ObserveCallback<T>): VoidFn => {
export const observe = <T extends Element = Element>(unloads: LunaUnloads, selector: string, cb: ObserveCallback<T>): VoidFn => {
if (observables.size === 0)
observer.observe(document.body, {
subtree: true,
childList: true,
});
const entry: ObserveEntry = [selector, cb as ObserveCallback<Element>];
observables.add(entry);
return () => {
const unload = () => {
observables.delete(entry);
if (observables.size === 0) observer.disconnect();
};
unloads.add(unload);
return unload;
};

// Disconnect and remove observables on unload
Expand All @@ -56,9 +59,9 @@ unloads.add(observables.clear.bind(observables));
* @param timeoutMs The maximum time (in milliseconds) to wait for the element to appear.
* @returns A Promise that resolves with the found Element (cast to type T) or null if the timeout is reached.
*/
export const observePromise = <T extends Element>(selector: string, timeoutMs: number = 1000): Promise<T | null> =>
export const observePromise = <T extends Element>(unloads: LunaUnloads, selector: string, timeoutMs: number = 1000): Promise<T | null> =>
new Promise<T | null>((res) => {
const unob = observe(selector, (elem) => {
const unob = observe(unloads, selector, (elem) => {
unob();
clearTimeout(timeout);
res(elem as T);
Expand Down
6 changes: 6 additions & 0 deletions plugins/lib/src/helpers/parseDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const parseDate = (date: string | Date | null | undefined): Date | undefined => {
if (date === null || date === undefined) return undefined;
if (typeof date === "string") date = new Date(date);
if (isNaN(date.getTime())) return undefined;
return date;
};
4 changes: 3 additions & 1 deletion plugins/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export * as redux from "./redux";

import { observePromise } from "./helpers/observable";

observePromise("div[class^='_mainContainer'] > div[class^='_bar'] > div[class^='_title']", 30000).then((title) => {
import { unloads } from "./index.safe";

observePromise(unloads, "div[class^='_mainContainer'] > div[class^='_bar'] > div[class^='_title']", 30000).then((title) => {
if (title !== null) title.innerHTML = 'TIDA<b><span style="color: #32f4ff;">Luna</span></b> <span style="color: red;">BETA</span>';
});

Expand Down
4 changes: 3 additions & 1 deletion plugins/ui/src/SettingsPage/PluginStoreTab/LunaStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ export const LunaStore = React.memo(({ url, onRemove }: { url: string; onRemove:
}
/>
<Grid columns={2} spacing={2} container>
{pkg?.plugins.map((plugin) => <Grid size={1} children={<LunaStorePlugin url={`${url}/${plugin}`} key={plugin} />} />)}
{pkg?.plugins.map((plugin) => (
<Grid size={1} children={<LunaStorePlugin url={`${url}/${isLocalDevStore ? plugin : plugin.replace(" ", ".")}`} key={plugin} />} />
))}
</Grid>
</Stack>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const LunaStorePlugin = React.memo(({ url }: { url: string }) => {

if (!plugin) return null;

const version = url.startsWith("http://127.0.0.1") ? `${plugin.package?.version ?? ""} [DEV]` : plugin.package?.version;

return (
<ButtonBase
onMouseEnter={() => setIsHovered(true)}
Expand All @@ -41,6 +43,7 @@ export const LunaStorePlugin = React.memo(({ url }: { url: string }) => {
<LunaPluginHeader
sx={{ transition: "opacity 0.3s ease-in-out", opacity: isHovered ? 0.2 : 1, width: "100%" }}
name={plugin.name}
version={version}
loadError={loadError}
author={plugin.package?.author}
desc={plugin.package?.description}
Expand Down
4 changes: 3 additions & 1 deletion plugins/ui/src/SettingsPage/PluginStoreTab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ export const addToStores = (url: string) => {
};

// Devs! Add your stores here <3
// TODO: Abstract this to a git repo with versioned stores
// TODO: Abstract this to a git repo
addToStores("https://github.com/Inrixia/neptune-plugins/releases/download/dev/store.json");
addToStores("https://github.com/wont-stream/lunar/releases/download/dev/store.json");
addToStores("https://github.com/jxnxsdev/luna-plugins/releases/download/latest/store.json");

export const PluginStoreTab = React.memo(() => {
const [_storeUrls, setPluginStores] = useState<string[]>(obyStore.unwrap(storeUrls));
Expand Down
8 changes: 6 additions & 2 deletions plugins/ui/src/SettingsPage/PluginsTab/LunaPluginHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ import { LunaAuthorDisplay, LunaLink } from "../../components";

export interface LunaPluginComponentProps extends PropsWithChildren {
name: string;
version?: string;
link?: string;
loadError?: string;
author?: LunaAuthor | string;
desc?: ReactNode;
sx?: BoxProps["sx"];
}
export const LunaPluginHeader = React.memo(({ name, loadError, author, desc, children, sx, link }: LunaPluginComponentProps) => (
export const LunaPluginHeader = React.memo(({ name, version, loadError, author, desc, children, sx, link }: LunaPluginComponentProps) => (
<Box sx={sx}>
<Stack direction="row" alignItems="center" spacing={1}>
<LunaLink href={link} children={<Typography variant="h6" children={name} />} />
<Typography variant="h6">
<LunaLink href={link}>{name}</LunaLink>
{version && <Typography variant="caption" style={{ opacity: 0.7, marginLeft: 6 }} children={version} />}
</Typography>
{children}
{loadError && (
<Typography
Expand Down
4 changes: 4 additions & 0 deletions plugins/ui/src/SettingsPage/PluginsTab/LunaPluginSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ export const LunaPluginSettings = React.memo(({ plugin }: { plugin: LunaPlugin }

const disabled = !enabled || loading;

const isDev = plugin.store.url.startsWith("http://127.0.0.1");

const author = pkg.author;
const desc = pkg.description;
const name = pkg.name;
const link = pkg.homepage ?? pkg.repository?.url;
let version = isDev ? `${pkg.version ?? ""} [DEV]` : pkg.version;

// Dont allow disabling core plugins
const isCore = LunaPlugin.corePlugins.has(name);
Expand All @@ -76,6 +79,7 @@ export const LunaPluginSettings = React.memo(({ plugin }: { plugin: LunaPlugin }
>
<LunaPluginHeader
name={name}
version={version}
link={link}
loadError={loadError}
author={author}
Expand Down
2 changes: 1 addition & 1 deletion plugins/ui/src/components/settings/LunaButtonSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const LunaButtonSetting = React.memo((props: LunaButtonSettingProps) => (
height: 40,
...props.sx,
}}
children={props.children}
children={props.children ?? props.title}
/>
}
/>
Expand Down
2 changes: 1 addition & 1 deletion render/src/LunaPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class LunaPlugin {
if (name in this.plugins) return this.plugins[name];

// Disable liveReload on load so people dont accidentally leave it on
storeInit.liveReload ??= false;
storeInit.liveReload = false;

const store = await LunaPlugin.pluginStorage.getReactive<LunaPluginStorage>(name);
Object.assign(store, storeInit);
Expand Down