Compare commits

...
6 Commits
Author SHA1 Message Date
meoware.exe 2150b87371 Merge pull request #71 from meowarex/dev
Word by Word Lyrics <3
2026-07-18 15:04:56 +10:00
meoware.exe 0d82c16168 Readme Update 2026-07-18 15:04:31 +10:00
meoware.exe 4c257f7ad9 Word by Word Lyrics <3 2026-07-18 15:02:07 +10:00
meoware.exe 7458df8eba Merge pull request #69 from meowarex/dev
Lyrics Button Hotfix <3
2026-07-17 00:01:18 +10:00
meoware.exe 6a5771cf9e Readme changes <3 2026-07-17 00:00:49 +10:00
meoware.exe a8a576c994 Lyrics Button Hotfix <3 2026-07-16 23:00:03 +10:00
19 changed files with 1920 additions and 107 deletions
@@ -32,11 +32,25 @@
"order": 20,
"fileNames": [
"lyrics-rl-api.patch",
"lyrics-rl-api-observer.patch"
"lyrics-rl-api-observer.patch",
"lyrics-rl-api-isrc.patch"
],
"title": "Radiant Lyrics API - WIP",
"description": "Use Radiant Lyrics API to fetch Lyrics (Higher quality & More providers)",
"default": false
"description": "Use Radiant Lyrics API to fetch Lyrics (Higher quality & More providers). Pick how the highlight moves: whole Line or word-by-word (Word).",
"default": false,
"defaultVariantIndex": 0,
"variants": [
{
"title": "Line",
"fileNames": []
},
{
"title": "Word",
"fileNames": [
"lyrics-rl-api-word.patch"
]
}
]
},
{
"id": "PlayerBackdrop",
@@ -144,7 +158,7 @@
"type": "toggle",
"key": "cover_everywhere",
"title": "Cover Everywhere - WIP",
"description": "Applies TIDAL's player backdrop to every Compose based page (Home & Collection)",
"description": "Applies TIDAL's native blurred album-art backdrop to every Compose based page (Home & Collection)",
"default": false,
"inline": true,
"fileNames": [
@@ -79,6 +79,8 @@ data class PatchOptions(
if (spec.variants.isEmpty()) {
if (!enabled) addAll(spec.fileNames)
} else {
// base fileNames apply for every variant while enabled; disable them when off
if (!enabled) addAll(spec.fileNames)
val selected = variantIndex(spec)
spec.variants.forEachIndexed { index, variant ->
if (!enabled || index != selected) addAll(variant.fileNames)
@@ -48,6 +48,7 @@ data class VariantSpec(
val title: String,
val fileNames: List<String> = emptyList(),
val extensionFiles: List<String> = emptyList(),
val enabled: Boolean = true,
)
/** A single advanced option. Discriminated by `"type"` in JSON. */
@@ -173,7 +174,7 @@ fun builtinPatchSpecs(context: Context, json: Json): List<PatchSpec> = try {
}
@Immutable
data class EffectiveVariant(val originalIndex: Int, val title: String)
data class EffectiveVariant(val originalIndex: Int, val title: String, val enabled: Boolean = true)
fun PatchSpec.effectiveVariants(isOptionOn: (OptionSpec.Toggle) -> Boolean): List<EffectiveVariant> {
if (variants.isEmpty()) return emptyList()
@@ -186,19 +187,22 @@ fun PatchSpec.effectiveVariants(isOptionOn: (OptionSpec.Toggle) -> Boolean): Lis
}
}
return variants.mapIndexedNotNull { index, variant ->
if (index in hidden) null else EffectiveVariant(index, relabel[index] ?: variant.title)
if (index in hidden) null
else EffectiveVariant(index, relabel[index] ?: variant.title, variant.enabled)
}
}
fun PatchSpec.resolveVariantIndex(stored: Int, isOptionOn: (OptionSpec.Toggle) -> Boolean): Int {
val visible = effectiveVariants(isOptionOn)
if (visible.isEmpty() || visible.any { it.originalIndex == stored }) return stored
// never resolve onto a greyed/disabled variant
if (visible.isEmpty() || visible.any { it.originalIndex == stored && it.enabled }) return stored
val relabelKeys = buildSet {
for (option in advancedOptions) {
if (option is OptionSpec.Toggle && isOptionOn(option)) addAll(option.relabelVariants.keys)
}
}
return visible.firstOrNull { it.originalIndex in relabelKeys }?.originalIndex
return visible.firstOrNull { it.originalIndex in relabelKeys && it.enabled }?.originalIndex
?: visible.firstOrNull { it.enabled }?.originalIndex
?: visible.first().originalIndex
}
@@ -169,15 +169,18 @@ fun PatchSelectionAccordion(
.fillMaxWidth()
.padding(bottom = 8.dp),
) {
val resolvedVariant =
if (patch.variants.isNotEmpty())
patch.resolveVariantIndex(variantIndex(patch)) {
optionState.toggle(patch, it)
}
else -1
if (patch.variants.isNotEmpty()) {
val effective = patch.effectiveVariants { optionState.toggle(patch, it) }
val resolved = patch.resolveVariantIndex(variantIndex(patch)) {
optionState.toggle(patch, it)
}
PatchVariantSelector(
variants = effective,
selectedIndex = effective
.indexOfFirst { it.originalIndex == resolved }
.indexOfFirst { it.originalIndex == resolvedVariant }
.coerceAtLeast(0),
onSelect = { pos ->
effective.getOrNull(pos)?.let { onSelectVariant(patch, it.originalIndex) }
@@ -186,12 +189,16 @@ fun PatchSelectionAccordion(
}
for (option in inlineToggles) key(option.key) {
InlineToggleRow(
title = option.title,
description = option.description,
checked = optionState.toggle(patch, option),
onCheckedChange = { optionState.setToggle(patch, option, it) },
)
val show = option.requiresVariant == null ||
option.requiresVariant == resolvedVariant
if (show) {
InlineToggleRow(
title = option.title,
description = option.description,
checked = optionState.toggle(patch, option),
onCheckedChange = { optionState.setToggle(patch, option, it) },
)
}
}
if (hasSheetOptions) {
@@ -22,7 +22,8 @@ fun PatchVariantSelector(
variants.forEachIndexed { index, variant ->
SegmentedButton(
selected = index == selectedIndex,
onClick = { onSelect(index) },
enabled = variant.enabled,
onClick = { if (variant.enabled) onSelect(index) },
shape = SegmentedButtonDefaults.itemShape(
index = index,
count = variants.size,
+20 -5
View File
@@ -2,13 +2,25 @@
A Manager app for Android that patches TIDAL to bring the Radiant Lyrics experience to mobile.
## What it does
## What it does?
- Blurred album art background in the player
- Progress pill overlay in lyrics mode - restored from old UI
- Hides the ugly cover art when in lyrics mode
This project adds ALLOT of completely Optional & Customisable Patches/Integrations to TIDAL on Android (iOS SoonTM).
Every Patch & Integration can be disabled/enabled & adjusted exactly how you want TIDAL to feel/look <3
### Patches:
- Insane Customisation of the Player Backdrop
- Higher quality lyrics from more providers (WIP)
- Progress pill overlay in lyrics mode (restored from old UI)
- Keeps lyrics controls visible
- One-handed player controls
- Redesigned mini-player with gestures
- Word Level Lyrics (Word by Word) <3
- Syllable Level lyrics (SoonTM)
### Integrations
- Waze | Control TIDAL playback from Waze
### Disclaimer
@@ -18,7 +30,10 @@ This project is not affiliated with TIDAL in any way. Use at your own risk.
## Installation
### Batteries <3
- Android 10.0+ & >340 DPI
- Phone of some unknown description
- The smallest amount of brain damage
- Android 7.0+ & >340 DPI
- Internet
- Paid TIDAL Account
### Steps
+1 -1
View File
@@ -1,5 +1,5 @@
{
"tidalVersionCode": 10004,
"tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk",
"patchesVersion": "0.11.1"
"patchesVersion": "0.12.0"
}
+61
View File
@@ -0,0 +1,61 @@
.class public final Lradiant/IsrcCache;
.super Ljava/lang/Object;
# Caches trackId(String) -> ISRC(String), captured from TIDAL's own track mapper
.field public static final map:Ljava/util/concurrent/ConcurrentHashMap;
.method static constructor <clinit>()V
.locals 1
new-instance v0, Ljava/util/concurrent/ConcurrentHashMap;
invoke-direct {v0}, Ljava/util/concurrent/ConcurrentHashMap;-><init>()V
sput-object v0, Lradiant/IsrcCache;->map:Ljava/util/concurrent/ConcurrentHashMap;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static put(Ljava/lang/String;Ljava/lang/String;)V
.locals 1
if-eqz p0, :ret
if-eqz p1, :ret
sget-object v0, Lradiant/IsrcCache;->map:Ljava/util/concurrent/ConcurrentHashMap;
invoke-virtual {v0, p0, p1}, Ljava/util/concurrent/ConcurrentHashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
:ret
return-void
.end method
.method public static get(Ljava/lang/String;)Ljava/lang/String;
.locals 2
const/4 v0, 0x0
if-eqz p0, :none
sget-object v1, Lradiant/IsrcCache;->map:Ljava/util/concurrent/ConcurrentHashMap;
invoke-virtual {v1, p0}, Ljava/util/concurrent/ConcurrentHashMap;->get(Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v0
check-cast v0, Ljava/lang/String;
:none
return-object v0
.end method
@@ -8,6 +8,8 @@
# stuff that makes lyrics view stays open.. i forgot what it actually does other than short circuit a call or some shit <3
.field public static lastMediaId:I
.field public static lastVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
# direct methods
.method static constructor <clinit>()V
@@ -17,5 +19,9 @@
sput v0, Lradiant/LyricsReinitGuard;->lastMediaId:I
const/4 v0, 0x0
sput-object v0, Lradiant/LyricsReinitGuard;->lastVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
return-void
.end method
+126 -19
View File
@@ -5,8 +5,12 @@
# static fields
.field public static volatile currentKey:Ljava/lang/String;
.field public static volatile currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
.field public static volatile isRlState:Z
.field public static volatile keepOpen:Z
# direct methods
.method static constructor <clinit>()V
@@ -16,8 +20,12 @@
sput-object v0, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-object v0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
sput-boolean v0, Lradiant/RLAPILyricsHook;->isRlState:Z
sput-boolean v0, Lradiant/RLAPILyricsHook;->keepOpen:Z
return-void
.end method
@@ -60,6 +68,45 @@
return v1
.end method
.method public static isOwner(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
.locals 2
sget-object v0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne p0, v0, :not_owner
sget-boolean v1, Lradiant/RLAPILyricsHook;->isRlState:Z
return v1
:not_owner
const/4 v1, 0x0
return v1
.end method
.method public static shouldKeepOpen(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
.locals 2
sget-object v0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne p0, v0, :do_not_keep
sget-boolean v1, Lradiant/RLAPILyricsHook;->keepOpen:Z
if-nez v1, :keep
sget-boolean v1, Lradiant/RLAPILyricsHook;->isRlState:Z
:keep
return v1
:do_not_keep
const/4 v1, 0x0
return v1
.end method
.method public static onWampTrack(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;Lcom/aspiro/wamp/model/Track;)V
.locals 11
@@ -80,7 +127,7 @@
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
goto :invalid_track
:have_track
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getTitle()Ljava/lang/String;
@@ -93,7 +140,7 @@
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
goto :invalid_track
:title_present
invoke-static {v1}, Lradiant/RLAPILyricsHook;->isBlank(Ljava/lang/String;)Z
@@ -106,20 +153,34 @@
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
goto :invalid_track
:title_ok
# match desktop: primary artist name (artist?.name), fall back to the joined list
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getArtist()Lcom/aspiro/wamp/model/Artist;
move-result-object v3
if-eqz v3, :use_artist_names
invoke-virtual {v3}, Lcom/aspiro/wamp/model/Artist;->getName()Ljava/lang/String;
move-result-object v2
if-nez v2, :artist_present
:use_artist_names
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getArtistNames()Ljava/lang/String;
move-result-object v2
if-nez v2, :artist_present
const-string v3, "bail: getArtistNames() returned null"
const-string v3, "bail: artist name null"
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
goto :invalid_track
:artist_present
invoke-static {v2}, Lradiant/RLAPILyricsHook;->isBlank(Ljava/lang/String;)Z
@@ -132,24 +193,14 @@
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
goto :invalid_track
:artist_ok
const-string v3, ""
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getId()I
new-instance v4, Ljava/lang/StringBuilder;
move-result v3
invoke-direct {v4}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v4, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v5, "|"
invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v4, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
invoke-static {v3}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v4
@@ -161,6 +212,10 @@
if-eqz v5, :rl_new_track
sget-object v5, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne p0, v5, :rl_new_track
const-string v5, "onWampTrack: same track re-fired, skipping duplicate fetch"
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
@@ -168,12 +223,51 @@
return-void
:rl_new_track
iget-object v5, p0, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
invoke-interface {v5}, Lkotlinx/coroutines/flow/MutableStateFlow;->getValue()Ljava/lang/Object;
move-result-object v5
check-cast v5, Ljava/lang/Boolean;
invoke-virtual {v5}, Ljava/lang/Boolean;->booleanValue()Z
move-result v5
if-nez v5, :store_keep_open
sget-object v6, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-eqz v6, :store_keep_open
if-eq p0, v6, :store_keep_open
iget-object v7, v6, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
invoke-interface {v7}, Lkotlinx/coroutines/flow/MutableStateFlow;->getValue()Ljava/lang/Object;
move-result-object v7
check-cast v7, Ljava/lang/Boolean;
invoke-virtual {v7}, Ljava/lang/Boolean;->booleanValue()Z
move-result v5
:store_keep_open
sput-boolean v5, Lradiant/RLAPILyricsHook;->keepOpen:Z
const/4 v5, 0x0
sput-boolean v5, Lradiant/RLAPILyricsHook;->isRlState:Z
sput-object v4, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-object p0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
move-object v3, v4
new-instance v5, Ljava/lang/StringBuilder;
const-string v6, "onWampTrack: fetching for title='"
@@ -222,6 +316,19 @@
invoke-virtual {v6}, Ljava/lang/Thread;->start()V
goto :done
:invalid_track
const/4 v3, 0x0
sput-boolean v3, Lradiant/RLAPILyricsHook;->isRlState:Z
sput-boolean v3, Lradiant/RLAPILyricsHook;->keepOpen:Z
sput-object v3, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-object p0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
:done
return-void
.end method
+371 -40
View File
@@ -14,6 +14,8 @@
.field public final lyricsId:Ljava/lang/String;
.field public static volatile clientIp:Ljava/lang/String;
# direct methods
.method public constructor <init>(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
@@ -34,6 +36,181 @@
return-void
.end method
.method private failCurrent()V
.locals 4
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isCurrent()Z
move-result v0
if-eqz v0, :done
invoke-static {}, Lradiant/WordLyrics;->clear()V
const/4 v0, 0x0
sput-object v0, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-boolean v0, Lradiant/RLAPILyricsHook;->isRlState:Z
sget-boolean v0, Lradiant/StickyLyrics;->enabled:Z
if-nez v0, :done
sget-boolean v0, Lradiant/RLAPILyricsHook;->keepOpen:Z
if-nez v0, :done
iget-object v1, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
iget-object v2, v1, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
sget-object v3, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;
invoke-interface {v2, v3}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
:done
return-void
.end method
.method private isCurrent()Z
.locals 4
iget-object v0, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
sget-object v1, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne v0, v1, :not_current
iget-object v1, v0, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->a:Lbi/e;
invoke-interface {v1}, Lbi/e;->getCurrentItem()Lcom/aspiro/wamp/playqueue/u0;
move-result-object v1
if-eqz v1, :not_current
invoke-interface {v1}, Lcom/aspiro/wamp/playqueue/u0;->getMediaItem()Lcom/aspiro/wamp/model/MediaItem;
move-result-object v1
if-eqz v1, :not_current
invoke-virtual {v1}, Lcom/aspiro/wamp/model/MediaItem;->getId()I
move-result v2
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v1
iget-object v2, p0, Lradiant/RLAPILyricsWorker;->key:Ljava/lang/String;
invoke-virtual {v2, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v3
return v3
:not_current
const/4 v3, 0x0
return v3
.end method
# Public client IP (needed for debugging with ratelimits)
.method static clientIp()Ljava/lang/String;
.locals 6
sget-object v0, Lradiant/RLAPILyricsWorker;->clientIp:Ljava/lang/String;
if-eqz v0, :fetch
return-object v0
:fetch
:try_start
new-instance v0, Ljava/net/URL;
const-string v1, "https://api.ipify.org?format=text"
invoke-direct {v0, v1}, Ljava/net/URL;-><init>(Ljava/lang/String;)V
invoke-virtual {v0}, Ljava/net/URL;->openConnection()Ljava/net/URLConnection;
move-result-object v0
check-cast v0, Ljava/net/HttpURLConnection;
const/16 v1, 0x5dc
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setConnectTimeout(I)V
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setReadTimeout(I)V
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getResponseCode()I
move-result v1
const/16 v2, 0xc8
if-eq v1, v2, :ok
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->disconnect()V
goto :fail
:ok
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getInputStream()Ljava/io/InputStream;
move-result-object v1
new-instance v2, Ljava/io/InputStreamReader;
const-string v3, "UTF-8"
invoke-direct {v2, v1, v3}, Ljava/io/InputStreamReader;-><init>(Ljava/io/InputStream;Ljava/lang/String;)V
new-instance v3, Ljava/io/BufferedReader;
invoke-direct {v3, v2}, Ljava/io/BufferedReader;-><init>(Ljava/io/Reader;)V
invoke-virtual {v3}, Ljava/io/BufferedReader;->readLine()Ljava/lang/String;
move-result-object v4
invoke-virtual {v3}, Ljava/io/BufferedReader;->close()V
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->disconnect()V
if-eqz v4, :fail
invoke-virtual {v4}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v4
invoke-virtual {v4}, Ljava/lang/String;->isEmpty()Z
move-result v5
if-nez v5, :fail
sput-object v4, Lradiant/RLAPILyricsWorker;->clientIp:Ljava/lang/String;
return-object v4
:try_end
.catchall {:try_start .. :try_end} :catch_all
:catch_all
move-exception v0
:fail
const-string v0, "null"
return-object v0
.end method
.method public static fetch(Ljava/lang/String;Z)Ljava/lang/String;
.locals 7
@@ -56,6 +233,8 @@
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setConnectTimeout(I)V
const v1, 0xafc8
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setReadTimeout(I)V
if-eqz p1, :no_auth
@@ -74,7 +253,9 @@
const-string v1, "x-client-ip"
const-string v2, "null"
invoke-static {}, Lradiant/RLAPILyricsWorker;->clientIp()Ljava/lang/String;
move-result-object v2
invoke-virtual {v0, v1, v2}, Ljava/net/URLConnection;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V
@@ -313,20 +494,168 @@
return-object v1
.end method
# Returns "&isrc=<enc>" for the current track
.method private isrcParam()Ljava/lang/String;
.locals 6
iget-object v0, p0, Lradiant/RLAPILyricsWorker;->key:Ljava/lang/String;
const/4 v1, 0x0
:loop
invoke-static {v0}, Lradiant/IsrcCache;->get(Ljava/lang/String;)Ljava/lang/String;
move-result-object v2
if-eqz v2, :retry
invoke-virtual {v2}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/String;->isEmpty()Z
move-result v3
if-nez v3, :retry
:try_enc_start
invoke-static {v2}, Lradiant/RLAPILyricsWorker;->uric(Ljava/lang/String;)Ljava/lang/String;
move-result-object v2
:try_enc_end
.catchall {:try_enc_start .. :try_enc_end} :enc_fail
:enc_done
new-instance v3, Ljava/lang/StringBuilder;
const-string v4, "&isrc="
invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v2
return-object v2
:enc_fail
move-exception v3
goto :enc_done
:retry
# keep waiting for TIDAL's mapper to cache the ISRC while this track is still current
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isCurrent()Z
move-result v3
if-eqz v3, :giveup
const/16 v3, 0x28
if-ge v1, v3, :giveup
:try_sleep_start
const-wide/16 v3, 0x64
invoke-static {v3, v4}, Ljava/lang/Thread;->sleep(J)V
:try_sleep_end
.catchall {:try_sleep_start .. :try_sleep_end} :sleep_catch
:sleep_done
add-int/lit8 v1, v1, 0x1
goto :loop
:sleep_catch
move-exception v3
goto :sleep_done
:giveup
const-string v0, ""
return-object v0
.end method
# URL-encode like JS encodeURIComponent (so requests are byte identical to the desktop plugin)
.method static uric(Ljava/lang/String;)Ljava/lang/String;
.locals 3
const-string v0, "UTF-8"
invoke-static {p0, v0}, Ljava/net/URLEncoder;->encode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
const-string v1, "+"
const-string v2, "%20"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%21"
const-string v2, "!"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%27"
const-string v2, "\'"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%28"
const-string v2, "("
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%29"
const-string v2, ")"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%7E"
const-string v2, "~"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
return-object v0
.end method
.method private runImpl()V
.locals 11
.locals 13
iget-object v0, p0, Lradiant/RLAPILyricsWorker;->title:Ljava/lang/String;
const-string v1, "UTF-8"
invoke-static {v0, v1}, Ljava/net/URLEncoder;->encode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
invoke-static {v0}, Lradiant/RLAPILyricsWorker;->uric(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
iget-object v2, p0, Lradiant/RLAPILyricsWorker;->artist:Ljava/lang/String;
invoke-static {v2, v1}, Ljava/net/URLEncoder;->encode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
invoke-static {v2}, Lradiant/RLAPILyricsWorker;->uric(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
@@ -344,6 +673,12 @@
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isrcParam()Ljava/lang/String;
move-result-object v0
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v0, "&platform=rl-mobile"
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
@@ -396,32 +731,11 @@
invoke-static {v4}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
iget-object v4, p0, Lradiant/RLAPILyricsWorker;->key:Ljava/lang/String;
sget-object v5, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
invoke-virtual {v4, v5}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v4
if-eqz v4, :no_close # Smthn needed to keep lyrics open
sget-boolean v4, Lradiant/StickyLyrics;->enabled:Z
if-nez v4, :no_close
iget-object v4, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
iget-object v5, v4, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
sget-object v6, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;
invoke-interface {v5, v6}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
:no_close
return-void
goto :request_failed
:got_body
move-object v11, v3
invoke-static {v3}, Lradiant/RLAPILyricsWorker;->parseLines(Ljava/lang/String;)Ljava/util/ArrayList;
move-result-object v3
@@ -432,7 +746,7 @@
invoke-static {v4}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
goto :request_failed
:parse_ok
invoke-virtual {v3}, Ljava/util/ArrayList;->size()I
@@ -445,7 +759,7 @@
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
goto :request_failed
:nonempty
new-instance v5, Ljava/lang/StringBuilder;
@@ -466,11 +780,7 @@
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
iget-object v4, p0, Lradiant/RLAPILyricsWorker;->key:Ljava/lang/String;
sget-object v5, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
invoke-virtual {v4, v5}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isCurrent()Z
move-result v4
@@ -483,6 +793,8 @@
return-void
:key_ok
invoke-static {v11}, Lradiant/WordLyrics;->ingest(Ljava/lang/String;)V
iget-object v4, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
iget-object v5, v4, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->O:Lkotlinx/coroutines/flow/MutableStateFlow;
@@ -512,6 +824,8 @@
const/4 v6, 0x1
invoke-interface {v5, v8}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
sput-boolean v6, Lradiant/RLAPILyricsHook;->isRlState:Z
iget-object v6, v4, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->K:Lkotlinx/coroutines/flow/MutableStateFlow;
@@ -520,25 +834,34 @@
invoke-interface {v6, v7}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
sget-boolean v9, Lradiant/RLAPILyricsHook;->keepOpen:Z
if-nez v9, :open_lyrics
sget-boolean v9, Lradiant/StickyLyrics;->enabled:Z
if-eqz v9, :skip_n
:open_lyrics
iget-object v6, v4, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
invoke-interface {v6, v7}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
:skip_n
invoke-interface {v5, v8}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
:done
return-void
:request_failed
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->failCurrent()V
return-void
.end method
# virtual methods
.method public run()V
.locals 1
.locals 2
:try_start
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->runImpl()V
@@ -551,5 +874,13 @@
:catch_all
move-exception v0
invoke-virtual {v0}, Ljava/lang/Throwable;->toString()Ljava/lang/String;
move-result-object v1
invoke-static {v1}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->failCurrent()V
return-void
.end method
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
.class public final Lradiant/WordLyricsTick;
.super Ljava/lang/Object;
.implements Ljava/lang/Runnable;
# 50ms clock tick driver for word level lyrics highlighting
.method public constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public run()V
.locals 0
invoke-static {}, Lradiant/WordLyrics;->tick()V
return-void
.end method
+7 -2
View File
@@ -11,13 +11,18 @@
.line 28
--- a/com/tidal/android/feature/playerscreen/ui/PlayerViewModel.smali
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerViewModel.smali
@@ -1382,6 +1382,14 @@
@@ -1382,6 +1382,19 @@
.line 109
.line 110
move-result v1
+ sget v4, Lradiant/LyricsReinitGuard;->lastMediaId:I
+ if-eq v1, v4, :rl_skip_reinit # same track re-emitted -> skip the redundant re-init (avoids the loading-state flash)
+ if-ne v1, v4, :rl_reinit_store
+ sget-object v4, Lradiant/LyricsReinitGuard;->lastVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
+ if-ne v0, v4, :rl_reinit_store
+ goto :rl_skip_reinit # only suppress duplicate initialization on the same ViewModel
+ :rl_reinit_store
+ sput v1, Lradiant/LyricsReinitGuard;->lastMediaId:I
+ sput-object v0, Lradiant/LyricsReinitGuard;->lastVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
+ goto :rl_reinit_ok
+ :rl_skip_reinit
+ sget-object v4, Lkotlin/u;->a:Lkotlin/u;
+26
View File
@@ -0,0 +1,26 @@
--- a/cg0/h.smali
+++ b/cg0/h.smali
@@ -363,6 +363,23 @@
.method public static final d(Lcom/tidal/sdk/tidalapi/generated/models/TracksResourceObject;Ljava/util/ArrayList;Ljava/util/ArrayList;Ljava/util/ArrayList;Ljava/util/ArrayList;Ljava/util/ArrayList;Ljava/util/ArrayList;Ljava/util/Map;)Lcom/tidal/android/tidalapi/domain/model/o;
.locals 23
+ invoke-virtual/range {p0 .. p0}, Lcom/tidal/sdk/tidalapi/generated/models/TracksResourceObject;->getAttributes()Lcom/tidal/sdk/tidalapi/generated/models/TracksAttributes;
+
+ move-result-object v1
+
+ if-eqz v1, :rl_isrc_skip
+
+ invoke-virtual/range {p0 .. p0}, Lcom/tidal/sdk/tidalapi/generated/models/TracksResourceObject;->getId()Ljava/lang/String;
+
+ move-result-object v0
+
+ invoke-virtual {v1}, Lcom/tidal/sdk/tidalapi/generated/models/TracksAttributes;->getIsrc()Ljava/lang/String;
+
+ move-result-object v1
+
+ invoke-static {v0, v1}, Lradiant/IsrcCache;->put(Ljava/lang/String;Ljava/lang/String;)V
+
+ :rl_isrc_skip
move-object/from16 v0, p4
move-object/from16 v1, p7
+13 -4
View File
@@ -1,14 +1,23 @@
--- a/com/tidal/android/feature/playerscreen/ui/PlayerViewModel$observeLyricsProgress$1$a.smali
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerViewModel$observeLyricsProgress$1$a.smali
@@ -191,6 +191,13 @@
@@ -58,6 +58,8 @@
.line 5
int-to-long p1, p1
+ invoke-static {p1, p2}, Lradiant/WordLyrics;->onProgress(J)V
+
.line 6
iget-object v0, p0, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel$observeLyricsProgress$1$a;->a:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
@@ -191,6 +193,13 @@
.line 67
:cond_3
+ sget-boolean v6, Lradiant/RLAPILyricsHook;->isRlState:Z # read RL flag
+ sget-boolean v6, Lradiant/RLAPILyricsHook;->isRlState:Z
+
+ if-eqz v6, :radiant_skip # skip if not RL
+ if-eqz v6, :radiant_skip
+
+ const/4 v5, -0x1 # RL no-match default = -1
+ const/4 v5, -0x1
+
+ :radiant_skip
if-gez v4, :cond_4
+56
View File
@@ -0,0 +1,56 @@
--- a/com/tidal/android/feature/playerscreen/ui/composables/f2.smali
+++ b/com/tidal/android/feature/playerscreen/ui/composables/f2.smali
@@ -87,7 +87,7 @@
# virtual methods
.method public final invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
- .locals 28
+ .locals 40
.line 1
move-object/from16 v0, p0
@@ -313,6 +313,8 @@
.line 100
check-cast v1, Lcom/tidal/android/feature/playerscreen/ui/f;
+ move/16 v33, v2
+
.line 101
.line 102
const v3, 0x6b82e849
@@ -801,6 +803,26 @@
.line 331
iget-object v3, v1, Lcom/tidal/android/feature/playerscreen/ui/f;->a:Ljava/lang/String;
+ iget v4, v0, Lcom/tidal/android/feature/playerscreen/ui/composables/f2;->b:I
+
+ move/16 v34, v4
+
+ iget-object v4, v1, Lcom/tidal/android/feature/playerscreen/ui/f;->a:Ljava/lang/String;
+
+ move-object/16 v35, v4
+
+ iget-wide v4, v0, Lcom/tidal/android/feature/playerscreen/ui/composables/f2;->d:J
+
+ move-wide/16 v36, v4
+
+ iget-wide v4, v0, Lcom/tidal/android/feature/playerscreen/ui/composables/f2;->e:J
+
+ move-wide/16 v38, v4
+
+ invoke-static/range {v33 .. v39}, Lradiant/WordLyrics;->render(IILjava/lang/String;JJ)Landroidx/compose/ui/text/AnnotatedString;
+
+ move-result-object v3
+
.line 332
.line 333
invoke-interface {v2}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object;
@@ -898,7 +920,7 @@
.line 377
.line 378
- invoke-static/range {v3 .. v27}, Landroidx/compose/material3/TextKt;->Text--4IGK_g(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextAlign;JIZIILam0/l;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/runtime/Composer;III)V
+ invoke-static/range {v3 .. v27}, Landroidx/compose/material3/TextKt;->Text--4IGK_g(Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextAlign;JIZILjava/util/Map;Lam0/l;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/runtime/Composer;III)V
.line 379
.line 380
+25 -14
View File
@@ -9,43 +9,53 @@
.line 106
.line 107
invoke-virtual {v1}, Lcom/aspiro/wamp/model/MediaItem;->getId()I
@@ -1655,7 +1657,7 @@
@@ -1655,7 +1657,11 @@
.line 239
.line 240
.line 241
- if-nez v9, :cond_a
+ goto :cond_a # skip TIDAL P=false in the has-lyrics path (for some reason)
+ invoke-static {v0}, Lradiant/RLAPILyricsHook;->shouldKeepOpen(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
+ move-result v20
+ if-nez v20, :cond_a # keep visibility only for RL state owned by this ViewModel
+ :rl_native_visibility
+ if-nez v9, :cond_a
.line 242
.line 243
@@ -2347,7 +2349,10 @@
@@ -2347,7 +2353,12 @@
.line 572
:goto_c
+ sget-boolean v5, Lradiant/RLAPILyricsHook;->isRlState:Z
+ if-nez v5, :rl_keep_n2 # RL active -> keep worker content
+ invoke-static {v0}, Lradiant/RLAPILyricsHook;->isOwner(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
+ move-result v20
+ if-nez v20, :rl_keep_n2 # only preserve RL content owned by this ViewModel
+ :rl_set_n2
invoke-interface {v9, v1}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
+ :rl_keep_n2
.line 573
.line 574
@@ -2396,7 +2401,10 @@
@@ -2396,7 +2407,12 @@
.line 595
.line 596
+ sget-boolean v7, Lradiant/RLAPILyricsHook;->isRlState:Z
+ if-nez v7, :rl_keep_o # RL active -> keep view-open flag
+ invoke-static {v0}, Lradiant/RLAPILyricsHook;->shouldKeepOpen(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
+ move-result v20
+ if-nez v20, :rl_keep_o # only preserve visibility owned by this ViewModel
+ :rl_set_o
invoke-interface {v1, v5}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
+ :rl_keep_o
.line 597
.line 598
@@ -2425,7 +2433,10 @@
@@ -2425,7 +2441,12 @@
.line 610
.line 611
+ sget-boolean v7, Lradiant/RLAPILyricsHook;->isRlState:Z
+ if-nez v7, :rl_keep_n # RL active -> keep worker content
+ invoke-static {v0}, Lradiant/RLAPILyricsHook;->isOwner(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
+ move-result v20
+ if-nez v20, :rl_keep_n # only preserve RL content owned by this ViewModel
+ :rl_set_n
invoke-interface {v1, v6}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
+ :rl_keep_n
@@ -53,12 +63,13 @@
.line 613
--- a/com/tidal/android/feature/playerscreen/ui/PlayerViewModel$special$$inlined$mapNotNull$1$2.smali
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerViewModel$special$$inlined$mapNotNull$1$2.smali
@@ -2256,6 +2256,10 @@
@@ -2256,6 +2256,11 @@
.line 1040
.line 1041
move-result v34
+ sget-boolean v4, Lradiant/RLAPILyricsHook;->isRlState:Z
+ if-eqz v4, :rl_hl_done # not RL = use TIDAL's hasLyrics
+ invoke-static {v1}, Lradiant/RLAPILyricsHook;->isOwner(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
+ move-result v4
+ if-eqz v4, :rl_hl_done # ignore non-RL state or state owned by an obsolete ViewModel
+ const/16 v34, 0x1 # RL lyrics active = force hasLyrics so the sparkle stays
+ :rl_hl_done
+17 -3
View File
@@ -32,11 +32,25 @@
"order": 20,
"fileNames": [
"lyrics-rl-api.patch",
"lyrics-rl-api-observer.patch"
"lyrics-rl-api-observer.patch",
"lyrics-rl-api-isrc.patch"
],
"title": "Radiant Lyrics API - WIP",
"description": "Use Radiant Lyrics API to fetch Lyrics (Higher quality & More providers)",
"default": false
"description": "Use Radiant Lyrics API to fetch Lyrics (Higher quality & More providers). Pick how the highlight moves: whole Line or word-by-word (Word).",
"default": false,
"defaultVariantIndex": 0,
"variants": [
{
"title": "Line",
"fileNames": []
},
{
"title": "Word",
"fileNames": [
"lyrics-rl-api-word.patch"
]
}
]
},
{
"id": "PlayerBackdrop",