mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 18:04:33 +01:00
YT Scraping instead of API(bcuz of Limited Quota),
YT Video Support Added(Limited Metadata), Service Fixes(Will Hopefully work in Idle Mode), Interrupted Download Lists Handling Improved!
This commit is contained in:
parent
3df41fbe87
commit
a47a3865c2
@ -5,6 +5,7 @@
|
|||||||
<w>flyer</w>
|
<w>flyer</w>
|
||||||
<w>insta</w>
|
<w>insta</w>
|
||||||
<w>instagram</w>
|
<w>instagram</w>
|
||||||
|
<w>maxresdefault</w>
|
||||||
<w>moshi</w>
|
<w>moshi</w>
|
||||||
<w>musicforeveryone</w>
|
<w>musicforeveryone</w>
|
||||||
<w>musicplaceholder</w>
|
<w>musicplaceholder</w>
|
||||||
@ -15,6 +16,8 @@
|
|||||||
<w>spotify</w>
|
<w>spotify</w>
|
||||||
<w>spotifydownloader</w>
|
<w>spotifydownloader</w>
|
||||||
<w>spotifyler</w>
|
<w>spotifyler</w>
|
||||||
|
<w>thru</w>
|
||||||
|
<w>youtu</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
</component>
|
</component>
|
@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="ProjectPlainTextFileTypeManager">
|
||||||
|
<file url="file://$PROJECT_DIR$/app/src/main/java/com/shabinder/spotiflyer/testing/YoutubeInterface.kt.backup" />
|
||||||
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
|
@ -20,7 +20,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||||
apply plugin: 'kotlinx-serialization'
|
//apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 29
|
||||||
@ -34,17 +34,28 @@ android {
|
|||||||
applicationId 'com.shabinder.spotiflyer'
|
applicationId 'com.shabinder.spotiflyer'
|
||||||
minSdkVersion 22
|
minSdkVersion 22
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 2
|
versionCode 3
|
||||||
versionName "1.1"
|
versionName "1.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/DEPENDENCIES'
|
||||||
|
exclude 'META-INF/LICENSE'
|
||||||
|
exclude 'META-INF/LICENSE.txt'
|
||||||
|
exclude 'META-INF/license.txt'
|
||||||
|
exclude 'META-INF/NOTICE'
|
||||||
|
exclude 'META-INF/NOTICE.txt'
|
||||||
|
exclude 'META-INF/notice.txt'
|
||||||
|
exclude 'META-INF/ASL2.0'
|
||||||
|
exclude("META-INF/*.kotlin_module")
|
||||||
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -65,6 +76,7 @@ dependencies {
|
|||||||
implementation 'androidx.core:core-ktx:1.3.1'
|
implementation 'androidx.core:core-ktx:1.3.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
|
implementation 'androidx.webkit:webkit:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||||
@ -86,9 +98,14 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
|
// Authentication Way Changed!
|
||||||
implementation 'com.google.apis:google-api-services-youtube:v3-rev180-1.22.0'
|
// implementation ('com.google.apis:google-api-services-youtube:v3-rev180-1.22.0'){
|
||||||
implementation 'com.google.oauth-client:google-oauth-client:1.22.0'
|
// exclude module: 'httpclient'
|
||||||
|
// }
|
||||||
|
// //noinspection GradleDependency
|
||||||
|
// implementation ('com.google.oauth-client:google-oauth-client:1.22.0'){
|
||||||
|
// exclude module: 'httpclient'
|
||||||
|
// }
|
||||||
// implementation 'com.spotify.android:auth:1.1.0'
|
// implementation 'com.spotify.android:auth:1.1.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.8.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.8.0'
|
||||||
|
|
||||||
@ -97,9 +114,6 @@ dependencies {
|
|||||||
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
|
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
|
||||||
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
|
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // or "kotlin-stdlib-jdk8"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0" // JVM dependency
|
|
||||||
|
|
||||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||||
implementation 'com.arthenica:mobile-ffmpeg-audio:4.4.LTS'
|
implementation 'com.arthenica:mobile-ffmpeg-audio:4.4.LTS'
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />-->
|
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />-->
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
package com.shabinder.spotiflyer
|
package com.shabinder.spotiflyer
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@ -25,6 +26,8 @@ import android.net.ConnectivityManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
@ -36,7 +39,6 @@ import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
|||||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyServiceToken
|
import com.shabinder.spotiflyer.utils.SpotifyServiceToken
|
||||||
import com.shabinder.spotiflyer.utils.YoutubeInterface
|
|
||||||
import com.shabinder.spotiflyer.utils.createDirectory
|
import com.shabinder.spotiflyer.utils.createDirectory
|
||||||
import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment
|
import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
@ -71,6 +73,8 @@ class MainActivity : AppCompatActivity(){
|
|||||||
binding = DataBindingUtil.setContentView(this,R.layout.main_activity)
|
binding = DataBindingUtil.setContentView(this,R.layout.main_activity)
|
||||||
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
|
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
|
||||||
sharedPref = this.getPreferences(Context.MODE_PRIVATE)
|
sharedPref = this.getPreferences(Context.MODE_PRIVATE)
|
||||||
|
//starting Notification and Downloader Service!
|
||||||
|
DownloadHelper.startService(this)
|
||||||
|
|
||||||
/* if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){
|
/* if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){
|
||||||
val savedToken = sharedPref?.getString("token","error")!!
|
val savedToken = sharedPref?.getString("token","error")!!
|
||||||
@ -88,6 +92,7 @@ class MainActivity : AppCompatActivity(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestPermission()
|
requestPermission()
|
||||||
|
disableDozeMode()
|
||||||
checkIfLatestVersion()
|
checkIfLatestVersion()
|
||||||
createDir()
|
createDir()
|
||||||
setUpi()
|
setUpi()
|
||||||
@ -98,12 +103,42 @@ class MainActivity : AppCompatActivity(){
|
|||||||
//Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"}
|
//Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"}
|
||||||
ytDownloader = YoutubeDownloader()
|
ytDownloader = YoutubeDownloader()
|
||||||
sharedViewModel.ytDownloader = ytDownloader
|
sharedViewModel.ytDownloader = ytDownloader
|
||||||
//Initialing Communication with Youtube
|
|
||||||
YoutubeInterface.youtubeConnector()
|
|
||||||
|
|
||||||
handleIntentFromExternalActivity()
|
handleIntentFromExternalActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("BatteryLife")
|
||||||
|
fun disableDozeMode() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val pm =
|
||||||
|
this.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
|
||||||
|
if (!isIgnoringBatteryOptimizations) {
|
||||||
|
val intent = Intent()
|
||||||
|
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
intent.data = Uri.parse("package:$packageName")
|
||||||
|
startActivityForResult(intent, 1233)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode == 1233) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val pm =
|
||||||
|
getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
val isIgnoringBatteryOptimizations =
|
||||||
|
pm.isIgnoringBatteryOptimizations(packageName)
|
||||||
|
if (isIgnoringBatteryOptimizations) {
|
||||||
|
// Ignoring battery optimization
|
||||||
|
} else {
|
||||||
|
disableDozeMode()//Again Ask For Permission!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adding my own new Spotify Web Api Requests!
|
* Adding my own new Spotify Web Api Requests!
|
||||||
* */
|
* */
|
||||||
@ -253,6 +288,7 @@ class MainActivity : AppCompatActivity(){
|
|||||||
createDirectory(DownloadHelper.defaultDir+"Tracks/")
|
createDirectory(DownloadHelper.defaultDir+"Tracks/")
|
||||||
createDirectory(DownloadHelper.defaultDir+"Albums/")
|
createDirectory(DownloadHelper.defaultDir+"Albums/")
|
||||||
createDirectory(DownloadHelper.defaultDir+"Playlists/")
|
createDirectory(DownloadHelper.defaultDir+"Playlists/")
|
||||||
|
createDirectory(DownloadHelper.defaultDir+"YT_Downloads/")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkIfLatestVersion() {
|
private fun checkIfLatestVersion() {
|
||||||
|
@ -17,28 +17,44 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.downloadHelper
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.AlphaAnimation
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.webkit.ValueCallback
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
import com.github.kiulian.downloader.model.formats.Format
|
||||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||||
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
import com.shabinder.spotiflyer.fragments.MainFragment
|
import com.shabinder.spotiflyer.fragments.MainFragment
|
||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Track
|
||||||
import com.shabinder.spotiflyer.utils.YoutubeInterface
|
|
||||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object DownloadHelper {
|
object DownloadHelper {
|
||||||
|
var webView:WebView? = null
|
||||||
var context : Context? = null
|
var context : Context? = null
|
||||||
|
var statusBar:TextView? = null
|
||||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||||
private var downloadList = arrayListOf<DownloadObject>()
|
private var downloadList = arrayListOf<DownloadObject>()
|
||||||
|
var sharedViewModel:SharedViewModel? = null
|
||||||
|
private var isBrowserLoading = false
|
||||||
|
private var total = 0
|
||||||
|
private var Processed = 0
|
||||||
|
var youtubeList = mutableListOf<YoutubeRequest>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function To Download All Tracks Available in a List
|
* Function To Download All Tracks Available in a List
|
||||||
@ -47,108 +63,179 @@ object DownloadHelper {
|
|||||||
type:String,
|
type:String,
|
||||||
subFolder: String?,
|
subFolder: String?,
|
||||||
trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
|
trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
|
||||||
var size = trackList.size
|
withContext(Dispatchers.Main){
|
||||||
trackList.forEach {
|
var size = trackList.size
|
||||||
size--
|
total += size
|
||||||
if(size == 0){
|
animateStatusBar()
|
||||||
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 )
|
trackList.forEach {
|
||||||
}else{
|
size--
|
||||||
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it )
|
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
}
|
defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(it.name!!)+".mp3")
|
||||||
}
|
if(File(outputFile).exists()){//Download Already Present!!
|
||||||
}
|
Processed++
|
||||||
|
updateStatusBar()
|
||||||
suspend fun downloadTrack(
|
}else{
|
||||||
mainFragment: MainFragment? = null,
|
if(isBrowserLoading){
|
||||||
type:String,
|
if(size == 0){
|
||||||
subFolder:String?,
|
youtubeList.add(YoutubeRequest(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 ))
|
||||||
ytDownloader: YoutubeDownloader?,
|
}else{
|
||||||
searchQuery: String,
|
youtubeList.add(YoutubeRequest(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it))
|
||||||
track: Track,
|
}
|
||||||
index: Int? = null
|
}else{
|
||||||
) {
|
if(size == 0){
|
||||||
withContext(Dispatchers.IO) {
|
getYTLink(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 )
|
||||||
val data: YoutubeInterface.VideoItem = YoutubeInterface.search(searchQuery)?.get(0)!!
|
}else{
|
||||||
|
getYTLink(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)
|
||||||
//Fetching a Video Object.
|
|
||||||
try {
|
|
||||||
val audioUrl = getDownloadLink(AudioQuality.medium, ytDownloader, data)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
mainFragment?.showToast("Starting Download")
|
|
||||||
}
|
|
||||||
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
|
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
|
||||||
try {
|
|
||||||
val audioUrl = getDownloadLink(AudioQuality.high, ytDownloader, data)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
mainFragment?.showToast("Starting Download")
|
|
||||||
}
|
|
||||||
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
|
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
|
||||||
try {
|
|
||||||
val audioUrl = getDownloadLink(AudioQuality.low, ytDownloader, data)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
mainFragment?.showToast("Starting Download")
|
|
||||||
}
|
}
|
||||||
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
|
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
|
||||||
Log.i("Catch", e.toString())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getDownloadLink(quality: AudioQuality ,ytDownloader: YoutubeDownloader?,data:YoutubeInterface.VideoItem): String {
|
|
||||||
val video = ytDownloader?.getVideo(data.id)
|
//TODO CleanUp here and there!!
|
||||||
val format: Format =
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
video?.findAudioWithQuality(quality)?.get(0) as Format
|
suspend fun getYTLink(mainFragment: MainFragment? = null,
|
||||||
Log.i("Format", video.findAudioWithQuality(AudioQuality.medium)?.get(0)!!.mimeType())
|
type:String,
|
||||||
val audioUrl:String = format.url()
|
subFolder:String?,
|
||||||
Log.i("DHelper Link Found", audioUrl)
|
ytDownloader: YoutubeDownloader?,
|
||||||
return audioUrl
|
searchQuery: String,
|
||||||
|
track: Track,
|
||||||
|
index: Int? = null){
|
||||||
|
val searchText = searchQuery.replace("\\s".toRegex(), "+")
|
||||||
|
val url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=$searchText"
|
||||||
|
Log.i("DH YT LINK ",url)
|
||||||
|
applyWebViewSettings(webView!!)
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
isBrowserLoading = true
|
||||||
|
webView!!.loadUrl(url)
|
||||||
|
webView!!.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
view?.evaluateJavascript(
|
||||||
|
"document.getElementsByClassName(\"yt-simple-endpoint style-scope ytd-video-renderer\")[0].href"
|
||||||
|
,object :ValueCallback<String>{
|
||||||
|
override fun onReceiveValue(value: String?) {
|
||||||
|
Log.i("YT-id",value.toString().replace("\"",""))
|
||||||
|
val id = value!!.substringAfterLast("=", "error").replace("\"","")
|
||||||
|
Log.i("YT-id",id)
|
||||||
|
if(id !="error"){//Link extracting error
|
||||||
|
mainFragment?.showToast("Starting Download")
|
||||||
|
Processed++
|
||||||
|
updateStatusBar()
|
||||||
|
downloadFile(subFolder, type, track, index,ytDownloader,id)
|
||||||
|
}
|
||||||
|
if(youtubeList.isNotEmpty()){
|
||||||
|
val request = youtubeList[0]
|
||||||
|
sharedViewModel!!.uiScope.launch {
|
||||||
|
getYTLink(request.mainFragment,request.type,request.subFolder,request.ytDownloader,request.searchQuery,request.track,request.index)
|
||||||
|
}
|
||||||
|
youtubeList.remove(request)
|
||||||
|
if(youtubeList.size == 0){//list processing completed , webView is free again!
|
||||||
|
isBrowserLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
fun applyWebViewSettings(webView: WebView) {
|
||||||
|
val desktopUserAgent =
|
||||||
|
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.4) Gecko/20100101 Firefox/4.0"
|
||||||
|
val mobileUserAgent =
|
||||||
|
"Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"
|
||||||
|
|
||||||
|
//Choose Mobile/Desktop client.
|
||||||
|
webView.settings.userAgentString = desktopUserAgent
|
||||||
|
webView.settings.loadWithOverviewMode = true
|
||||||
|
webView.settings.loadWithOverviewMode = true
|
||||||
|
webView.settings.builtInZoomControls = true
|
||||||
|
webView.settings.setSupportZoom(true)
|
||||||
|
webView.isScrollbarFadingEnabled = false
|
||||||
|
webView.scrollBarStyle = WebView.SCROLLBARS_OUTSIDE_OVERLAY
|
||||||
|
webView.settings.displayZoomControls = false
|
||||||
|
webView.settings.useWideViewPort = true
|
||||||
|
webView.settings.javaScriptEnabled = true
|
||||||
|
webView.settings.loadsImagesAutomatically = false
|
||||||
|
webView.settings.blockNetworkImage = true
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
webView.settings.safeBrowsingEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStatusBar() {
|
||||||
|
statusBar!!.visibility = View.VISIBLE
|
||||||
|
statusBar?.text = "Total: $total Processed: $Processed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun downloadFile(url: String, title: String, subFolder: String?, type: String, track:Track, index:Int? = null,mainFragment: MainFragment? = null) {
|
fun downloadFile(subFolder: String?, type: String, track:Track, index:Int? = null,ytDownloader: YoutubeDownloader?,id: String) {
|
||||||
withContext(Dispatchers.IO) {
|
sharedViewModel!!.uiScope.launch {
|
||||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
withContext(Dispatchers.IO) {
|
||||||
DownloadHelper.defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a")
|
val video = ytDownloader?.getVideo(id)
|
||||||
|
val format:Format? =try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||||
|
}catch (e:java.lang.IndexOutOfBoundsException){
|
||||||
|
try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||||
|
}catch (e:java.lang.IndexOutOfBoundsException){
|
||||||
|
try{
|
||||||
|
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
||||||
|
}catch (e:java.lang.IndexOutOfBoundsException){
|
||||||
|
Log.i("YTDownloader",e.toString())
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format?.let {
|
||||||
|
val url:String = format.url()
|
||||||
|
|
||||||
if(!File(removeIllegalChars(outputFile.substringBeforeLast('.')) +".mp3").exists()){
|
Log.i("DHelper Link Found", url)
|
||||||
val downloadObject = DownloadObject(
|
|
||||||
track = track,
|
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
url = url,
|
defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a")
|
||||||
outputDir = outputFile
|
|
||||||
)
|
val downloadObject = DownloadObject(
|
||||||
Log.i("DH",outputFile)
|
track = track,
|
||||||
if(index==null){
|
url = url,
|
||||||
|
outputDir = outputFile
|
||||||
|
)
|
||||||
|
Log.i("DH",outputFile)
|
||||||
|
startService(context!!, downloadObject)
|
||||||
|
|
||||||
|
/*if(index==null){
|
||||||
downloadList.add(downloadObject)
|
downloadList.add(downloadObject)
|
||||||
}else{
|
}else{
|
||||||
downloadList.add(downloadObject)
|
downloadList.add(downloadObject)
|
||||||
startService(context!!, downloadList)
|
startService(context!!, downloadList)
|
||||||
|
Log.i("DH No of Songs", downloadList.size.toString())
|
||||||
downloadList = arrayListOf()
|
downloadList = arrayListOf()
|
||||||
}
|
}*/
|
||||||
}else{withContext(Dispatchers.Main){
|
// downloadList.add(downloadObject)
|
||||||
mainFragment?.showToast("${track.name} is already Downloaded")
|
// downloadList = arrayListOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun startService(context:Context,list: ArrayList<DownloadObject>) {
|
fun startService(context:Context,obj:DownloadObject? = null ) {
|
||||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||||
serviceIntent.putParcelableArrayListExtra("list",list)
|
serviceIntent.putExtra("object",obj)
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
ContextCompat.startForegroundService(context, serviceIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removing Illegal Chars from File Name
|
* Removing Illegal Chars from File Name
|
||||||
* **/
|
* **/
|
||||||
private fun removeIllegalChars(fileName: String): String? {
|
fun removeIllegalChars(fileName: String): String? {
|
||||||
val illegalCharArray = charArrayOf(
|
val illegalCharArray = charArrayOf(
|
||||||
'/',
|
'/',
|
||||||
'\n',
|
'\n',
|
||||||
@ -165,7 +252,6 @@ object DownloadHelper {
|
|||||||
'|',
|
'|',
|
||||||
'\"',
|
'\"',
|
||||||
'.',
|
'.',
|
||||||
':',
|
|
||||||
'-',
|
'-',
|
||||||
'\''
|
'\''
|
||||||
)
|
)
|
||||||
@ -180,6 +266,29 @@ object DownloadHelper {
|
|||||||
name = name.replace("\\[".toRegex(), "")
|
name = name.replace("\\[".toRegex(), "")
|
||||||
name = name.replace("]".toRegex(), "")
|
name = name.replace("]".toRegex(), "")
|
||||||
name = name.replace("\\.".toRegex(), "")
|
name = name.replace("\\.".toRegex(), "")
|
||||||
|
name = name.replace("\"".toRegex(), "")
|
||||||
|
name = name.replace("\'".toRegex(), "")
|
||||||
|
name = name.replace(":".toRegex(), "")
|
||||||
|
name = name.replace("\\|".toRegex(), "")
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun animateStatusBar() {
|
||||||
|
val anim: Animation = AlphaAnimation(0.0f, 0.9f)
|
||||||
|
anim.duration = 650 //You can manage the blinking time with this parameter
|
||||||
|
anim.startOffset = 20
|
||||||
|
anim.repeatMode = Animation.REVERSE
|
||||||
|
anim.repeatCount = Animation.INFINITE
|
||||||
|
statusBar?.animation = anim
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
data class YoutubeRequest(
|
||||||
|
val mainFragment: MainFragment? = null,
|
||||||
|
val type:String,
|
||||||
|
val subFolder:String?,
|
||||||
|
val ytDownloader: YoutubeDownloader?,
|
||||||
|
val searchQuery: String,
|
||||||
|
val track: Track,
|
||||||
|
val index: Int? = null
|
||||||
|
)
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.fragments
|
package com.shabinder.spotiflyer.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@ -29,6 +30,9 @@ import android.util.Log
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.ValueCallback
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
@ -45,6 +49,7 @@ import com.shabinder.spotiflyer.R
|
|||||||
import com.shabinder.spotiflyer.SharedViewModel
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
|
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
|
||||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||||
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.applyWebViewSettings
|
||||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Track
|
||||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||||
@ -67,23 +72,25 @@ class MainFragment : Fragment() {
|
|||||||
private var type:String = ""
|
private var type:String = ""
|
||||||
private var spotifyLink = ""
|
private var spotifyLink = ""
|
||||||
private var i: Intent? = null
|
private var i: Intent? = null
|
||||||
|
private var webView: WebView? = null
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
|
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
|
||||||
|
webView = binding.webView
|
||||||
|
DownloadHelper.webView = binding.webView
|
||||||
DownloadHelper.context = requireContext()
|
DownloadHelper.context = requireContext()
|
||||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||||
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
||||||
spotifyService = sharedViewModel.spotifyService
|
spotifyService = sharedViewModel.spotifyService
|
||||||
|
DownloadHelper.sharedViewModel = sharedViewModel
|
||||||
|
DownloadHelper.statusBar = binding.StatusBar
|
||||||
|
|
||||||
val spanStringBuilder = SpannableStringBuilder()
|
setUpUsageText()
|
||||||
spanStringBuilder.append(getText(R.string.d_one)).append("\n")
|
|
||||||
spanStringBuilder.append(getText(R.string.d_two)).append("\n")
|
|
||||||
spanStringBuilder.append(getText(R.string.d_three)).append("\n")
|
|
||||||
|
|
||||||
binding.usage.text = spanStringBuilder
|
|
||||||
openSpotifyButton()
|
openSpotifyButton()
|
||||||
openGithubButton()
|
openGithubButton()
|
||||||
openInstaButton()
|
openInstaButton()
|
||||||
@ -93,127 +100,14 @@ class MainFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnSearch.setOnClickListener {
|
binding.btnSearch.setOnClickListener {
|
||||||
spotifyLink = binding.linkSearch.text.toString()
|
val link = binding.linkSearch.text.toString()
|
||||||
|
if(link.contains("open.spotify",true)){
|
||||||
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
spotifySearch()
|
||||||
type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
}
|
||||||
|
if(link.contains("youtube.com",true) || link.contains("youtu.be",true) ){
|
||||||
Log.i("Fragment", "$type : $link")
|
youtubeSearch()
|
||||||
|
|
||||||
if(sharedViewModel.spotifyService == null && !isOnline()){
|
|
||||||
(activity as MainActivity).authenticateSpotify()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == "Error" || link == "Error") {
|
|
||||||
showToast("Please Check Your Link!")
|
|
||||||
} else if(!isOnline()){
|
|
||||||
sharedViewModel.showAlertDialog(resources,requireContext())
|
|
||||||
} else {
|
|
||||||
adapter = TrackListAdapter()
|
|
||||||
binding.trackList.adapter = adapter
|
|
||||||
adapter.sharedViewModel = sharedViewModel
|
|
||||||
adapter.mainFragment = this
|
|
||||||
setUiVisibility()
|
|
||||||
|
|
||||||
if(mainViewModel.searchLink == spotifyLink){
|
|
||||||
//it's a Device Configuration Change
|
|
||||||
adapterConfig(mainViewModel.trackList)
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
bindImage(binding.imageView,mainViewModel.coverUrl)
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
when (type) {
|
|
||||||
"track" -> {
|
|
||||||
mainViewModel.searchLink = spotifyLink
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
val trackObject = sharedViewModel.getTrackDetails(link)
|
|
||||||
val trackList = mutableListOf<Track>()
|
|
||||||
trackList.add(trackObject!!)
|
|
||||||
mainViewModel.trackList = trackList
|
|
||||||
mainViewModel.coverUrl = trackObject.album!!.images?.get(0)!!.url!!
|
|
||||||
bindImage(binding.imageView,mainViewModel.coverUrl)
|
|
||||||
adapterConfig(trackList)
|
|
||||||
|
|
||||||
binding.btnDownloadAll.setOnClickListener {
|
|
||||||
showToast("Starting Download in Few Seconds")
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
downloadAllTracks(
|
|
||||||
"Tracks",
|
|
||||||
null,
|
|
||||||
trackList,
|
|
||||||
sharedViewModel.ytDownloader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"album" -> {
|
|
||||||
mainViewModel.searchLink = spotifyLink
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
val albumObject = sharedViewModel.getAlbumDetails(link)
|
|
||||||
val trackList = mutableListOf<Track>()
|
|
||||||
albumObject!!.tracks?.items?.forEach { trackList.add(it) }
|
|
||||||
mainViewModel.trackList = trackList
|
|
||||||
mainViewModel.coverUrl = albumObject.images?.get(0)!!.url!!
|
|
||||||
bindImage(binding.imageView,mainViewModel.coverUrl)
|
|
||||||
adapter.isAlbum = true
|
|
||||||
adapterConfig(trackList)
|
|
||||||
binding.btnDownloadAll.setOnClickListener {
|
|
||||||
showToast("Starting Download in Few Seconds")
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
downloadAllTracks(
|
|
||||||
"Albums",
|
|
||||||
albumObject.name,
|
|
||||||
trackList,
|
|
||||||
sharedViewModel.ytDownloader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
"playlist" -> {
|
|
||||||
mainViewModel.searchLink = spotifyLink
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
val playlistObject = sharedViewModel.getPlaylistDetails(link)
|
|
||||||
val trackList = mutableListOf<Track>()
|
|
||||||
playlistObject!!.tracks?.items!!.forEach { trackList.add(it.track!!) }
|
|
||||||
mainViewModel.trackList = trackList
|
|
||||||
mainViewModel.coverUrl = playlistObject.images?.get(0)!!.url!!
|
|
||||||
bindImage(binding.imageView,mainViewModel.coverUrl)
|
|
||||||
adapterConfig(trackList)
|
|
||||||
binding.btnDownloadAll.setOnClickListener {
|
|
||||||
showToast("Starting Download in Few Seconds")
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
loadAllImages(trackList)
|
|
||||||
downloadAllTracks(
|
|
||||||
"Playlists",
|
|
||||||
playlistObject.name,
|
|
||||||
trackList,
|
|
||||||
sharedViewModel.ytDownloader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
"episode" -> {
|
|
||||||
showToast("Implementation Pending")
|
|
||||||
}
|
|
||||||
"show" -> {
|
|
||||||
showToast("Implementation Pending ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
handleIntent()
|
handleIntent()
|
||||||
//Handling Device Configuration Change
|
//Handling Device Configuration Change
|
||||||
@ -225,6 +119,173 @@ class MainFragment : Fragment() {
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun youtubeSearch() {
|
||||||
|
val youtubeLink = binding.linkSearch.text.toString()
|
||||||
|
var title = ""
|
||||||
|
val link = youtubeLink.removePrefix("https://").removePrefix("http://")
|
||||||
|
val sampleDomain1 = "youtube.com"
|
||||||
|
val sampleDomain2 = "youtu.be"
|
||||||
|
if(!link.contains("playlist",true)){
|
||||||
|
var searchId = "error"
|
||||||
|
if(link.contains(sampleDomain1,true) ){
|
||||||
|
searchId = link.substringAfterLast("=","error")
|
||||||
|
}
|
||||||
|
if(link.contains(sampleDomain2,true) && !link.contains("playlist",true) ){
|
||||||
|
searchId = link.substringAfterLast("/","error")
|
||||||
|
}
|
||||||
|
if(searchId != "error"){
|
||||||
|
val coverLink = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||||
|
applyWebViewSettings(webView!!)
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
webView!!.loadUrl(youtubeLink)
|
||||||
|
webView!!.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
view?.evaluateJavascript(
|
||||||
|
"document.getElementsByTagName(\"h1\")[0].textContent"
|
||||||
|
,object : ValueCallback<String> {
|
||||||
|
override fun onReceiveValue(value: String?) {
|
||||||
|
title = DownloadHelper.removeIllegalChars(value.toString()).toString()
|
||||||
|
Log.i("YT-id", title)
|
||||||
|
Log.i("YT-id", value)
|
||||||
|
Log.i("YT-id", coverLink)
|
||||||
|
setUiVisibility()
|
||||||
|
bindImage(binding.imageView,coverLink)
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
showToast("Starting Download in Few Seconds")
|
||||||
|
//TODO Clean This Code!
|
||||||
|
DownloadHelper.downloadFile(null,"YT_Downloads",Track(name = value,ytCoverUrl = coverLink),0,sharedViewModel.ytDownloader,searchId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else(showToast("Your Youtube Link is not of a Video!!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun spotifySearch(){
|
||||||
|
spotifyLink = binding.linkSearch.text.toString()
|
||||||
|
|
||||||
|
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||||
|
type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||||
|
|
||||||
|
Log.i("Fragment", "$type : $link")
|
||||||
|
|
||||||
|
if(sharedViewModel.spotifyService == null && !isOnline()){
|
||||||
|
(activity as MainActivity).authenticateSpotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == "Error" || link == "Error") {
|
||||||
|
showToast("Please Check Your Link!")
|
||||||
|
} else if(!isOnline()){
|
||||||
|
sharedViewModel.showAlertDialog(resources,requireContext())
|
||||||
|
} else {
|
||||||
|
adapter = TrackListAdapter()
|
||||||
|
binding.trackList.adapter = adapter
|
||||||
|
adapter.sharedViewModel = sharedViewModel
|
||||||
|
adapter.mainFragment = this
|
||||||
|
setUiVisibility()
|
||||||
|
|
||||||
|
if(mainViewModel.searchLink == spotifyLink){
|
||||||
|
//it's a Device Configuration Change
|
||||||
|
adapterConfig(mainViewModel.trackList)
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
bindImage(binding.imageView,mainViewModel.coverUrl)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
when (type) {
|
||||||
|
"track" -> {
|
||||||
|
mainViewModel.searchLink = spotifyLink
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
val trackObject = sharedViewModel.getTrackDetails(link)
|
||||||
|
val trackList = mutableListOf<Track>()
|
||||||
|
trackList.add(trackObject!!)
|
||||||
|
mainViewModel.trackList = trackList
|
||||||
|
mainViewModel.coverUrl = trackObject.album!!.images?.get(0)!!.url!!
|
||||||
|
bindImage(binding.imageView,mainViewModel.coverUrl)
|
||||||
|
adapterConfig(trackList)
|
||||||
|
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
showToast("Starting Download in Few Seconds")
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
downloadAllTracks(
|
||||||
|
"Tracks",
|
||||||
|
null,
|
||||||
|
trackList,
|
||||||
|
sharedViewModel.ytDownloader
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"album" -> {
|
||||||
|
mainViewModel.searchLink = spotifyLink
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
val albumObject = sharedViewModel.getAlbumDetails(link)
|
||||||
|
val trackList = mutableListOf<Track>()
|
||||||
|
albumObject!!.tracks?.items?.forEach { trackList.add(it) }
|
||||||
|
mainViewModel.trackList = trackList
|
||||||
|
mainViewModel.coverUrl = albumObject.images?.get(0)!!.url!!
|
||||||
|
bindImage(binding.imageView,mainViewModel.coverUrl)
|
||||||
|
adapter.isAlbum = true
|
||||||
|
adapterConfig(trackList)
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
showToast("Starting Download in Few Seconds")
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
loadAllImages(trackList)
|
||||||
|
downloadAllTracks(
|
||||||
|
"Albums",
|
||||||
|
albumObject.name,
|
||||||
|
trackList,
|
||||||
|
sharedViewModel.ytDownloader
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"playlist" -> {
|
||||||
|
mainViewModel.searchLink = spotifyLink
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
val playlistObject = sharedViewModel.getPlaylistDetails(link)
|
||||||
|
val trackList = mutableListOf<Track>()
|
||||||
|
playlistObject!!.tracks?.items!!.forEach { trackList.add(it.track!!) }
|
||||||
|
mainViewModel.trackList = trackList
|
||||||
|
mainViewModel.coverUrl = playlistObject.images?.get(0)!!.url!!
|
||||||
|
bindImage(binding.imageView,mainViewModel.coverUrl)
|
||||||
|
adapterConfig(trackList)
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
showToast("Starting Download in Few Seconds")
|
||||||
|
sharedViewModel.uiScope.launch {
|
||||||
|
loadAllImages(trackList)
|
||||||
|
downloadAllTracks(
|
||||||
|
"Playlists",
|
||||||
|
playlistObject.name,
|
||||||
|
trackList,
|
||||||
|
sharedViewModel.ytDownloader
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"episode" -> {
|
||||||
|
showToast("Implementation Pending")
|
||||||
|
}
|
||||||
|
"show" -> {
|
||||||
|
showToast("Implementation Pending ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to fetch all Images for using in mp3 tag.
|
* Function to fetch all Images for using in mp3 tag.
|
||||||
**/
|
**/
|
||||||
@ -351,6 +412,15 @@ class MainFragment : Fragment() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setUpUsageText() {
|
||||||
|
val spanStringBuilder = SpannableStringBuilder()
|
||||||
|
spanStringBuilder.append(getText(R.string.d_one)).append("\n")
|
||||||
|
spanStringBuilder.append(getText(R.string.d_two)).append("\n")
|
||||||
|
spanStringBuilder.append(getText(R.string.d_three)).append("\n")
|
||||||
|
binding.usage.text = spanStringBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util. Function to create toasts!
|
* Util. Function to create toasts!
|
||||||
**/
|
**/
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.models
|
package com.shabinder.spotiflyer.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import android.os.Parcelable
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
@Serializable
|
@Parcelize
|
||||||
data class Followers(
|
data class Followers(
|
||||||
var href: String? = null,
|
var href: String? = null,
|
||||||
var total: Int? = null):java.io.Serializable
|
var total: Int? = null):Parcelable
|
@ -39,4 +39,5 @@ data class Track(
|
|||||||
var uri: String? = null,
|
var uri: String? = null,
|
||||||
var album: Album? = null,
|
var album: Album? = null,
|
||||||
var external_ids: Map<String?, String?>? = null,
|
var external_ids: Map<String?, String?>? = null,
|
||||||
var popularity: Int? = null):Parcelable
|
var popularity: Int? = null,
|
||||||
|
var ytCoverUrl:String? = null):Parcelable
|
@ -26,7 +26,7 @@ import android.widget.TextView
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
import com.shabinder.spotiflyer.SharedViewModel
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadTrack
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.getYTLink
|
||||||
import com.shabinder.spotiflyer.fragments.MainFragment
|
import com.shabinder.spotiflyer.fragments.MainFragment
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Track
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.utils.bindImage
|
||||||
@ -63,7 +63,7 @@ class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>() {
|
|||||||
holder.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
holder.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
||||||
holder.downloadBtn.setOnClickListener{
|
holder.downloadBtn.setOnClickListener{
|
||||||
sharedViewModel.uiScope.launch {
|
sharedViewModel.uiScope.launch {
|
||||||
downloadTrack(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,"${item.name} ${item.artists?.get(0)!!.name?:""}",track = item,index = 0)
|
getYTLink(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,"${item.name} ${item.artists?.get(0)!!.name?:""}",track = item,index = 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ fun bindImage(imgView: ImageView, imgUrl: String?) {
|
|||||||
try {
|
try {
|
||||||
val file = File(
|
val file = File(
|
||||||
Environment.getExternalStorageDirectory(),
|
Environment.getExternalStorageDirectory(),
|
||||||
DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
|
DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
|
||||||
) // the File to save , append increasing numeric counter to prevent files from getting overwritten.
|
) // the File to save , append increasing numeric counter to prevent files from getting overwritten.
|
||||||
val options = BitmapFactory.Options()
|
val options = BitmapFactory.Options()
|
||||||
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Shabinder Singh
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.shabinder.spotiflyer.utils
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.api.client.http.HttpRequestInitializer
|
|
||||||
import com.google.api.client.http.javanet.NetHttpTransport
|
|
||||||
import com.google.api.client.json.jackson2.JacksonFactory
|
|
||||||
import com.google.api.services.youtube.YouTube
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
object YoutubeInterface {
|
|
||||||
private var youtube: YouTube? = null
|
|
||||||
private var query:YouTube.Search.List? = null
|
|
||||||
private var apiKey:String = "AIzaSyDuRmMA_2mF56BjlhhNpa0SIbjMgjjFaEI"
|
|
||||||
private var apiKey2:String = "AIzaSyCotyqgqmz5qw4-IH0tiezIrIIDHLI2yNs"
|
|
||||||
|
|
||||||
fun youtubeConnector() {
|
|
||||||
youtube =
|
|
||||||
YouTube.Builder(NetHttpTransport(), JacksonFactory(), HttpRequestInitializer { })
|
|
||||||
.setApplicationName("spotifyler").build()
|
|
||||||
try {
|
|
||||||
query = youtube?.search()?.list("id,snippet")
|
|
||||||
query?.key = apiKey
|
|
||||||
query?.maxResults = 1
|
|
||||||
query?.type = "video"
|
|
||||||
query?.fields =
|
|
||||||
"items(id/videoId,snippet/title,snippet/thumbnails/default/url)"
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.i("YI", "Could not initialize: $e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(keywords: String?): List<VideoItem>? {
|
|
||||||
Log.i("YI searched for",keywords.toString())
|
|
||||||
if (youtube == null){youtubeConnector()}
|
|
||||||
query!!.q= keywords
|
|
||||||
return try {
|
|
||||||
val response = query!!.execute()
|
|
||||||
val results =
|
|
||||||
response.items
|
|
||||||
val items = mutableListOf<VideoItem>()
|
|
||||||
for (result in results) {
|
|
||||||
val item = VideoItem(
|
|
||||||
id = result.id.videoId,
|
|
||||||
title = result.snippet.title,
|
|
||||||
// description = result.snippet.description,
|
|
||||||
thumbnailUrl = result.snippet.thumbnails.default.url
|
|
||||||
)
|
|
||||||
items.add(item)
|
|
||||||
Log.i("YI links received",item.id)
|
|
||||||
}
|
|
||||||
items
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.d("YI", "Could not search: $e")
|
|
||||||
if(query?.key == apiKey2){return null}
|
|
||||||
query?.key = apiKey2
|
|
||||||
search(keywords)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class VideoItem(
|
|
||||||
val id:String,
|
|
||||||
val title:String,
|
|
||||||
// val description: String,
|
|
||||||
val thumbnailUrl:String
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
@ -18,11 +18,16 @@
|
|||||||
package com.shabinder.spotiflyer.worker
|
package com.shabinder.spotiflyer.worker
|
||||||
|
|
||||||
import android.app.*
|
import android.app.*
|
||||||
|
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@ -41,22 +46,41 @@ import com.shabinder.spotiflyer.models.Track
|
|||||||
import com.tonyodev.fetch2.*
|
import com.tonyodev.fetch2.*
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
import com.tonyodev.fetch2core.DownloadBlock
|
||||||
import com.tonyodev.fetch2core.Func
|
import com.tonyodev.fetch2core.Func
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
class ForegroundService : Service(){
|
class ForegroundService : Service(){
|
||||||
private val tag = "Foreground Service"
|
private val tag = "Foreground Service"
|
||||||
private val channelId = "ForegroundDownloaderService"
|
private val channelId = "ForegroundDownloaderService"
|
||||||
|
private val notificationId = 101
|
||||||
private var total = 0 //Total Downloads Requested
|
private var total = 0 //Total Downloads Requested
|
||||||
private var converted = 0//Total Files Converted
|
private var converted = 0//Total Files Converted
|
||||||
|
private var downloaded = 0//Total Files downloaded
|
||||||
private var fetch:Fetch? = null
|
private var fetch:Fetch? = null
|
||||||
|
private var downloadManager : DownloadManager? = null
|
||||||
private var downloadList = mutableListOf<DownloadObject>()
|
private var downloadList = mutableListOf<DownloadObject>()
|
||||||
private var serviceJob = Job()
|
private var serviceJob = Job()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||||
private val requestMap = mutableMapOf<Request,Track>()
|
private val requestMap = mutableMapOf<Request,Track>()
|
||||||
private val downloadMap = mutableMapOf<String,Track>()
|
private val downloadMap = mutableMapOf<String,Track>()
|
||||||
private var speed :Long = 0
|
private var speed :Long = 0
|
||||||
|
private var defaultDirectory = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||||
|
private val parentDirectory = File(Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDirectory+File.separator
|
||||||
|
)
|
||||||
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
private var isServiceStarted = false
|
||||||
|
private var messageSnippet1 = ""
|
||||||
|
private var messageSnippet2 = ""
|
||||||
|
private var messageSnippet3 = ""
|
||||||
|
private var messageSnippet4 = ""
|
||||||
|
var notificationLine = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
return null
|
return null
|
||||||
@ -69,26 +93,22 @@ class ForegroundService : Service(){
|
|||||||
this,
|
this,
|
||||||
0, notificationIntent, 0
|
0, notificationIntent, 0
|
||||||
)
|
)
|
||||||
val notification = NotificationCompat.Builder(this, channelId)
|
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
.setContentTitle("SpotiFlyer: Downloading Your Music")
|
|
||||||
.setSubText("Speed: $speed KB/s ")
|
|
||||||
.setNotificationSilent()
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
|
|
||||||
.setSmallIcon(R.drawable.down_arrowbw)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val fetchConfiguration =
|
val fetchConfiguration =
|
||||||
FetchConfiguration.Builder(this)
|
FetchConfiguration.Builder(this)
|
||||||
.setDownloadConcurrentLimit(4)
|
.setDownloadConcurrentLimit(4)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
Fetch.Impl.setDefaultInstanceConfiguration(fetchConfiguration)
|
fetch = Fetch.Impl.getInstance(fetchConfiguration)
|
||||||
fetch = Fetch.getDefaultInstance()
|
// fetch?.enableLogging(true)
|
||||||
fetch?.addListener(fetchListener)
|
fetch?.addListener(fetchListener)
|
||||||
startForeground()
|
startForeground()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Starting Service with Notification as Foreground!
|
||||||
|
**/
|
||||||
private fun startForeground() {
|
private fun startForeground() {
|
||||||
val channelId =
|
val channelId =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@ -105,10 +125,15 @@ class ForegroundService : Service(){
|
|||||||
.setSubText("Speed: $speed KB/s ")
|
.setSubText("Speed: $speed KB/s ")
|
||||||
.setNotificationSilent()
|
.setNotificationSilent()
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
|
.setContentText("Total: $total Downloaded: $downloaded Completed:$converted ")
|
||||||
.setSmallIcon(R.drawable.down_arrowbw)
|
.setSmallIcon(R.drawable.down_arrowbw)
|
||||||
|
.setStyle(NotificationCompat.InboxStyle()
|
||||||
|
.addLine(messageSnippet1)
|
||||||
|
.addLine(messageSnippet2)
|
||||||
|
.addLine(messageSnippet3)
|
||||||
|
.addLine(messageSnippet4))
|
||||||
.build()
|
.build()
|
||||||
startForeground(101, notification)
|
startForeground(notificationId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@ -126,52 +151,107 @@ class ForegroundService : Service(){
|
|||||||
Log.i(tag,"Service Started.")
|
Log.i(tag,"Service Started.")
|
||||||
|
|
||||||
//do heavy work on a background thread
|
//do heavy work on a background thread
|
||||||
// val list = intent.getSerializableExtra("list") as List<Any?>
|
//val list = intent.getSerializableExtra("list") as List<Any?>
|
||||||
val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
|
// val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
|
||||||
Log.i(tag,"Intent List Size: ${list!!.size}")
|
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
||||||
total += list.size
|
val obj = intent.getParcelableExtra<DownloadObject>("object") ?: intent.extras?.getParcelable<DownloadObject>("object")
|
||||||
list.forEach { downloadList.add(it as DownloadObject) }
|
obj?.let {
|
||||||
|
total ++
|
||||||
serviceScope.launch {
|
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
||||||
withContext(Dispatchers.IO){
|
updateNotification()
|
||||||
for (downloadObject in downloadList) {
|
serviceScope.launch {
|
||||||
val request= Request(downloadObject.url, downloadObject.outputDir)
|
val request= Request(obj.url, obj.outputDir)
|
||||||
request.priority = Priority.NORMAL
|
request.priority = Priority.NORMAL
|
||||||
request.networkType = NetworkType.ALL
|
request.networkType = NetworkType.ALL
|
||||||
|
|
||||||
fetch?.enqueue(request,
|
fetch!!.enqueue(request,
|
||||||
Func {
|
Func {
|
||||||
Log.i("DownloadManager", "Download Request Sent")
|
requestMap[it] = obj.track
|
||||||
requestMap[it] = downloadObject.track
|
downloadList.remove(obj)
|
||||||
downloadList.remove(downloadObject) },
|
Log.i(tag, "Enqueuing Download")
|
||||||
|
},
|
||||||
Func {
|
Func {
|
||||||
Log.i("DownloadManager", "Download Request Error:${it.throwable.toString()}")}
|
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return START_NOT_STICKY
|
|
||||||
|
//Wake locks and misc tasks from here :
|
||||||
|
return if (isServiceStarted){
|
||||||
|
START_STICKY
|
||||||
|
} else{
|
||||||
|
Log.i(tag,"Starting the foreground service task")
|
||||||
|
isServiceStarted = true
|
||||||
|
|
||||||
|
wakeLock =
|
||||||
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
||||||
|
acquire()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
START_STICKY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if(downloadMap.isEmpty() && converted == total){
|
if(downloadMap.isEmpty() && converted == total){
|
||||||
Log.i(tag,"Service destroyed.")
|
Log.i(tag,"Service destroyed.")
|
||||||
|
fetch?.close()
|
||||||
|
deleteFile(parentDirectory)
|
||||||
|
releaseWakeLock()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun releaseWakeLock() {
|
||||||
|
Log.i(tag,"Releasing Wake Lock")
|
||||||
|
try {
|
||||||
|
wakeLock?.let {
|
||||||
|
if (it.isHeld) {
|
||||||
|
it.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.i(tag,"Service stopped without being started: ${e.message}")
|
||||||
|
}
|
||||||
|
isServiceStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
if(downloadMap.isEmpty() && converted == total ){
|
if(downloadMap.isEmpty() && converted == total ){
|
||||||
Log.i(tag,"Service destroyed.")
|
Log.i(tag,"Service Removed.")
|
||||||
|
fetch?.close()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deleting All Residual Files except Mp3 Files
|
||||||
|
* */
|
||||||
|
private fun deleteFile(dir:File) {
|
||||||
|
Log.i(tag,"Starting Deletions in ${dir.path} ")
|
||||||
|
val fList = dir.listFiles()
|
||||||
|
fList?.let {
|
||||||
|
for (file in fList) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
Log.i(tag,"Cleaning ${file.path} Directory")
|
||||||
|
deleteFile(file)
|
||||||
|
} else if(file.isFile) {
|
||||||
|
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
||||||
|
// Log.i(tag,"deleting ${file.path}")
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Listener/ Responsible for Fetch Behaviour
|
||||||
|
**/
|
||||||
private var fetchListener: FetchListener = object : FetchListener {
|
private var fetchListener: FetchListener = object : FetchListener {
|
||||||
override fun onQueued(
|
override fun onQueued(
|
||||||
download: Download,
|
download: Download,
|
||||||
@ -194,7 +274,32 @@ class ForegroundService : Service(){
|
|||||||
totalBlocks: Int
|
totalBlocks: Int
|
||||||
) {
|
) {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
|
when(notificationLine){
|
||||||
|
1 -> {
|
||||||
|
messageSnippet1 = "Downloading ${track?.name}"
|
||||||
|
notificationLine = 2
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
messageSnippet2 = "Downloading ${track?.name}"
|
||||||
|
notificationLine = 3
|
||||||
|
}
|
||||||
|
3-> {
|
||||||
|
messageSnippet3 = "Downloading ${track?.name}"
|
||||||
|
notificationLine = 4
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
messageSnippet4 = "Downloading ${track?.name}"
|
||||||
|
notificationLine = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Log.i(tag,"${track?.name} Download Started")
|
Log.i(tag,"${track?.name} Download Started")
|
||||||
|
updateNotification()
|
||||||
|
|
||||||
|
val link = "https://m.youtube.com/watch?v=shCX5YgU9yc"
|
||||||
|
var result = ""
|
||||||
|
result = link.removePrefix("https://")
|
||||||
|
result = link.removePrefix("http://")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onWaitingNetwork(download: Download) {
|
override fun onWaitingNetwork(download: Download) {
|
||||||
@ -211,13 +316,21 @@ class ForegroundService : Service(){
|
|||||||
|
|
||||||
override fun onCompleted(download: Download) {
|
override fun onCompleted(download: Download) {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
speed = 0
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
convertToMp3(download.file, track!!)
|
try{
|
||||||
|
convertToMp3(download.file, track!!)
|
||||||
|
Log.i(tag,"${track.name} Download Completed")
|
||||||
|
}catch (e:KotlinNullPointerException
|
||||||
|
){
|
||||||
|
Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!")
|
||||||
|
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
||||||
|
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Log.i(tag,"${track?.name} Download Completed")
|
downloaded++
|
||||||
requestMap.remove(download.request)
|
requestMap.remove(download.request)
|
||||||
updateNotification()
|
if(requestMap.keys.toList().isEmpty()) speed = 0
|
||||||
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleted(download: Download) {
|
override fun onDeleted(download: Download) {
|
||||||
@ -233,7 +346,13 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
||||||
|
val track = requestMap[download.request]
|
||||||
Log.i(tag,download.error.throwable.toString())
|
Log.i(tag,download.error.throwable.toString())
|
||||||
|
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
||||||
|
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||||
|
downloaded++
|
||||||
|
requestMap.remove(download.request)
|
||||||
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPaused(download: Download) {
|
override fun onPaused(download: Download) {
|
||||||
@ -253,12 +372,51 @@ class ForegroundService : Service(){
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If fetch Fails , Android Download Manager To RESCUE!!
|
||||||
|
**/
|
||||||
|
fun downloadUsingDM(url:String,outputDir:String,track: Track){
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
val request = DownloadManager.Request(uri)
|
||||||
|
.setAllowedNetworkTypes(
|
||||||
|
DownloadManager.Request.NETWORK_WIFI or
|
||||||
|
DownloadManager.Request.NETWORK_MOBILE
|
||||||
|
)
|
||||||
|
.setAllowedOverRoaming(false)
|
||||||
|
.setTitle(track.name)
|
||||||
|
.setDescription("Spotify Downloader Working Up here...")
|
||||||
|
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
|
||||||
|
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
|
||||||
|
))
|
||||||
|
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
//Start Download
|
||||||
|
val downloadID = downloadManager?.enqueue(request)
|
||||||
|
Log.i("DownloadManager", "Download Request Sent")
|
||||||
|
|
||||||
|
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
//Fetching the download id received with the broadcast
|
||||||
|
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||||
|
//Checking if the received broadcast is for our enqueued download by matching download id
|
||||||
|
if (downloadID == id) {
|
||||||
|
convertToMp3(outputDir,track)
|
||||||
|
converted++
|
||||||
|
//Unregister this broadcast Receiver
|
||||||
|
this@ForegroundService.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerReceiver(onDownloadComplete,IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
|
||||||
|
**/
|
||||||
fun convertToMp3(filePath: String,track: Track){
|
fun convertToMp3(filePath: String,track: Track){
|
||||||
val m4aFile = File(filePath)
|
val m4aFile = File(filePath)
|
||||||
|
|
||||||
val executionId = FFmpeg.executeAsync(
|
FFmpeg.executeAsync(
|
||||||
"-i $filePath -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
|
"-i $filePath -b:a 160k -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
|
||||||
) { _, returnCode ->
|
) { _, returnCode ->
|
||||||
when (returnCode) {
|
when (returnCode) {
|
||||||
RETURN_CODE_SUCCESS -> {
|
RETURN_CODE_SUCCESS -> {
|
||||||
@ -292,11 +450,11 @@ class ForegroundService : Service(){
|
|||||||
updateNotification()
|
updateNotification()
|
||||||
//All tasks completed (REST IN PEACE)
|
//All tasks completed (REST IN PEACE)
|
||||||
if(converted == total){
|
if(converted == total){
|
||||||
stopForeground(false)
|
onDestroy()
|
||||||
stopSelf()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the method that can be called to update the Notification
|
* This is the method that can be called to update the Notification
|
||||||
*/
|
*/
|
||||||
@ -305,15 +463,23 @@ class ForegroundService : Service(){
|
|||||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
val notification = NotificationCompat.Builder(this, channelId)
|
val notification = NotificationCompat.Builder(this, channelId)
|
||||||
.setContentTitle("SpotiFlyer: Downloading Your Music")
|
.setContentTitle("SpotiFlyer: Downloading Your Music")
|
||||||
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
|
.setContentText("Total: $total Completed:$converted ")
|
||||||
.setSubText("Speed: $speed KB/s ")
|
.setSubText("Speed: $speed KB/s ")
|
||||||
.setNotificationSilent()
|
.setNotificationSilent()
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setSmallIcon(R.drawable.down_arrowbw)
|
.setSmallIcon(R.drawable.down_arrowbw)
|
||||||
|
.setStyle(NotificationCompat.InboxStyle()
|
||||||
|
.addLine(messageSnippet1)
|
||||||
|
.addLine(messageSnippet2)
|
||||||
|
.addLine(messageSnippet3)
|
||||||
|
.addLine(messageSnippet4))
|
||||||
.build()
|
.build()
|
||||||
mNotificationManager.notify(101, notification)
|
mNotificationManager.notify(notificationId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Modifying Mp3 Tags with MetaData!
|
||||||
|
**/
|
||||||
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
|
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
|
||||||
val id3v1Tag = ID3v1Tag()
|
val id3v1Tag = ID3v1Tag()
|
||||||
id3v1Tag.track = track.disc_number.toString()
|
id3v1Tag.track = track.disc_number.toString()
|
||||||
@ -342,10 +508,22 @@ class ForegroundService : Service(){
|
|||||||
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
||||||
id3v2Tag.copyright = copyrights.joinToString()
|
id3v2Tag.copyright = copyrights.joinToString()
|
||||||
id3v2Tag.url = track.href
|
id3v2Tag.url = track.href
|
||||||
track.let {
|
track.ytCoverUrl?.let {
|
||||||
val file = File(
|
val file = File(
|
||||||
Environment.getExternalStorageDirectory(),
|
Environment.getExternalStorageDirectory(),
|
||||||
DownloadHelper.defaultDir +".Images/" + (it.album!!.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg")
|
DownloadHelper.defaultDir +".Images/" + it.substringAfterLast('/',it) + ".jpeg")
|
||||||
|
Log.i("Mp3Tags editing Tags",file.path)
|
||||||
|
//init array with file length
|
||||||
|
val bytesArray = ByteArray(file.length().toInt())
|
||||||
|
val fis = FileInputStream(file)
|
||||||
|
fis.read(bytesArray) //read file into bytes[]
|
||||||
|
fis.close()
|
||||||
|
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
||||||
|
}
|
||||||
|
track.album?.let {
|
||||||
|
val file = File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
DownloadHelper.defaultDir +".Images/" + (it.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg")
|
||||||
Log.i("Mp3Tags editing Tags",file.path)
|
Log.i("Mp3Tags editing Tags",file.path)
|
||||||
//init array with file length
|
//init array with file length
|
||||||
val bytesArray = ByteArray(file.length().toInt())
|
val bytesArray = ByteArray(file.length().toInt())
|
||||||
|
@ -68,7 +68,6 @@
|
|||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="@drawable/text_background_accented"
|
android:background="@drawable/text_background_accented"
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
android:hint="Link From Spotify"
|
android:hint="Link From Spotify"
|
||||||
@ -78,7 +77,6 @@
|
|||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textColorHint="@color/grey"
|
android:textColorHint="@color/grey"
|
||||||
android:textSize="19sp"
|
android:textSize="19sp"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/image_view"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btn_search"
|
app:layout_constraintEnd_toStartOf="@+id/btn_search"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
@ -103,11 +101,11 @@
|
|||||||
android:id="@+id/image_view"
|
android:id="@+id/image_view"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="3dp"
|
||||||
android:contentDescription="Album Cover"
|
android:contentDescription="Album Cover"
|
||||||
android:foreground="@drawable/gradient"
|
android:foreground="@drawable/gradient"
|
||||||
android:padding="20dp"
|
android:padding="20dp"
|
||||||
android:layout_marginBottom="3dp"
|
|
||||||
android:paddingBottom="10dp"
|
android:paddingBottom="10dp"
|
||||||
android:src="@drawable/spotify_download"
|
android:src="@drawable/spotify_download"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
@ -115,7 +113,29 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/btn_search" />
|
app:layout_constraintTop_toBottomOf="@+id/linkSearch" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/StatusBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="@drawable/text_background_accented"
|
||||||
|
android:fontFamily="@font/raleway_semibold"
|
||||||
|
android:foreground="@drawable/rounded_gradient"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingLeft="12dp"
|
||||||
|
android:paddingTop="1dp"
|
||||||
|
android:paddingRight="12dp"
|
||||||
|
android:paddingBottom="1dp"
|
||||||
|
android:text="Total: 100 Processed: 50"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="@color/grey"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/btn_search"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/linkSearch"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/linkSearch" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -312,7 +332,15 @@
|
|||||||
app:layout_constraintStart_toEndOf="@id/heart"
|
app:layout_constraintStart_toEndOf="@id/heart"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/developer_insta" />
|
app:layout_constraintTop_toBottomOf="@+id/developer_insta" />
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/webView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="300dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
<AppUpdater>
|
<AppUpdater>
|
||||||
<update>
|
<update>
|
||||||
<latestVersion>1.1</latestVersion>
|
<latestVersion>1.2</latestVersion>
|
||||||
<latestVersionCode>2</latestVersionCode>
|
<latestVersionCode>3</latestVersionCode>
|
||||||
<url>https://github.com/Shabinder/SpotiFlyer/releases</url>
|
<url>https://github.com/Shabinder/SpotiFlyer/releases</url>
|
||||||
</update>
|
</update>
|
||||||
</AppUpdater>
|
</AppUpdater>
|
@ -31,7 +31,7 @@ buildscript {
|
|||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
//safe-Args
|
//safe-Args
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
// classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user