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>insta</w>
|
||||
<w>instagram</w>
|
||||
<w>maxresdefault</w>
|
||||
<w>moshi</w>
|
||||
<w>musicforeveryone</w>
|
||||
<w>musicplaceholder</w>
|
||||
@ -15,6 +16,8 @@
|
||||
<w>spotify</w>
|
||||
<w>spotifydownloader</w>
|
||||
<w>spotifyler</w>
|
||||
<w>thru</w>
|
||||
<w>youtu</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
|
@ -20,7 +20,7 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
//apply plugin: 'kotlinx-serialization'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
@ -34,17 +34,28 @@ android {
|
||||
applicationId 'com.shabinder.spotiflyer'
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 29
|
||||
versionCode 2
|
||||
versionName "1.1"
|
||||
versionCode 3
|
||||
versionName "1.2"
|
||||
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 {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -65,6 +76,7 @@ dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.3.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.webkit:webkit:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions: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 'com.google.apis:google-api-services-youtube:v3-rev180-1.22.0'
|
||||
implementation 'com.google.oauth-client:google-oauth-client:1.22.0'
|
||||
// Authentication Way Changed!
|
||||
// implementation ('com.google.apis:google-api-services-youtube:v3-rev180-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.squareup.okhttp3:okhttp:4.8.0'
|
||||
|
||||
@ -97,9 +114,6 @@ dependencies {
|
||||
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
|
||||
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.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.WAKE_LOCK" />
|
||||
<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" />-->
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
package com.shabinder.spotiflyer
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
@ -25,6 +26,8 @@ import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.databinding.DataBindingUtil
|
||||
@ -36,7 +39,6 @@ import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||
import com.shabinder.spotiflyer.utils.SpotifyServiceToken
|
||||
import com.shabinder.spotiflyer.utils.YoutubeInterface
|
||||
import com.shabinder.spotiflyer.utils.createDirectory
|
||||
import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment
|
||||
import com.squareup.moshi.Moshi
|
||||
@ -71,6 +73,8 @@ class MainActivity : AppCompatActivity(){
|
||||
binding = DataBindingUtil.setContentView(this,R.layout.main_activity)
|
||||
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
|
||||
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)) ){
|
||||
val savedToken = sharedPref?.getString("token","error")!!
|
||||
@ -88,6 +92,7 @@ class MainActivity : AppCompatActivity(){
|
||||
}
|
||||
|
||||
requestPermission()
|
||||
disableDozeMode()
|
||||
checkIfLatestVersion()
|
||||
createDir()
|
||||
setUpi()
|
||||
@ -98,12 +103,42 @@ class MainActivity : AppCompatActivity(){
|
||||
//Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"}
|
||||
ytDownloader = YoutubeDownloader()
|
||||
sharedViewModel.ytDownloader = ytDownloader
|
||||
//Initialing Communication with Youtube
|
||||
YoutubeInterface.youtubeConnector()
|
||||
|
||||
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!
|
||||
* */
|
||||
@ -253,6 +288,7 @@ class MainActivity : AppCompatActivity(){
|
||||
createDirectory(DownloadHelper.defaultDir+"Tracks/")
|
||||
createDirectory(DownloadHelper.defaultDir+"Albums/")
|
||||
createDirectory(DownloadHelper.defaultDir+"Playlists/")
|
||||
createDirectory(DownloadHelper.defaultDir+"YT_Downloads/")
|
||||
}
|
||||
|
||||
private fun checkIfLatestVersion() {
|
||||
|
@ -17,28 +17,44 @@
|
||||
|
||||
package com.shabinder.spotiflyer.downloadHelper
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
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 com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.fragments.MainFragment
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.utils.YoutubeInterface
|
||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
object DownloadHelper {
|
||||
|
||||
var webView:WebView? = null
|
||||
var context : Context? = null
|
||||
var statusBar:TextView? = null
|
||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||
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
|
||||
@ -47,108 +63,179 @@ object DownloadHelper {
|
||||
type:String,
|
||||
subFolder: String?,
|
||||
trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
|
||||
var size = trackList.size
|
||||
trackList.forEach {
|
||||
size--
|
||||
if(size == 0){
|
||||
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 )
|
||||
}else{
|
||||
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadTrack(
|
||||
mainFragment: MainFragment? = null,
|
||||
type:String,
|
||||
subFolder:String?,
|
||||
ytDownloader: YoutubeDownloader?,
|
||||
searchQuery: String,
|
||||
track: Track,
|
||||
index: Int? = null
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val data: YoutubeInterface.VideoItem = YoutubeInterface.search(searchQuery)?.get(0)!!
|
||||
|
||||
//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")
|
||||
withContext(Dispatchers.Main){
|
||||
var size = trackList.size
|
||||
total += size
|
||||
animateStatusBar()
|
||||
trackList.forEach {
|
||||
size--
|
||||
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()
|
||||
}else{
|
||||
if(isBrowserLoading){
|
||||
if(size == 0){
|
||||
youtubeList.add(YoutubeRequest(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 ))
|
||||
}else{
|
||||
youtubeList.add(YoutubeRequest(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it))
|
||||
}
|
||||
}else{
|
||||
if(size == 0){
|
||||
getYTLink(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 )
|
||||
}else{
|
||||
getYTLink(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)
|
||||
}
|
||||
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)
|
||||
val format: Format =
|
||||
video?.findAudioWithQuality(quality)?.get(0) as Format
|
||||
Log.i("Format", video.findAudioWithQuality(AudioQuality.medium)?.get(0)!!.mimeType())
|
||||
val audioUrl:String = format.url()
|
||||
Log.i("DHelper Link Found", audioUrl)
|
||||
return audioUrl
|
||||
|
||||
//TODO CleanUp here and there!!
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
suspend fun getYTLink(mainFragment: MainFragment? = null,
|
||||
type:String,
|
||||
subFolder:String?,
|
||||
ytDownloader: YoutubeDownloader?,
|
||||
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) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||
DownloadHelper.defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a")
|
||||
fun downloadFile(subFolder: String?, type: String, track:Track, index:Int? = null,ytDownloader: YoutubeDownloader?,id: String) {
|
||||
sharedViewModel!!.uiScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
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()){
|
||||
val downloadObject = DownloadObject(
|
||||
track = track,
|
||||
url = url,
|
||||
outputDir = outputFile
|
||||
)
|
||||
Log.i("DH",outputFile)
|
||||
if(index==null){
|
||||
Log.i("DHelper Link Found", url)
|
||||
|
||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||
defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a")
|
||||
|
||||
val downloadObject = DownloadObject(
|
||||
track = track,
|
||||
url = url,
|
||||
outputDir = outputFile
|
||||
)
|
||||
Log.i("DH",outputFile)
|
||||
startService(context!!, downloadObject)
|
||||
|
||||
/*if(index==null){
|
||||
downloadList.add(downloadObject)
|
||||
}else{
|
||||
downloadList.add(downloadObject)
|
||||
startService(context!!, downloadList)
|
||||
Log.i("DH No of Songs", downloadList.size.toString())
|
||||
downloadList = arrayListOf()
|
||||
}
|
||||
}else{withContext(Dispatchers.Main){
|
||||
mainFragment?.showToast("${track.name} is already Downloaded")
|
||||
}*/
|
||||
// downloadList.add(downloadObject)
|
||||
// downloadList = arrayListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun startService(context:Context,list: ArrayList<DownloadObject>) {
|
||||
fun startService(context:Context,obj:DownloadObject? = null ) {
|
||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||
serviceIntent.putParcelableArrayListExtra("list",list)
|
||||
serviceIntent.putExtra("object",obj)
|
||||
ContextCompat.startForegroundService(context, serviceIntent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removing Illegal Chars from File Name
|
||||
* **/
|
||||
private fun removeIllegalChars(fileName: String): String? {
|
||||
fun removeIllegalChars(fileName: String): String? {
|
||||
val illegalCharArray = charArrayOf(
|
||||
'/',
|
||||
'\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(), "")
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@ -29,6 +30,9 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.DataBindingUtil
|
||||
@ -45,6 +49,7 @@ import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.applyWebViewSettings
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||
@ -67,23 +72,25 @@ class MainFragment : Fragment() {
|
||||
private var type:String = ""
|
||||
private var spotifyLink = ""
|
||||
private var i: Intent? = null
|
||||
private var webView: WebView? = null
|
||||
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
|
||||
webView = binding.webView
|
||||
DownloadHelper.webView = binding.webView
|
||||
DownloadHelper.context = requireContext()
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
||||
spotifyService = sharedViewModel.spotifyService
|
||||
DownloadHelper.sharedViewModel = sharedViewModel
|
||||
DownloadHelper.statusBar = binding.StatusBar
|
||||
|
||||
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
|
||||
setUpUsageText()
|
||||
openSpotifyButton()
|
||||
openGithubButton()
|
||||
openInstaButton()
|
||||
@ -93,127 +100,14 @@ class MainFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.btnSearch.setOnClickListener {
|
||||
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()
|
||||
val link = binding.linkSearch.text.toString()
|
||||
if(link.contains("open.spotify",true)){
|
||||
spotifySearch()
|
||||
}
|
||||
if(link.contains("youtube.com",true) || link.contains("youtu.be",true) ){
|
||||
youtubeSearch()
|
||||
}
|
||||
|
||||
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()
|
||||
//Handling Device Configuration Change
|
||||
@ -225,6 +119,173 @@ class MainFragment : Fragment() {
|
||||
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.
|
||||
**/
|
||||
@ -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!
|
||||
**/
|
||||
|
@ -17,9 +17,10 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class Followers(
|
||||
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 album: Album? = 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 com.shabinder.spotiflyer.R
|
||||
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.models.Track
|
||||
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.downloadBtn.setOnClickListener{
|
||||
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 {
|
||||
val file = File(
|
||||
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.
|
||||
val options = BitmapFactory.Options()
|
||||
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
|
||||
|
||||
import android.app.*
|
||||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -41,22 +46,41 @@ import com.shabinder.spotiflyer.models.Track
|
||||
import com.tonyodev.fetch2.*
|
||||
import com.tonyodev.fetch2core.DownloadBlock
|
||||
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.FileInputStream
|
||||
|
||||
class ForegroundService : Service(){
|
||||
private val tag = "Foreground Service"
|
||||
private val channelId = "ForegroundDownloaderService"
|
||||
private val notificationId = 101
|
||||
private var total = 0 //Total Downloads Requested
|
||||
private var converted = 0//Total Files Converted
|
||||
private var downloaded = 0//Total Files downloaded
|
||||
private var fetch:Fetch? = null
|
||||
private var downloadManager : DownloadManager? = null
|
||||
private var downloadList = mutableListOf<DownloadObject>()
|
||||
private var serviceJob = Job()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||
private val requestMap = mutableMapOf<Request,Track>()
|
||||
private val downloadMap = mutableMapOf<String,Track>()
|
||||
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? {
|
||||
return null
|
||||
@ -69,26 +93,22 @@ class ForegroundService : Service(){
|
||||
this,
|
||||
0, notificationIntent, 0
|
||||
)
|
||||
val notification = NotificationCompat.Builder(this, channelId)
|
||||
.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()
|
||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
val fetchConfiguration =
|
||||
FetchConfiguration.Builder(this)
|
||||
.setDownloadConcurrentLimit(4)
|
||||
.build()
|
||||
|
||||
Fetch.Impl.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||
fetch = Fetch.getDefaultInstance()
|
||||
fetch = Fetch.Impl.getInstance(fetchConfiguration)
|
||||
// fetch?.enableLogging(true)
|
||||
fetch?.addListener(fetchListener)
|
||||
startForeground()
|
||||
}
|
||||
|
||||
/**
|
||||
*Starting Service with Notification as Foreground!
|
||||
**/
|
||||
private fun startForeground() {
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@ -105,10 +125,15 @@ class ForegroundService : Service(){
|
||||
.setSubText("Speed: $speed KB/s ")
|
||||
.setNotificationSilent()
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
|
||||
.setContentText("Total: $total Downloaded: $downloaded Completed:$converted ")
|
||||
.setSmallIcon(R.drawable.down_arrowbw)
|
||||
.setStyle(NotificationCompat.InboxStyle()
|
||||
.addLine(messageSnippet1)
|
||||
.addLine(messageSnippet2)
|
||||
.addLine(messageSnippet3)
|
||||
.addLine(messageSnippet4))
|
||||
.build()
|
||||
startForeground(101, notification)
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@ -126,52 +151,107 @@ class ForegroundService : Service(){
|
||||
Log.i(tag,"Service Started.")
|
||||
|
||||
//do heavy work on a background thread
|
||||
// val list = intent.getSerializableExtra("list") as List<Any?>
|
||||
val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
|
||||
Log.i(tag,"Intent List Size: ${list!!.size}")
|
||||
total += list.size
|
||||
list.forEach { downloadList.add(it as DownloadObject) }
|
||||
|
||||
serviceScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
for (downloadObject in downloadList) {
|
||||
val request= Request(downloadObject.url, downloadObject.outputDir)
|
||||
//val list = intent.getSerializableExtra("list") as List<Any?>
|
||||
// val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
|
||||
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
||||
val obj = intent.getParcelableExtra<DownloadObject>("object") ?: intent.extras?.getParcelable<DownloadObject>("object")
|
||||
obj?.let {
|
||||
total ++
|
||||
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
||||
updateNotification()
|
||||
serviceScope.launch {
|
||||
val request= Request(obj.url, obj.outputDir)
|
||||
request.priority = Priority.NORMAL
|
||||
request.networkType = NetworkType.ALL
|
||||
|
||||
fetch?.enqueue(request,
|
||||
fetch!!.enqueue(request,
|
||||
Func {
|
||||
Log.i("DownloadManager", "Download Request Sent")
|
||||
requestMap[it] = downloadObject.track
|
||||
downloadList.remove(downloadObject) },
|
||||
requestMap[it] = obj.track
|
||||
downloadList.remove(obj)
|
||||
Log.i(tag, "Enqueuing Download")
|
||||
},
|
||||
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() {
|
||||
super.onDestroy()
|
||||
if(downloadMap.isEmpty() && converted == total){
|
||||
Log.i(tag,"Service destroyed.")
|
||||
fetch?.close()
|
||||
deleteFile(parentDirectory)
|
||||
releaseWakeLock()
|
||||
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?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
if(downloadMap.isEmpty() && converted == total ){
|
||||
Log.i(tag,"Service destroyed.")
|
||||
Log.i(tag,"Service Removed.")
|
||||
fetch?.close()
|
||||
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 {
|
||||
override fun onQueued(
|
||||
download: Download,
|
||||
@ -194,7 +274,32 @@ class ForegroundService : Service(){
|
||||
totalBlocks: Int
|
||||
) {
|
||||
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")
|
||||
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) {
|
||||
@ -211,13 +316,21 @@ class ForegroundService : Service(){
|
||||
|
||||
override fun onCompleted(download: Download) {
|
||||
val track = requestMap[download.request]
|
||||
speed = 0
|
||||
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")
|
||||
requestMap.remove(download.request)
|
||||
updateNotification()
|
||||
downloaded++
|
||||
requestMap.remove(download.request)
|
||||
if(requestMap.keys.toList().isEmpty()) speed = 0
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
override fun onDeleted(download: Download) {
|
||||
@ -233,7 +346,13 @@ class ForegroundService : Service(){
|
||||
}
|
||||
|
||||
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
||||
val track = requestMap[download.request]
|
||||
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) {
|
||||
@ -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){
|
||||
val m4aFile = File(filePath)
|
||||
|
||||
val executionId = FFmpeg.executeAsync(
|
||||
"-i $filePath -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
|
||||
FFmpeg.executeAsync(
|
||||
"-i $filePath -b:a 160k -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
|
||||
) { _, returnCode ->
|
||||
when (returnCode) {
|
||||
RETURN_CODE_SUCCESS -> {
|
||||
@ -292,11 +450,11 @@ class ForegroundService : Service(){
|
||||
updateNotification()
|
||||
//All tasks completed (REST IN PEACE)
|
||||
if(converted == total){
|
||||
stopForeground(false)
|
||||
stopSelf()
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
val notification = NotificationCompat.Builder(this, channelId)
|
||||
.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 ")
|
||||
.setNotificationSilent()
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(R.drawable.down_arrowbw)
|
||||
.setStyle(NotificationCompat.InboxStyle()
|
||||
.addLine(messageSnippet1)
|
||||
.addLine(messageSnippet2)
|
||||
.addLine(messageSnippet3)
|
||||
.addLine(messageSnippet4))
|
||||
.build()
|
||||
mNotificationManager.notify(101, notification)
|
||||
mNotificationManager.notify(notificationId, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
*Modifying Mp3 Tags with MetaData!
|
||||
**/
|
||||
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
|
||||
val id3v1Tag = ID3v1Tag()
|
||||
id3v1Tag.track = track.disc_number.toString()
|
||||
@ -342,10 +508,22 @@ class ForegroundService : Service(){
|
||||
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
||||
id3v2Tag.copyright = copyrights.joinToString()
|
||||
id3v2Tag.url = track.href
|
||||
track.let {
|
||||
track.ytCoverUrl?.let {
|
||||
val file = File(
|
||||
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)
|
||||
//init array with file length
|
||||
val bytesArray = ByteArray(file.length().toInt())
|
||||
|
@ -68,7 +68,6 @@
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/text_background_accented"
|
||||
android:ems="10"
|
||||
android:hint="Link From Spotify"
|
||||
@ -78,7 +77,6 @@
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/grey"
|
||||
android:textSize="19sp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/image_view"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_search"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
@ -103,11 +101,11 @@
|
||||
android:id="@+id/image_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="3dp"
|
||||
android:contentDescription="Album Cover"
|
||||
android:foreground="@drawable/gradient"
|
||||
android:padding="20dp"
|
||||
android:layout_marginBottom="3dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:src="@drawable/spotify_download"
|
||||
android:visibility="visible"
|
||||
@ -115,7 +113,29 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="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>
|
||||
@ -312,7 +332,15 @@
|
||||
app:layout_constraintStart_toEndOf="@id/heart"
|
||||
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.core.widget.NestedScrollView>
|
||||
|
@ -18,8 +18,8 @@
|
||||
|
||||
<AppUpdater>
|
||||
<update>
|
||||
<latestVersion>1.1</latestVersion>
|
||||
<latestVersionCode>2</latestVersionCode>
|
||||
<latestVersion>1.2</latestVersion>
|
||||
<latestVersionCode>3</latestVersionCode>
|
||||
<url>https://github.com/Shabinder/SpotiFlyer/releases</url>
|
||||
</update>
|
||||
</AppUpdater>
|
@ -31,7 +31,7 @@ buildscript {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
//safe-Args
|
||||
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
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user