Tidal Luna Documentation

repository·master·Indexed 17 days ago

https://github.com/inrixia/tidaluna

A 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.

Tokens
22.1K
Snippets
68
Records
79
Agent score
69%

What's inside Tidal Luna

  1. Manually install Luna

    master

    If the Luna Installer fails, you can perform a manual installation:

    1. Download the desired luna.zip release from the Tidal Luna releases page.
    2. Locate your Tidal install resources folder:
      • Windows: %localappdata%\TIDAL\app-x.xx.x\resources
      • MacOS: /Applications/TIDAL.app/Contents/Resources
      • Linux: /opt/tidal-hifi/resources
    3. Rename the existing app.asar file to original.asar.
    4. Unzip the contents of luna.zip into a new folder named app inside the resources directory. Your directory structure should have the app folder sitting alongside original.asar.
    5. Start Tidal. You should see the Luna splashscreen.
  2. Install Luna via Nix

    master

    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:

    Method 1: Using an Overlay

    Add TidaLuna to your nixpkgs.overlay list:

    nixpkgs.overlay = [
      inputs.tidaLuna.overlays.default
    ];

    After adding the overlay, install the tidal-hifi package as usual.

    Method 2: Replacing the Package

    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
    ];
  3. Develop for the Luna client

    master

    To develop for the Luna client, follow these steps:

    1. Fork and clone the repository locally.
    2. Install dependencies using pnpm i.
    3. Start the build watcher with pnpm run watch.
    4. Link your build output to the Tidal app folder. You can do this via a directory symlink or by setting an environment variable:
      • Windows Symlink:
        mklink /D "%LOCALAPPDATA%\TIDAL\app-x.xx.x\resources\app" "./dist"
      • Environment Variable: Set TIDALUNA_DIST_PATH to the path of your dist folder (this avoids needing to restart the client when /native/injector.ts changes).
    5. Launch Luna.

    Reloading Code

    • Core Plugins: Plugins located under /plugins can be reloaded via the Luna Settings menu within the client.
    • Render/Native Code: Changes to code in /render or /native require a full client restart to take effect.
    mklink /D "%LOCALAPPDATA%\TIDAL\app-x.xx.x\resources\app" "./dist"
  4. How Redux action interception works

    master

    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:

    1. An action is dispatched.
    2. The system checks the interceptors registry for the specific ActionType.
    3. All registered InterceptCallback functions are executed.
    4. Decision Logic:
      • If a callback returns true, the action is considered cancelled and the dispatch process stops.
      • If a callback returns 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.

  5. Configure Tidal playback and player settings

    master

    The Player and SettingsState interfaces control how audio is delivered and how the player interacts with devices.

    Player Configuration

    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.

    Audio Settings

    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>;
    }
  6. Develop Luna plugins using the LunaPlugin class

    master

    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 */ }
    ]);
  7. Manage user session and authentication

    master

    The application tracks user identity and connectivity through the SessionState and Auth interfaces.

    Session State

    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).

    Authentication

    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;
    }
  8. Use the MediaItem type

    master

    The MediaItem<T> type is a generic wrapper used to associate a ContentType with its corresponding data object.

    • If T is `
  9. Compare audio qualities using min() and max()

    master

    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.Low
    import { 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);
  10. Control playback with PlayState

    master

    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.

    Playback Controls

    • 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.

    Shuffle and Repeat

    • 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);