Sign Luna on MacOS
masterAfter a manual installation on MacOS, you must sign the application to prevent it from being reverted. Run the following command in your terminal:
codesign --force --deep --sign - /Applications/TIDAL.apprepository·master·Indexed 17 days ago
https://github.com/inrixia/tidalunaA client modification for the TIDAL desktop application that provides a plugin system for developers to enhance and modify the client experience. Includes documentation on installation, developing for the Luna client, registering native modules via the global luna object, and using the @luna/lib.native package for system operations, media stream fetching, and decryption.
After a manual installation on MacOS, you must sign the application to prevent it from being reverted. Run the following command in your terminal:
codesign --force --deep --sign - /Applications/TIDAL.appIf the Luna Installer fails, you can perform a manual installation:
luna.zip release from the Tidal Luna releases page.%localappdata%\TIDAL\app-x.xx.x\resources/Applications/TIDAL.app/Contents/Resources/opt/tidal-hifi/resourcesapp.asar file to original.asar.luna.zip into a new folder named app inside the resources directory. Your directory structure should have the app folder sitting alongside original.asar.Luna is managed through Nix flakes. First, add TidaLuna to your inputs:
inputs.tidaLuna.url = "github:Inrixia/TidaLuna"Then, choose one of the following two methods to install the injected tidal-hifi client:
Add TidaLuna to your nixpkgs.overlay list:
nixpkgs.overlay = [
inputs.tidaLuna.overlays.default
];After adding the overlay, install the tidal-hifi package as usual.
Directly replace your current tidal-hifi package in your system configuration with the TidaLuna package:
environment.systemPackages = with pkgs; [
- tidal-hifi
+ inputs.tidaLuna.packages.${stdenv.hostPlatform.system}.default
];To develop for the Luna client, follow these steps:
pnpm i.pnpm run watch.app folder. You can do this via a directory symlink or by setting an environment variable:mklink /D "%LOCALAPPDATA%\TIDAL\app-x.xx.x\resources\app" "./dist"TIDALUNA_DIST_PATH to the path of your dist folder (this avoids needing to restart the client when /native/injector.ts changes)./plugins can be reloaded via the Luna Settings menu within the client./render or /native require a full client restart to take effect.mklink /D "%LOCALAPPDATA%\TIDAL\app-x.xx.x\resources\app" "./dist"To install Luna, follow these steps:
Important Notes:
Tidal Luna provides an interception layer for its Redux-based state management. This allows developers to inject logic into the action lifecycle.
The Interception Lifecycle:
interceptors registry for the specific ActionType.InterceptCallback functions are executed.true, the action is considered cancelled and the dispatch process stops.false, undefined, or any other value, the action continues its normal dispatch lifecycle.This mechanism is useful for side-effect management, preventing certain state transitions, or synchronizing external logic with internal state changes.
The Player and SettingsState interfaces control how audio is delivered and how the player interacts with devices.
Use the Player interface to manage active devices and volume control:
activeDeviceId: The ID of the currently active device.activeDeviceMode: Can be either "exclusive" or "shared".availableDevices: A list of PlayerAvailableDevice objects containing id, name, nativeDeviceId, and webDeviceId.Use SettingsState to manage user preferences:
audioNormalization: Set to "NONE", "ALBUM", or "TRACK".quality.streaming: Controls the AudioQuality of the stream.explicitContentEnabled: Boolean to toggle explicit content.// Example of Player interface structure
export interface Player {
activeDeviceId: string;
activeDeviceMode: PlayerDeviceMode; // "exclusive" | "shared"
availableDevices: PlayerAvailableDevice[];
desiredDeviceMode: Record<string, string>;
forceVolume: Record<string, boolean>;
hasPreloadedNextProduct: boolean;
}
// Example of SettingsState structure
export interface SettingsState {
audioNormalization: "NONE" | "ALBUM" | "TRACK";
audioSpectrumEnabled: boolean;
autoPlay: boolean;
desktop: {
autoStartMode: 0 | 1;
closeToTray: boolean;
};
explicitContentEnabled: boolean;
language: string;
openLinksInDesktopApp: boolean;
quality: { streaming: AudioQuality };
updateAvailable: boolean;
urls: Record<string, string>;
}To create a plugin for Tidal Luna, you must implement a module that exports specific properties. The LunaPlugin class manages the lifecycle (loading, enabling, disabling, unloading) of these modules.
When your plugin is loaded via import(), it can export the following ModuleExports to interact with the Luna runtime:
unloads?: LunaUnloads: A set of cleanup functions (of type LunaUnload) to be called when the plugin is unloaded.onUnload?: LunaUnload: A single cleanup function.Settings?: React.FC: A React component used to render the plugin's settings UI.errSignal?: Signal<string | undefined>: A signal used to communicate errors from the plugin back to the Luna runtime.Note on Unloading: It is critical to use the unloads set or onUnload hook to clean up side effects (like event listeners or timers), otherwise, they will leak when the plugin is reloaded or uninstalled.
// Example of what your plugin module (.mjs) should export
export const Settings = ({ data }) => <div>Plugin Settings</div>;
export const onUnload = () => {
console.log("Cleaning up plugin...");
};
export const errSignal = new Signal<string | undefined>(undefined);
export const unloads = new Set([
() => { /* cleanup task */ }
]);The application tracks user identity and connectivity through the SessionState and Auth interfaces.
SessionState contains metadata about the current client connection:
userId: The numeric ID of the logged-in user.isInitialized: Indicates if the session setup is complete.countryCode: The user's current country code.utmParameters: Tracking parameters for marketing/campaigns (banner, campaign, content, medium, source).Auth provides the necessary data for the authentication flow:
verificationUri: The URI used for user verification/login.export interface SessionState {
clientId: string;
clientUniqueKey: string;
countryCode: string;
facebookAccessToken?: string | null;
isInitialized: boolean;
isLoading: boolean;
isPolling: boolean;
userId: number;
utmParameters: {
banner: string;
campaign: string;
content: string;
medium: string;
source: string;
};
migrated: boolean;
}
export interface Auth {
verificationUri: string;
}The MediaItem<T> type is a generic wrapper used to associate a ContentType with its corresponding data object.
T is `The Quality class allows for easy comparison of multiple quality levels using static min and max methods. These methods return the Quality object representing the lowest or highest quality in the provided set based on their internal index.
import { Quality } from 'tidaluna/plugins/lib/classes/Quality';
const highest = Quality.max(Quality.Low, Quality.HiRes, Quality.MQA);
// returns Quality.HiRes
const lowest = Quality.min(Quality.Low, Quality.HiRes, Quality.MQA);
// returns Quality.Lowimport { Quality } from 'tidaluna/plugins/lib/classes/Quality';
const highest = Quality.max(Quality.Low, Quality.HiRes, Quality.MQA);
const lowest = Quality.min(Quality.Low, Quality.HiRes, Quality.MQA);The PlayState class provides a static API to control and monitor the current playback state. You can play, pause, skip, seek, and manage the play queue.
PlayState.play([mediaItemId]): Plays the current track or a specific track if an ID is provided.PlayState.pause(): Pauses playback.PlayState.next(): Skips to the next track.PlayState.previous(): Skips to the previous track.PlayState.moveTo(playQueueIndex): Moves to a specific index in the play queue.PlayState.playNext(mediaItemIds): Adds one or more media items to the queue immediately after the current track (these are temporary and removed after playing).PlayState.seek(time): Seeks to a specific time in seconds.PlayState.setShuffle(shuffle, shuffleItems?): Enables or disables shuffle mode. If shuffleItems is true, it will re-shuffle the current items.PlayState.setRepeatMode(repeatMode): Sets the repeat mode using redux.RepeatMode values.// Play a specific track
PlayState.play('some-media-item-id');
// Skip to next
PlayState.next();
// Seek to 30 seconds
PlayState.seek(30);
// Enable shuffle and re-shuffle current queue
PlayState.setShuffle(true, true);