diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d74f509..44fde7f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -4,14 +4,82 @@ jobs:
build:
name: Build APK
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- - uses: actions/checkout@v1
- - uses: actions/setup-java@v1
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
with:
- java-version: '12.x'
- - uses: subosito/flutter-action@v1
+ distribution: 'zulu'
+ java-version: '17'
+ - uses: subosito/flutter-action@v2
with:
- flutter-version: '1.20.1'
- - run: flutter build apk --release --target-platform android-arm,android-arm64,android-x64
- - name: Create a Release APK
- run: curl -F chat_id="-467829930" -F caption="$(git log -1)" -F document=@"$PWD/build/app/outputs/apk/release/app-release.apk" https://ci.sumanjay.ga/
+ flutter-version: '3.35.6'
+ channel: 'stable'
+ - run: flutter pub get
+ - run: flutter build apk --release --split-per-abi
+ - name: Upload Universal APK Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-universal-release
+ path: build/app/outputs/flutter-apk/app-release.apk
+ retention-days: 720
+ - name: Upload ARM64 APK Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-arm64-v8a-release
+ path: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
+ retention-days: 720
+ - name: Upload ARMv7 APK Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-armeabi-v7a-release
+ path: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
+ retention-days: 720
+ - name: Upload x86_64 APK Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-x86_64-release
+ path: build/app/outputs/flutter-apk/app-x86_64-release.apk
+ retention-days: 720
+ - name: Create GitHub Release
+ uses: ncipollo/release-action@v1
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ with:
+ artifacts: "build/app/outputs/flutter-apk/*.apk"
+ tag: v1.0.${{ github.run_number }}
+ name: Release v1.0.${{ github.run_number }}
+ body: |
+ ## 🎵 Musify Release
+
+ **Commit:** ${{ github.event.head_commit.message }}
+
+ ### Features:
+ - 🎵 Background playback with notifications
+ - 🔒 Lock screen controls
+ - 📱 Album queue system
+ - 🔁 Loop/repeat functionality
+ - ⬇️ Song download with permissions
+ - 📐 Responsive UI for all screen sizes
+ - 🚀 Dart 3 Compatible
+
+ ### Available APKs:
+
+ **📦 Universal APK** (`app-release.apk`) - ~65MB
+ - Works on all devices (recommended if unsure)
+
+ **📱 ARM64 APK** (`app-arm64-v8a-release.apk`) - ~23MB
+ - For modern devices (2019+): Pixel, Samsung Galaxy S10+, OnePlus 7+, etc.
+
+ **📱 ARMv7 APK** (`app-armeabi-v7a-release.apk`) - ~21MB
+ - For older devices: Samsung Galaxy S6-S9, older budget phones
+
+ **💻 x86_64 APK** (`app-x86_64-release.apk`) - ~25MB
+ - For emulators and Intel-based devices
+
+ ### How to choose:
+ - **Not sure?** → Download Universal APK
+ - **Want smaller size?** → Check your device's CPU architecture in Settings > About Phone
+ token: ${{ secrets.GITHUB_TOKEN }}
+ allowUpdates: true
+ makeLatest: true
diff --git a/README.md b/README.md
index 9dd3ede..bd5fcd6 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
-
+
Musify
Music Streaming and Downloading app made in Flutter!
-Show some :heart: and :star: the Repo
+Show some ❤️ and ⭐ the Repo
---
-[](https://flutter.dev/)     
+[](https://flutter.dev/)   
---
@@ -15,29 +15,117 @@
Online Song Search :mag:
Streaming Support :musical_note:
+ Background Playback :headphones:
+ Lock Screen Controls :lock:
Offline Download Support :arrow_down:
- 320Kbps m4a/mp3 Format :fire:
+ 320Kbps m4a Format :fire:
ID3 Tags Attached :notes:
Lyrics Support :pencil:
+ Loop/Repeat Mode :repeat:
+ Album Queue System :cd:
+ Responsive UI :iphone:
---
-
Screenshots
+ Download
+ 
+
+---
+
+ Which APK Should I Download?
- Homepage | Now Playing
-:-------------------------:|:-------------------------:
- | 
+
+ We provide multiple APK versions to give you the best experience! 🎯
+
+
+
+ | APK Type |
+ File Size |
+ Best For |
+
+
+ 📦 Universal APK
app-release.apk |
+ ~61 MB |
+
+ ✅ Recommended for most users
+ • Works on ALL Android devices
+ • Don't know your phone's specs? Choose this!
+ • Slightly larger but guaranteed to work
+ |
+
+
+ 📱 ARM64 APK
app-arm64-v8a-release.apk |
+ ~23 MB |
+
+ ✅ For Modern Phones (2019 onwards)
+ • Samsung Galaxy S10, S20, S21, S22, S23, S24
+ • Google Pixel 3, 4, 5, 6, 7, 8
+ • OnePlus 7, 8, 9, 10, 11, 12
+ • Xiaomi Redmi Note 8 Pro and newer
+ • Realme 5 Pro and newer
+ • Most phones from 2019 or later
+ |
+
+
+ 📱 ARMv7 APK
app-armeabi-v7a-release.apk |
+ ~21 MB |
+
+ ✅ For Older Phones (2014-2019)
+ • Samsung Galaxy S6, S7, S8, S9
+ • Older budget smartphones
+ • Most 32-bit Android devices
+ |
+
+
+ 💻 x86_64 APK
app-x86_64-release.apk |
+ ~25 MB |
+
+ ✅ For Special Cases
+ • Android emulators (BlueStacks, etc.)
+ • Intel/AMD based Android devices (rare)
+ • Some tablets
+ |
+
+
- Lyrics | About
-:-------------------------:|:-------------------------:
- | 
+ 🤔 How to Check Your Phone's Architecture
+
+ Method 1 (Easy): Just download the Universal APK - it works on everything!
+ Method 2 (For smaller file size):
+ 1. Install CPU-Z app from Play Store
+ 2. Open it and check the "SoC" or "CPU" tab
+ 3. Look for the architecture (ARM64/ARMv7/x86)
+ Method 3 (Settings):
+ Go to Settings → About Phone → Look for "Processor" or "Chipset"
+
+
+ 💡 Quick Decision Guide
+
+ 🟢 Phone bought after 2019? → Download ARM64 APK
+ 🟡 Phone bought between 2014-2019? → Download ARMv7 APK
+ 🔵 Not sure or installation fails? → Download Universal APK
+ 🟣 Using emulator? → Download x86_64 APK
+
---
- Download
- 
+ Credits
+
+ This project is a fork of Musify by Harsh-23.
+ Special thanks to the original author and all contributors for their work.
+ Enhancements in this fork:
+ • Background playback with audio_service
+ • Lock screen & notification controls
+ • Album-based queue system
+ • Loop/Repeat functionality
+ • Responsive UI for all screen sizes
+ • Updated dependencies & build system
+ • Dart 3.0 Compatibility
+ • and many more..
+
---
+
License
- Licensed under GPL-3.0 License
+ Licensed under GPL-3.0 License
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 58881a2..f8a3bb7 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,3 +1,9 @@
+plugins {
+ id "com.android.application"
+ id "org.jetbrains.kotlin.android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -21,44 +27,65 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
android {
- compileSdkVersion 28
+ namespace 'me.musify'
+ compileSdk 36
- sourceSets {
- main.java.srcDirs += 'src/main/kotlin'
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = '11'
+ freeCompilerArgs += ['-Xlint:-options']
+ }
+
+ // Suppress Java compilation warnings for obsolete options
+ tasks.withType(JavaCompile) {
+ options.compilerArgs += ['-Xlint:-options']
}
- lintOptions {
- disable 'InvalidPackage'
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "me.musify"
- minSdkVersion 16
- targetSdkVersion 28
+ minSdkVersion flutter.minSdkVersion // Required for audio_service
+ targetSdkVersion 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
+ // Enable APK splits by ABI to generate separate APKs for different architectures
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
+ universalApk true // Generate universal APK as well
+ }
+ }
+
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
+
+ // Disable minification - audio_service notifications don't work with R8/ProGuard
+ // This ensures background playback and lock screen controls work properly
+ minifyEnabled false
shrinkResources false
+
+ // ProGuard rules kept for future reference if needed
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
-flutter {
- source '../..'
-}
-
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0"
}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 0000000..83f7802
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1,152 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Critical attributes for reflection and annotations
+-keepattributes *Annotation*
+-keepattributes Signature
+-keepattributes InnerClasses
+-keepattributes EnclosingMethod
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Flutter specific rules
+-keep class io.flutter.app.** { *; }
+-keep class io.flutter.plugin.** { *; }
+-keep class io.flutter.util.** { *; }
+-keep class io.flutter.view.** { *; }
+-keep class io.flutter.** { *; }
+-keep class io.flutter.plugins.** { *; }
+-dontwarn io.flutter.embedding.**
+
+# Flutter embedding
+-keep class io.flutter.embedding.** { *; }
+
+# audio_service plugin (CRITICAL - prevent R8 from stripping these classes)
+-keep class com.ryanheise.audioservice.** { *; }
+-keepclassmembers class com.ryanheise.audioservice.** { *; }
+-dontwarn com.ryanheise.audioservice.**
+
+# Keep the AudioServiceActivity specifically
+-keep public class * extends com.ryanheise.audioservice.AudioServiceActivity
+
+# MediaSession and MediaBrowser (required for notifications and lock screen)
+-keep class android.support.v4.media.** { *; }
+-keep interface android.support.v4.media.** { *; }
+-keep class androidx.media.** { *; }
+-keep interface androidx.media.** { *; }
+-keepclassmembers class androidx.media.** { *; }
+-dontwarn android.support.v4.media.**
+-dontwarn androidx.media.**
+
+# MediaSession compatibility
+-keep class androidx.media.session.** { *; }
+-keep class android.support.v4.media.session.** { *; }
+
+# Keep all Service classes (critical for background playback)
+-keep public class * extends android.app.Service
+-keep public class * extends androidx.media.MediaBrowserServiceCompat
+
+# Keep BroadcastReceiver for media buttons
+-keep public class * extends android.content.BroadcastReceiver
+
+# Keep MainActivity (extends AudioServiceActivity)
+-keep class me.musify.MainActivity { *; }
+
+# Keep all classes referenced in AndroidManifest.xml
+-keep class * extends android.app.Activity
+-keepclassmembers class * extends android.app.Activity {
+ public void *(android.view.View);
+}
+
+# just_audio plugin
+-keep class com.ryanheise.just_audio.** { *; }
+-dontwarn com.ryanheise.just_audio.**
+
+# ExoPlayer (used by just_audio)
+-keep class com.google.android.exoplayer2.** { *; }
+-dontwarn com.google.android.exoplayer2.**
+
+# Notification and notification channels (Android 8.0+)
+-keep class android.app.Notification { *; }
+-keep class android.app.NotificationChannel { *; }
+-keep class android.app.NotificationManager { *; }
+-keep class androidx.core.app.NotificationCompat { *; }
+-keep class androidx.core.app.NotificationCompat$* { *; }
+
+# Keep notification builder and style classes
+-keep class androidx.core.app.NotificationCompat$Builder { *; }
+-keep class androidx.core.app.NotificationCompat$MediaStyle { *; }
+-keep class androidx.media.app.NotificationCompat$MediaStyle { *; }
+
+# HTTP and networking
+-keep class okhttp3.** { *; }
+-keep class retrofit2.** { *; }
+-dontwarn okhttp3.**
+-dontwarn retrofit2.**
+
+# Keep native methods
+-keepclasseswithmembernames class * {
+ native ;
+}
+
+# Keep enums
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+# Keep Parcelable implementations
+-keep class * implements android.os.Parcelable {
+ public static final android.os.Parcelable$Creator *;
+}
+
+# Keep Serializable classes
+-keepnames class * implements java.io.Serializable
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ private static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+# Remove logging
+-assumenosideeffects class android.util.Log {
+ public static boolean isLoggable(java.lang.String, int);
+ public static int v(...);
+ public static int i(...);
+ public static int w(...);
+ public static int d(...);
+ public static int e(...);
+}
+
+# Flutter embedding
+-keep class io.flutter.embedding.** { *; }
+
+# Gson (if used)
+-keepattributes Signature
+-keepattributes *Annotation*
+-dontwarn sun.misc.**
+-keep class com.google.gson.** { *; }
+-keep class * implements com.google.gson.TypeAdapterFactory
+-keep class * implements com.google.gson.JsonSerializer
+-keep class * implements com.google.gson.JsonDeserializer
+
+# General optimizations
+-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
+-optimizationpasses 5
+-allowaccessmodification
+-dontpreverify
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-verbose
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fd32be2..64bbb75 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,11 +1,23 @@
-
+
-
-
+
+
-
+
+
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/me/musify/MainActivity.kt b/android/app/src/main/kotlin/me/musify/MainActivity.kt
index 2aa6a42..7c11492 100644
--- a/android/app/src/main/kotlin/me/musify/MainActivity.kt
+++ b/android/app/src/main/kotlin/me/musify/MainActivity.kt
@@ -1,6 +1,6 @@
package me.musify
-import io.flutter.embedding.android.FlutterActivity
+import com.ryanheise.audioservice.AudioServiceActivity
-class MainActivity: FlutterActivity() {
+class MainActivity: AudioServiceActivity() {
}
diff --git a/android/app/src/main/res/drawable/ic_action_pause.xml b/android/app/src/main/res/drawable/ic_action_pause.xml
new file mode 100644
index 0000000..d0a7951
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_action_pause.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_action_play_arrow.xml b/android/app/src/main/res/drawable/ic_action_play_arrow.xml
new file mode 100644
index 0000000..c85bcf3
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_action_play_arrow.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_action_skip_next.xml b/android/app/src/main/res/drawable/ic_action_skip_next.xml
new file mode 100644
index 0000000..0406e1c
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_action_skip_next.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_action_skip_previous.xml b/android/app/src/main/res/drawable/ic_action_skip_previous.xml
new file mode 100644
index 0000000..a5d7ca7
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_action_skip_previous.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_action_stop.xml b/android/app/src/main/res/drawable/ic_action_stop.xml
new file mode 100644
index 0000000..172430c
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_action_stop.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_notification.xml b/android/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..30e509a
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
index 3100ad2..ad89b89 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,20 +1,7 @@
-buildscript {
- ext.kotlin_version = '1.3.50'
- repositories {
- google()
- jcenter()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:3.5.0'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
@@ -26,6 +13,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
-task clean(type: Delete) {
- delete rootProject.buildDir
+tasks.register("clean", Delete) {
+ delete rootProject.layout.buildDirectory
}
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 296b146..3c85cfe 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,5 @@
-#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
index 5a2f14f..161f9ed 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,15 +1,25 @@
-include ':app'
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }()
-def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-def plugins = new Properties()
-def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
-if (pluginsFile.exists()) {
- pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
}
-plugins.each { name, path ->
- def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
- include ":$name"
- project(":$name").projectDir = pluginDirectory
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "8.6.1" apply false
+ id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
+
+include ':app'
diff --git a/assets/image.png b/assets/image.png
new file mode 100644
index 0000000..3e6c6f6
Binary files /dev/null and b/assets/image.png differ
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py
new file mode 100644
index 0000000..a88caf9
--- /dev/null
+++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py
@@ -0,0 +1,32 @@
+#
+# Generated file, do not edit.
+#
+
+import lldb
+
+def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
+ """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
+ base = frame.register["x0"].GetValueAsAddress()
+ page_len = frame.register["x1"].GetValueAsUnsigned()
+
+ # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
+ # first page to see if handled it correctly. This makes diagnosing
+ # misconfiguration (e.g. missing breakpoint) easier.
+ data = bytearray(page_len)
+ data[0:8] = b'IHELPED!'
+
+ error = lldb.SBError()
+ frame.GetThread().GetProcess().WriteMemory(base, data, error)
+ if not error.Success():
+ print(f'Failed to write into {base}[+{page_len}]', error)
+ return
+
+def __lldb_init_module(debugger: lldb.SBDebugger, _):
+ target = debugger.GetDummyTarget()
+ # Caveat: must use BreakpointCreateByRegEx here and not
+ # BreakpointCreateByName. For some reasons callback function does not
+ # get carried over from dummy target for the later.
+ bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
+ bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
+ bp.SetAutoContinue(True)
+ print("-- LLDB integration loaded --")
diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit
new file mode 100644
index 0000000..e3ba6fb
--- /dev/null
+++ b/ios/Flutter/ephemeral/flutter_lldbinit
@@ -0,0 +1,5 @@
+#
+# Generated file, do not edit.
+#
+
+command script import --relative-to-command-file flutter_lldb_helper.py
diff --git a/lib/API/des_helper.dart b/lib/API/des_helper.dart
new file mode 100644
index 0000000..c18952c
--- /dev/null
+++ b/lib/API/des_helper.dart
@@ -0,0 +1,134 @@
+import 'dart:convert';
+import 'package:flutter/material.dart';
+import 'package:dart_des/dart_des.dart';
+
+/// DES helper class that uses dart_des (pyDES port) for exact Python behavior
+class DESHelper {
+ static const String _key = "38346591";
+
+ /// Main decryption function using dart_des (pyDES port) - exactly matching Python behavior
+ static String decryptUrl(String encryptedUrl) {
+ try {
+ if (encryptedUrl.isEmpty) return "";
+
+ debugPrint('🔐 DES Decrypting with dart_des (pyDES port): $encryptedUrl');
+
+ // Step 1: Base64 decode (matching Python base64.b64decode)
+ List encryptedBytes;
+ try {
+ encryptedBytes = base64.decode(encryptedUrl);
+ debugPrint('🔍 Base64 decoded ${encryptedBytes.length} bytes');
+ } catch (e) {
+ debugPrint('❌ Base64 decode failed: $e');
+ return "";
+ }
+
+ // Step 2: Use dart_des (pyDES port) for exact Python behavior
+ try {
+ // Create DES instance exactly like Python: pyDes.des("38346591", pyDes.ECB, pad=None, padmode=pyDes.PAD_PKCS5)
+ DES desDecryptor = DES(
+ key: _key.codeUnits, // "38346591" as bytes
+ mode: DESMode.ECB, // ECB mode like Python
+ // Note: dart_des handles padding automatically
+ );
+
+ debugPrint('🔧 DES decryptor initialized (ECB mode, key: $_key)');
+
+ // Decrypt the bytes (exactly like Python des_cipher.decrypt())
+ List decryptedBytes = desDecryptor.decrypt(encryptedBytes);
+
+ debugPrint(
+ '✅ DES decryption successful, got ${decryptedBytes.length} bytes');
+
+ // Convert to string and extract URL
+ String decryptedText =
+ utf8.decode(decryptedBytes, allowMalformed: true);
+
+ debugPrint(
+ '🔍 Decrypted text: ${decryptedText.length > 100 ? decryptedText.substring(0, 100) : decryptedText}...');
+
+ // Apply transformations (as per your Python helper.py)
+ String processedUrl = _extractValidUrl(decryptedText, "dart_des DES");
+
+ if (processedUrl.isNotEmpty) {
+ // Apply quality transformation
+ processedUrl = processedUrl.replaceAll("_96.mp4", "_320.mp4");
+ debugPrint('✅ Successfully decrypted URL: $processedUrl');
+ return processedUrl;
+ }
+ } catch (e) {
+ debugPrint('❌ dart_des DES decryption failed: $e');
+ }
+
+ } catch (e) {
+ debugPrint('❌ DES decryption failed: $e');
+ return "";
+ }
+ return "";
+ }
+
+ /// Extract valid URL from decrypted text
+ static String _extractValidUrl(String text, String approach) {
+ try {
+ if (text.isEmpty) return "";
+
+ debugPrint(
+ '🔍 $approach checking: ${text.length > 50 ? text.substring(0, 50) : text}...');
+
+ // Clean the text first
+ text = text.replaceAll(
+ RegExp(r'[^\x20-\x7E]'), ''); // Remove non-printable chars
+
+ // Look for complete HTTP URLs
+ RegExp httpPattern =
+ RegExp(r'https?://[^\s]+\.mp4', caseSensitive: false);
+ Match? httpMatch = httpPattern.firstMatch(text);
+
+ if (httpMatch != null) {
+ String url = httpMatch.group(0)!;
+ debugPrint('✅ $approach found HTTP URL: $url');
+ return url;
+ }
+
+ // Look for saavncdn patterns and construct URL
+ if (text.contains('saavncdn') || text.contains('saavn')) {
+ debugPrint('🔍 $approach found saavn pattern, extracting...');
+
+ // Try to extract path parts
+ List parts = text.split(RegExp(r'[\s\x00-\x1F\x7F-\xFF]+'));
+ for (String part in parts) {
+ if (part.contains('.mp4') && part.length > 10) {
+ String url = part;
+ if (!url.startsWith('http')) {
+ url = 'https://aac.saavncdn.com/' +
+ url.replaceFirst(RegExp(r'^[^\w]*'), '');
+ }
+
+ if (url.contains('saavncdn.com') && url.contains('.mp4')) {
+ debugPrint('✅ $approach constructed URL: $url');
+ return url;
+ }
+ }
+ }
+ }
+
+ // Look for URL patterns without protocol
+ RegExp pathPattern = RegExp(r'[a-zA-Z0-9/]+\.mp4', caseSensitive: false);
+ Match? pathMatch = pathPattern.firstMatch(text);
+
+ if (pathMatch != null) {
+ String path = pathMatch.group(0)!;
+ if (path.length > 10) {
+ String url = 'https://aac.saavncdn.com/' + path;
+ debugPrint('✅ $approach constructed from path: $url');
+ return url;
+ }
+ }
+
+ return "";
+ } catch (e) {
+ debugPrint('❌ $approach URL extraction failed: $e');
+ return "";
+ }
+ }
+}
diff --git a/lib/API/saavn.dart b/lib/API/saavn.dart
index bdd8cce..d369bc2 100644
--- a/lib/API/saavn.dart
+++ b/lib/API/saavn.dart
@@ -1,142 +1,347 @@
import 'dart:convert';
-
-import 'package:des_plugin/des_plugin.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
+import 'des_helper.dart';
List searchedList = [];
List topSongsList = [];
String kUrl = "",
- checker,
+ checker = "",
image = "",
title = "",
album = "",
artist = "",
- lyrics,
- has_320,
- rawkUrl;
-String key = "38346591";
-String decrypt = "";
-
-Future fetchSongsList(searchQuery) async {
- String searchUrl =
- "https://www.jiosaavn.com/api.php?app_version=5.18.3&api_version=4&readable_version=5.18.3&v=79&_format=json&query=" +
- searchQuery +
- "&__call=autocomplete.get";
- var res = await http.get(searchUrl, headers: {"Accept": "application/json"});
- var resEdited = (res.body).split("-->");
- var getMain = json.decode(resEdited[1]);
-
- searchedList = getMain["songs"]["data"];
- for (int i = 0; i < searchedList.length; i++) {
- searchedList[i]['title'] = searchedList[i]['title']
- .toString()
- .replaceAll("&", "&")
- .replaceAll("'", "'")
- .replaceAll(""", "\"");
-
- searchedList[i]['more_info']['singers'] = searchedList[i]['more_info']
- ['singers']
- .toString()
- .replaceAll("&", "&")
- .replaceAll("'", "'")
- .replaceAll(""", "\"");
- }
- return searchedList;
+ lyrics = "",
+ has_lyrics = "",
+ has_320 = "",
+ albumId = "",
+ rawkUrl = "";
+
+// API Endpoints (exactly as in your Python endpoints.py)
+const String searchBaseUrl =
+ "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query=";
+const String songDetailsBaseUrl =
+ "https://www.jiosaavn.com/api.php?__call=song.getDetails&cc=in&_marker=0%3F_marker%3D0&_format=json&pids=";
+const String albumDetailsBaseUrl =
+ "https://www.jiosaavn.com/api.php?__call=content.getAlbumDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&albumid=";
+const String playlistDetailsBaseUrl =
+ "https://www.jiosaavn.com/api.php?__call=playlist.getDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&listid=";
+const String lyricsBaseUrl =
+ "https://www.jiosaavn.com/api.php?__call=lyrics.getLyrics&ctx=web6dot0&api_version=4&_format=json&_marker=0%3F_marker%3D0&lyrics_id=";
+
+// DES Decryption function (Python pyDes equivalent implementation)
+String decryptUrl(String encryptedUrl) {
+ return DESHelper.decryptUrl(encryptedUrl);
}
-Future topSongs() async {
- String topSongsUrl =
- "https://www.jiosaavn.com/api.php?__call=webapi.get&token=8MT-LQlP35c_&type=playlist&p=1&n=20&includeMetaTags=0&ctx=web6dot0&api_version=4&_format=json&_marker=0";
- var songsListJSON =
- await http.get(topSongsUrl, headers: {"Accept": "application/json"});
- var songsList = json.decode(songsListJSON.body);
- topSongsList = songsList["list"];
- for (int i = 0; i < topSongsList.length; i++) {
- topSongsList[i]['title'] = topSongsList[i]['title']
- .toString()
- .replaceAll("&", "&")
- .replaceAll("'", "'")
- .replaceAll(""", "\"");
- topSongsList[i]["more_info"]["artistMap"]["primary_artists"][0]["name"] =
- topSongsList[i]["more_info"]["artistMap"]["primary_artists"][0]["name"]
+// Format string function (as per your Python helper.py)
+String formatString(String input) {
+ return input
+ .replaceAll(""", "'")
+ .replaceAll("&", "&")
+ .replaceAll("'", "'");
+}
+
+// Search for songs (exactly as per your Python jiosaavn.py search_for_song function)
+Future fetchSongsList(String searchQuery) async {
+ try {
+ String searchUrl = searchBaseUrl + Uri.encodeComponent(searchQuery);
+ debugPrint('🔍 Search URL: $searchUrl');
+
+ var response = await http.get(Uri.parse(searchUrl));
+
+ if (response.statusCode != 200) {
+ throw Exception('Search API failed with status: ${response.statusCode}');
+ }
+
+ // Handle response format (as per your Python code)
+ String responseBody = response.body;
+ if (responseBody.contains("-->")) {
+ var resEdited = responseBody.split("-->");
+ if (resEdited.length < 2) {
+ throw Exception('Invalid search API response format');
+ }
+ responseBody = resEdited[1];
+ }
+
+ // Parse JSON response
+ dynamic responseJson = json.decode(responseBody);
+
+ // Process songs data (as per your Python logic)
+ List songResponse = responseJson['songs']['data'];
+ searchedList = songResponse;
+
+ // Format song data (as per your Python helper.py)
+ for (int i = 0; i < searchedList.length; i++) {
+ searchedList[i]['title'] = formatString(searchedList[i]['title'] ?? '');
+ searchedList[i]['music'] = formatString(searchedList[i]['music'] ?? '');
+ searchedList[i]['singers'] =
+ formatString(searchedList[i]['singers'] ?? '');
+ searchedList[i]['album'] = formatString(searchedList[i]['album'] ?? '');
+
+ // Enhance image quality
+ if (searchedList[i]['image'] != null) {
+ searchedList[i]['image'] = searchedList[i]['image']
.toString()
- .replaceAll("&", "&")
- .replaceAll("'", "'")
- .replaceAll(""", "\"");
- topSongsList[i]['image'] =
- topSongsList[i]['image'].toString().replaceAll("150x150", "500x500");
+ .replaceAll("150x150", "500x500");
+ }
+ }
+
+ return searchedList;
+ } catch (e) {
+ debugPrint('❌ Search failed: $e');
+ return [];
}
- return topSongsList;
}
-Future fetchSongDetails(songId) async {
- String songUrl =
- "https://www.jiosaavn.com/api.php?app_version=5.18.3&api_version=4&readable_version=5.18.3&v=79&_format=json&__call=song.getDetails&pids=" +
- songId;
- var res = await http.get(songUrl, headers: {"Accept": "application/json"});
- var resEdited = (res.body).split("-->");
- var getMain = json.decode(resEdited[1]);
-
- title = (getMain[songId]["title"])
- .toString()
- .split("(")[0]
- .replaceAll("&", "&")
- .replaceAll("'", "'")
- .replaceAll(""", "\"");
- image = (getMain[songId]["image"]).replaceAll("150x150", "500x500");
- album = (getMain[songId]["more_info"]["album"])
- .toString()
- .replaceAll(""", "\"")
- .replaceAll("'", "'")
- .replaceAll("&", "&");
+// Get song details (exactly as per your Python jiosaavn.py get_song+get_lyrics function)
+Future fetchSongDetails(String songId) async {
+ try {
+ debugPrint('🎵 Getting song details for ID: $songId');
+
+ // Use the exact API endpoint from your Python endpoints.py
+ String songDetailsUrl = songDetailsBaseUrl + songId;
+
+ debugPrint('📡 API URL: $songDetailsUrl');
+
+ var response = await http.get(Uri.parse(songDetailsUrl));
+
+ if (response.statusCode != 200) {
+ debugPrint('❌ API failed with status: ${response.statusCode}');
+ checker = "something went wrong";
+ return false;
+ }
+
+ // Handle response format (as per your Python code)
+ String responseBody = response.body;
+ if (responseBody.contains("-->")) {
+ var resEdited = responseBody.split("-->");
+ if (resEdited.length < 2) {
+ debugPrint('❌ Invalid API response format');
+ checker = "something went wrong";
+ return false;
+ }
+ responseBody = resEdited[1];
+ }
+
+ // Parse JSON
+ dynamic songResponse = json.decode(responseBody);
+
+ if (!songResponse.containsKey(songId)) {
+ debugPrint('❌ Song ID not found in response');
+ checker = "something went wrong";
+ return false;
+ }
+
+ var songData = songResponse[songId];
+
+ // Extract song information (following your Python format_song function)
+ title = formatString(songData["song"] ?? "");
+ album = formatString(songData["album"] ?? "");
+ artist = formatString(songData["singers"] ?? "");
+ albumId = songData["albumid"] ?? "";
+ has_lyrics = songData["has_lyrics"] ?? "false";
+ image =
+ (songData["image"] ?? "").toString().replaceAll("150x150", "500x500");
+ has_320 = songData["320kbps"] ?? "false";
+
+ debugPrint('🎼 Song: $title');
+ debugPrint('🎤 Artist: $artist');
+ debugPrint('💿 Album: $album');
+ debugPrint('💿 Album ID: $albumId');
+ debugPrint('🔊 320kbps: $has_320');
+ debugPrint('🔊 has lyrics: $has_lyrics');
+
+ if (has_lyrics == "true") {
+ String lyricsUrl = lyricsBaseUrl + songId;
+ debugPrint('📡 API URL: $lyricsUrl');
+ var resLyrics = await http.get(Uri.parse(lyricsUrl));
+ if (resLyrics.statusCode != 200) {
+ debugPrint('❌ API failed with status: ${resLyrics.statusCode}');
+ checker = "something went wrong";
+ }
+ dynamic lyricsResponse = json.decode(resLyrics.body);
+ lyrics = lyricsResponse["lyrics"];
+ lyrics = lyrics.replaceAll("
", "\n");
+ }
+
+ // Debug: Print all available fields in songData
+ debugPrint('🔍 Available songData fields: ${songData.keys.toList()}');
+ if (songData["more_info"] != null) {
+ debugPrint(
+ '🔍 Available more_info fields: ${songData["more_info"].keys.toList()}');
+ }
+
+ // URL processing (following your Python helper.py format_song logic)
+ String mediaUrl = "";
+ String encryptedMediaUrl = songData["encrypted_media_url"] ?? "";
+
+ try {
+ // Try to decrypt encrypted_media_url (main method from your Python code)
+ if (encryptedMediaUrl.isNotEmpty) {
+ debugPrint('🔐 Found encrypted_media_url, decrypting...');
+ mediaUrl = decryptUrl(encryptedMediaUrl);
+ if (mediaUrl.isNotEmpty) {
+ debugPrint('✅ Successfully decrypted URL');
+
+ // Apply quality selection (as per your Python logic)
+ if (has_320 != "true") {
+ mediaUrl = mediaUrl.replaceAll("_320.mp4", "_160.mp4");
+ debugPrint('📶 Using 160kbps quality');
+ } else {
+ debugPrint('📶 Using 320kbps quality');
+ }
+ }
+ }
+ } catch (e) {
+ debugPrint('❌ Decryption failed: $e');
+ }
+
+
+ if (mediaUrl.isEmpty) {
+ debugPrint('❌ Failed to get any working media URL');
+ checker = "something went wrong";
+ return false;
+ }
+
+ kUrl = mediaUrl;
+ rawkUrl = mediaUrl;
+
+ debugPrint('🎯 Final media URL: $kUrl');
+ checker = "successfully done";
+ return true;
+ } catch (e) {
+ debugPrint('❌ fetchSongDetails failed: $e');
+ checker = "something went wrong";
+ return false;
+ }
+}
+
+// Top songs function
+Future topSongs() async {
try {
- artist =
- getMain[songId]['more_info']['artistMap']['primary_artists'][0]['name'];
+ String topSongsUrl =
+ "https://www.jiosaavn.com/api.php?__call=webapi.get&token=8MT-LQlP35c_&type=playlist&p=1&n=20&includeMetaTags=0&ctx=web6dot0&api_version=4&_format=json&_marker=0";
+
+ // Add headers to mimic browser behavior and handle SSL issues
+ Map headers = {
+ "Accept": "application/json",
+ "User-Agent":
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Cache-Control": "no-cache",
+ "Pragma": "no-cache"
+ };
+
+ var songsListJSON =
+ await http.get(Uri.parse(topSongsUrl), headers: headers);
+
+ if (songsListJSON.statusCode == 200) {
+ var songsList = json.decode(songsListJSON.body);
+ topSongsList = songsList["list"];
+
+ for (int i = 0; i < topSongsList.length; i++) {
+ topSongsList[i]['title'] =
+ formatString(topSongsList[i]['title'].toString());
+
+ if (topSongsList[i]["more_info"]["artistMap"]["primary_artists"] !=
+ null &&
+ topSongsList[i]["more_info"]["artistMap"]["primary_artists"]
+ .length >
+ 0) {
+ topSongsList[i]["more_info"]["artistMap"]["primary_artists"][0]
+ ["name"] = formatString(topSongsList[i]["more_info"]["artistMap"]
+ ["primary_artists"][0]["name"]
+ .toString());
+ }
+
+ topSongsList[i]['image'] = topSongsList[i]['image']
+ .toString()
+ .replaceAll("150x150", "500x500");
+ }
+
+ debugPrint('✅ Successfully fetched ${topSongsList.length} top songs');
+ return topSongsList;
+ } else {
+ debugPrint(
+ '❌ Top songs API failed with status: ${songsListJSON.statusCode}');
+ return [];
+ }
} catch (e) {
- artist = "-";
+ debugPrint('❌ Failed to fetch top songs: $e');
+ // Return empty list instead of throwing error
+ return [];
}
- print(getMain[songId]["more_info"]["has_lyrics"]);
- if (getMain[songId]["more_info"]["has_lyrics"] == "true") {
- String lyricsUrl =
- "https://www.jiosaavn.com/api.php?__call=lyrics.getLyrics&lyrics_id=" +
- songId +
- "&ctx=web6dot0&api_version=4&_format=json";
- var lyricsRes =
- await http.get(lyricsUrl, headers: {"Accept": "application/json"});
- var lyricsEdited = (lyricsRes.body).split("-->");
- var fetchedLyrics = json.decode(lyricsEdited[1]);
- lyrics = fetchedLyrics["lyrics"].toString().replaceAll("
", "\n");
+}
+
+bool isVpnConnected() {
+ if (kUrl.isNotEmpty) {
+ return kUrl.contains('150x150');
} else {
- lyrics = "null";
- String lyricsApiUrl =
- "https://musifydev.vercel.app/lyrics/" + artist + "/" + title;
- var lyricsApiRes =
- await http.get(lyricsApiUrl, headers: {"Accept": "application/json"});
- var lyricsResponse = json.decode(lyricsApiRes.body);
- if (lyricsResponse['status'] == true && lyricsResponse['lyrics'] != null) {
- lyrics = lyricsResponse['lyrics'];
- }
+ return false;
}
+}
+
+// Get album details (for fetching album song IDs)
+// Returns minimal info - just song IDs and basic metadata
+// Actual song details (including playback URLs) will be fetched on-demand
+Future>> fetchAlbumDetails(String albumId) async {
+ try {
+ debugPrint('💿 Fetching album details for ID: $albumId');
- has_320 = getMain[songId]["more_info"]["320kbps"];
- kUrl = await DesPlugin.decrypt(
- key, getMain[songId]["more_info"]["encrypted_media_url"]);
-
- rawkUrl = kUrl;
-
- final client = http.Client();
- final request = http.Request('HEAD', Uri.parse(kUrl))
- ..followRedirects = false;
- final response = await client.send(request);
- print(response);
- kUrl = (response.headers['location']);
- artist = (getMain[songId]["more_info"]["artistMap"]["primary_artists"][0]
- ["name"])
- .toString()
- .replaceAll(""", "\"")
- .replaceAll("'", "'")
- .replaceAll("&", "&");
- debugPrint(kUrl);
+ String albumUrl = albumDetailsBaseUrl + albumId;
+ debugPrint('📡 Album API URL: $albumUrl');
+
+ var response = await http.get(Uri.parse(albumUrl));
+
+ if (response.statusCode != 200) {
+ debugPrint('❌ Album API failed with status: ${response.statusCode}');
+ return [];
+ }
+
+ // Handle response format
+ String responseBody = response.body;
+ if (responseBody.contains("-->")) {
+ var resEdited = responseBody.split("-->");
+ if (resEdited.length < 2) {
+ debugPrint('❌ Invalid album API response format');
+ return [];
+ }
+ responseBody = resEdited[1];
+ }
+
+ // Parse JSON
+ dynamic albumResponse = json.decode(responseBody);
+
+ // Check if songs array exists
+ if (albumResponse == null || albumResponse['songs'] == null) {
+ debugPrint('❌ No songs found in album');
+ return [];
+ }
+
+ List albumSongs = albumResponse['songs'];
+ List