mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-25 02:14:32 +01:00
Youtube Playlist and more support
This commit is contained in:
parent
099a103e98
commit
c0e3a35898
@ -1,6 +1,7 @@
|
|||||||
<component name="ProjectDictionaryState">
|
<component name="ProjectDictionaryState">
|
||||||
<dictionary name="shabinder">
|
<dictionary name="shabinder">
|
||||||
<words>
|
<words>
|
||||||
|
<w>cherrypick</w>
|
||||||
<w>downloadrecord</w>
|
<w>downloadrecord</w>
|
||||||
<w>emoji</w>
|
<w>emoji</w>
|
||||||
<w>ffmpeg</w>
|
<w>ffmpeg</w>
|
||||||
|
@ -118,6 +118,7 @@ dependencies {
|
|||||||
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
|
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||||
implementation 'com.beust:klaxon:5.4'
|
implementation 'com.beust:klaxon:5.4'
|
||||||
|
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
|
||||||
|
|
||||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||||
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
|
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
|
||||||
|
@ -35,10 +35,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import com.github.javiersantos.appupdater.AppUpdater
|
import com.github.javiersantos.appupdater.AppUpdater
|
||||||
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
||||||
import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
import com.shabinder.spotiflyer.utils.Provider.activity
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyServiceTokenRequest
|
import com.shabinder.spotiflyer.utils.SpotifyServiceTokenRequest
|
||||||
import com.shabinder.spotiflyer.utils.createDirectories
|
import com.shabinder.spotiflyer.utils.createDirectories
|
||||||
|
import com.shabinder.spotiflyer.utils.startService
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -84,7 +85,7 @@ class MainActivity : AppCompatActivity(){
|
|||||||
Log.i("Connection Status", isConnected.toString())
|
Log.i("Connection Status", isConnected.toString())
|
||||||
|
|
||||||
//starting Notification and Downloader Service!
|
//starting Notification and Downloader Service!
|
||||||
SpotifyDownloadHelper.startService(this)
|
startService(this)
|
||||||
|
|
||||||
handleIntentFromExternalActivity()
|
handleIntentFromExternalActivity()
|
||||||
}
|
}
|
||||||
@ -227,5 +228,6 @@ class MainActivity : AppCompatActivity(){
|
|||||||
}
|
}
|
||||||
init {
|
init {
|
||||||
instance = this
|
instance = this
|
||||||
|
activity = this
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,25 +17,20 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.downloadHelper
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
import android.content.Context
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AlphaAnimation
|
import android.view.animation.AlphaAnimation
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import android.widget.Toast
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
import com.shabinder.spotiflyer.models.*
|
||||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
import com.shabinder.spotiflyer.utils.*
|
||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
import com.shabinder.spotiflyer.utils.Provider.activity
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
|
|
||||||
import com.shabinder.spotiflyer.utils.YoutubeMusicApi
|
|
||||||
import com.shabinder.spotiflyer.utils.getEmojiByUnicode
|
|
||||||
import com.shabinder.spotiflyer.utils.makeJsonBody
|
|
||||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -45,13 +40,13 @@ import retrofit2.Response
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object SpotifyDownloadHelper {
|
object SpotifyDownloadHelper {
|
||||||
var context : Context? = null
|
|
||||||
var statusBar:TextView? = null
|
var statusBar:TextView? = null
|
||||||
var youtubeMusicApi:YoutubeMusicApi? = null
|
var youtubeMusicApi:YoutubeMusicApi? = null
|
||||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
var sharedViewModel: SharedViewModel? = null
|
||||||
var spotifyViewModel: SpotifyViewModel? = null
|
|
||||||
var total = 0
|
private var total = 0
|
||||||
var Processed = 0
|
private var processed = 0
|
||||||
var notFound = 0
|
var notFound = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,16 +55,94 @@ object SpotifyDownloadHelper {
|
|||||||
suspend fun downloadAllTracks(
|
suspend fun downloadAllTracks(
|
||||||
type:String,
|
type:String,
|
||||||
subFolder: String?,
|
subFolder: String?,
|
||||||
trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
|
trackList: List<Track>) {
|
||||||
|
val downloadList = ArrayList<DownloadObject>()
|
||||||
|
|
||||||
withContext(Dispatchers.Main){
|
withContext(Dispatchers.Main){
|
||||||
total += trackList.size // Adding New Download List Count to StatusBar
|
total += trackList.size // Adding New Download List Count to StatusBar
|
||||||
trackList.forEach {
|
trackList.forEachIndexed { index, it ->
|
||||||
if(it.downloaded == "Downloaded"){//Download Already Present!!
|
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
|
||||||
Processed++
|
processed++
|
||||||
|
if(index == (trackList.size-1)){//LastElement
|
||||||
|
Handler().postDelayed({
|
||||||
|
//Delay is Added ,if a request is in processing it may finish
|
||||||
|
Log.i("Spotify Helper","Download Request Sent")
|
||||||
|
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
|
||||||
|
Toast.makeText(activity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
startService(activity,downloadList)
|
||||||
|
},5000)
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
val artistsList = mutableListOf<String>()
|
val artistsList = mutableListOf<String>()
|
||||||
it.artists?.forEach { artist -> artistsList.add(artist!!.name!!) }
|
it.artists?.forEach { artist -> artistsList.add(artist!!.name!!) }
|
||||||
searchYTMusic(type,subFolder,ytDownloader,"${it.name} - ${artistsList.joinToString(",")}", it)
|
val searchQuery = "${it.name} - ${artistsList.joinToString(",")}"
|
||||||
|
|
||||||
|
val jsonBody = makeJsonBody(searchQuery.trim())
|
||||||
|
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
|
||||||
|
object : Callback<String>{
|
||||||
|
override fun onResponse(call: Call<String>, response: Response<String>) {
|
||||||
|
sharedViewModel?.uiScope?.launch {
|
||||||
|
val videoId = sortByBestMatch(
|
||||||
|
getYTTracks(response.body().toString()),
|
||||||
|
trackName = it.name.toString(),
|
||||||
|
trackArtists = artistsList,
|
||||||
|
trackDurationSec = (it.duration_ms/1000).toInt()
|
||||||
|
).keys.firstOrNull()
|
||||||
|
Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
|
||||||
|
|
||||||
|
if(videoId.isNullOrBlank()) {notFound++ ; updateStatusBar()}
|
||||||
|
else {//Found Youtube Video ID
|
||||||
|
val trackDetails = TrackDetails(
|
||||||
|
title = it.name.toString(),
|
||||||
|
artists = artistsList,
|
||||||
|
durationSec = (it.duration_ms/1000).toInt(),
|
||||||
|
albumArt = File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + (it.album?.images?.get(0)?.url.toString()).substringAfterLast('/') + ".jpeg"),
|
||||||
|
albumName = it.album?.name,
|
||||||
|
year = it.album?.release_date,
|
||||||
|
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||||
|
trackUrl = it.href,
|
||||||
|
source = Source.Spotify
|
||||||
|
)
|
||||||
|
|
||||||
|
val outputFile: String =
|
||||||
|
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
|
defaultDir +
|
||||||
|
removeIllegalChars(type) + File.separator +
|
||||||
|
(if (subFolder == null) { "" }
|
||||||
|
else { removeIllegalChars(subFolder) + File.separator }
|
||||||
|
+ removeIllegalChars(it.name!!) + ".m4a")
|
||||||
|
|
||||||
|
val downloadObject = DownloadObject(
|
||||||
|
trackDetails = trackDetails,
|
||||||
|
ytVideoId = videoId,
|
||||||
|
outputFile = outputFile
|
||||||
|
)
|
||||||
|
processed++
|
||||||
|
sharedViewModel?.uiScope?.launch(Dispatchers.Main) {
|
||||||
|
updateStatusBar()
|
||||||
|
}
|
||||||
|
downloadList.add(downloadObject)
|
||||||
|
if(index == (trackList.size-1)){//LastElement
|
||||||
|
Handler().postDelayed({
|
||||||
|
//Delay is Added ,if a request is in processing it may finish
|
||||||
|
Log.i("Spotify Helper","Download Request Sent")
|
||||||
|
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
|
||||||
|
Toast.makeText(activity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
startService(activity,downloadList)
|
||||||
|
},5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onFailure(call: Call<String>, t: Throwable) {
|
||||||
|
Log.i("YT API Req. Fail",t.message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
updateStatusBar()
|
updateStatusBar()
|
||||||
}
|
}
|
||||||
@ -77,140 +150,18 @@ object SpotifyDownloadHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun searchYTMusic(type:String,
|
|
||||||
subFolder:String?,
|
|
||||||
ytDownloader: YoutubeDownloader?,
|
|
||||||
searchQuery: String,
|
|
||||||
track: Track){
|
|
||||||
val jsonBody = makeJsonBody(searchQuery.trim())
|
|
||||||
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
|
|
||||||
object : Callback<String>{
|
|
||||||
override fun onResponse(call: Call<String>, response: Response<String>) {
|
|
||||||
spotifyViewModel?.uiScope?.launch {
|
|
||||||
Log.i("YT API BODY",response.body().toString())
|
|
||||||
Log.i("YT Search Query",searchQuery)
|
|
||||||
getYTLink(type,subFolder,ytDownloader,response.body().toString(),track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<String>, t: Throwable) {
|
|
||||||
Log.i("YT API Fail",t.message.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun updateStatusBar() {
|
|
||||||
statusBar!!.visibility = View.VISIBLE
|
|
||||||
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $Processed ${getEmojiByUnicode(0x274C)}: $notFound"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun downloadFile(subFolder: String?, type: String, track:Track, ytDownloader: YoutubeDownloader?, id: String) {
|
|
||||||
spotifyViewModel!!.uiScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
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()
|
|
||||||
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)
|
|
||||||
Processed++
|
|
||||||
spotifyViewModel?.uiScope?.launch(Dispatchers.Main) {
|
|
||||||
updateStatusBar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
|
||||||
Log.i("DH", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startService(context:Context,obj:DownloadObject? = null ) {
|
|
||||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
|
||||||
obj?.let { serviceIntent.putExtra("object",it) }
|
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removing Illegal Chars from File Name
|
|
||||||
* **/
|
|
||||||
fun removeIllegalChars(fileName: String): String? {
|
|
||||||
val illegalCharArray = charArrayOf(
|
|
||||||
'/',
|
|
||||||
'\n',
|
|
||||||
'\r',
|
|
||||||
'\t',
|
|
||||||
'\u0000',
|
|
||||||
'\u000C',
|
|
||||||
'`',
|
|
||||||
'?',
|
|
||||||
'*',
|
|
||||||
'\\',
|
|
||||||
'<',
|
|
||||||
'>',
|
|
||||||
'|',
|
|
||||||
'\"',
|
|
||||||
'.',
|
|
||||||
'-',
|
|
||||||
'\''
|
|
||||||
)
|
|
||||||
|
|
||||||
var name = fileName
|
|
||||||
for (c in illegalCharArray) {
|
|
||||||
name = fileName.replace(c, '_')
|
|
||||||
}
|
|
||||||
name = name.replace("\\s".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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateStatusBar() {
|
private fun animateStatusBar() {
|
||||||
val anim: Animation = AlphaAnimation(0.0f, 0.9f)
|
val anim: Animation = AlphaAnimation(0.3f, 0.9f)
|
||||||
anim.duration = 650 //You can manage the blinking time with this parameter
|
anim.duration = 1500 //You can manage the blinking time with this parameter
|
||||||
anim.startOffset = 20
|
anim.startOffset = 20
|
||||||
anim.repeatMode = Animation.REVERSE
|
anim.repeatMode = Animation.REVERSE
|
||||||
anim.repeatCount = Animation.INFINITE
|
anim.repeatCount = Animation.INFINITE
|
||||||
statusBar?.animation = anim
|
statusBar?.animation = anim
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
fun updateStatusBar() {
|
||||||
|
statusBar!!.visibility = View.VISIBLE
|
||||||
|
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound"
|
||||||
|
}
|
||||||
}
|
}
|
@ -17,49 +17,48 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.downloadHelper
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.widget.Toast
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
|
||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
import com.shabinder.spotiflyer.utils.Provider.activity
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
|
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||||
|
import com.shabinder.spotiflyer.utils.startService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object YTDownloadHelper {
|
object YTDownloadHelper {
|
||||||
var context : Context? = null
|
suspend fun downloadYTTracks(
|
||||||
var statusBar: TextView? = null
|
type:String,
|
||||||
|
subFolder: String?,
|
||||||
fun downloadFile(subFolder: String?, type: String,ytTrack: Track,format: Format?) {
|
tracks:List<TrackDetails>,
|
||||||
format?.let {
|
){
|
||||||
val url:String = format.url()
|
val downloadList = ArrayList<DownloadObject>()
|
||||||
// Log.i("DHelper Link Found", url)
|
tracks.forEach {
|
||||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
val outputFile: String =
|
||||||
SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator} + SpotifyDownloadHelper.removeIllegalChars(
|
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
ytTrack.name!!
|
defaultDir +
|
||||||
) +".m4a")
|
removeIllegalChars(type) + File.separator +
|
||||||
|
(if (subFolder == null) { "" }
|
||||||
|
else { removeIllegalChars(subFolder) + File.separator }
|
||||||
|
+ removeIllegalChars(it.title) + ".m4a")
|
||||||
|
|
||||||
val downloadObject = DownloadObject(
|
val downloadObject = DownloadObject(
|
||||||
track = ytTrack,
|
trackDetails = it,
|
||||||
url = url,
|
ytVideoId = "https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
|
||||||
outputDir = outputFile
|
.substringBeforeLast(".")}/maxresdefault.jpg",
|
||||||
|
outputFile = outputFile
|
||||||
)
|
)
|
||||||
Log.i("DH",outputFile)
|
|
||||||
startService(context!!, downloadObject)
|
downloadList.add(downloadObject)
|
||||||
statusBar?.visibility= View.VISIBLE
|
}
|
||||||
|
Log.i("YT Downloader Helper","Download Request Sent")
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
Toast.makeText(activity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
|
||||||
|
startService(activity,downloadList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun startService(context:Context, obj: DownloadObject? = null ) {
|
|
||||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
|
||||||
serviceIntent.putExtra("object",obj)
|
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -17,34 +17,26 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.downloadHelper
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.beust.klaxon.JsonArray
|
import com.beust.klaxon.JsonArray
|
||||||
import com.beust.klaxon.JsonObject
|
import com.beust.klaxon.JsonObject
|
||||||
import com.beust.klaxon.Parser
|
import com.beust.klaxon.Parser
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.downloadFile
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.notFound
|
|
||||||
import com.shabinder.spotiflyer.models.Track
|
|
||||||
import com.shabinder.spotiflyer.models.YoutubeTrack
|
import com.shabinder.spotiflyer.models.YoutubeTrack
|
||||||
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Thanks and credits To https://github.com/spotDL/spotify-downloader
|
* Thanks To https://github.com/spotDL/spotify-downloader
|
||||||
* */
|
* */
|
||||||
fun getYTLink(type:String,
|
fun getYTTracks(response: String):List<YoutubeTrack>{
|
||||||
subFolder:String?,
|
|
||||||
ytDownloader: YoutubeDownloader?,
|
|
||||||
response: String,
|
|
||||||
track: Track
|
|
||||||
){
|
|
||||||
//TODO Download File
|
|
||||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
val parser: Parser = Parser.default()
|
|
||||||
val stringBuilder: StringBuilder = StringBuilder(response)
|
val stringBuilder: StringBuilder = StringBuilder(response)
|
||||||
val responseObj: JsonObject = parser.parse(stringBuilder) as JsonObject
|
val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject
|
||||||
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
|
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
|
||||||
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
||||||
if (contentBlocks != null) {
|
if (contentBlocks != null) {
|
||||||
Log.i("Total Content Blocks:", contentBlocks.size.toString())
|
|
||||||
for (cBlock in contentBlocks){
|
for (cBlock in contentBlocks){
|
||||||
/**
|
/**
|
||||||
*Ignore user-suggestion
|
*Ignore user-suggestion
|
||||||
@ -109,8 +101,6 @@ fun getYTLink(type:String,
|
|||||||
! we do so only if their Type is 'Song' or 'Video
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
*/
|
*/
|
||||||
|
|
||||||
val simplifiedResults = mutableListOf<JsonObject>()
|
|
||||||
|
|
||||||
for(result in resultBlocks){
|
for(result in resultBlocks){
|
||||||
|
|
||||||
// Blindly gather available details
|
// Blindly gather available details
|
||||||
@ -126,7 +116,7 @@ fun getYTLink(type:String,
|
|||||||
! other constituents of a result block will lead to errors, hence the 'in
|
! other constituents of a result block will lead to errors, hence the 'in
|
||||||
! result[:-1] ,i.e., skip last element in array '
|
! result[:-1] ,i.e., skip last element in array '
|
||||||
*/
|
*/
|
||||||
for(detail in result.subList(0,result.size-2)){
|
for(detail in result.subList(0,result.size-1)){
|
||||||
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
||||||
|
|
||||||
// if not a dummy, collect All Variables
|
// if not a dummy, collect All Variables
|
||||||
@ -138,7 +128,7 @@ fun getYTLink(type:String,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Log.i("Text Api",availableDetails.toString())
|
||||||
/*
|
/*
|
||||||
! Filter Out non-Song/Video results and incomplete results here itself
|
! Filter Out non-Song/Video results and incomplete results here itself
|
||||||
! From what we know about detail order, note that [1] - indicate result type
|
! From what we know about detail order, note that [1] - indicate result type
|
||||||
@ -146,14 +136,7 @@ fun getYTLink(type:String,
|
|||||||
if ( availableDetails.size > 1 && availableDetails[1] in listOf("Song","Video") ){
|
if ( availableDetails.size > 1 && availableDetails[1] in listOf("Song","Video") ){
|
||||||
|
|
||||||
// skip if result is in hours instead of minutes (no song is that long)
|
// skip if result is in hours instead of minutes (no song is that long)
|
||||||
// if(availableDetails[4].split(':').size != 2) continue TODO
|
if(availableDetails[4].split(':').size != 2) continue //Has Been Giving Issues
|
||||||
|
|
||||||
/*
|
|
||||||
! grab position of result
|
|
||||||
! This helps for those oddball cases where 2+ results are rated equally,
|
|
||||||
! lower position --> better match
|
|
||||||
*/
|
|
||||||
val resultPosition = resultBlocks.indexOf(result)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
! grab Video ID
|
! grab Video ID
|
||||||
@ -168,23 +151,86 @@ fun getYTLink(type:String,
|
|||||||
name = availableDetails[0],
|
name = availableDetails[0],
|
||||||
type = availableDetails[1],
|
type = availableDetails[1],
|
||||||
artist = availableDetails[2],
|
artist = availableDetails[2],
|
||||||
|
duration = availableDetails[4],
|
||||||
videoId = videoId
|
videoId = videoId
|
||||||
)
|
)
|
||||||
youtubeTracks.add(ytTrack)
|
youtubeTracks.add(ytTrack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Songs First, Videos Later
|
|
||||||
youtubeTracks.sortWith { o1: YoutubeTrack, o2: YoutubeTrack -> o1.type.toString().compareTo(o2.type.toString()) }
|
|
||||||
|
|
||||||
if(youtubeTracks.firstOrNull()?.videoId.isNullOrBlank()) notFound++
|
return youtubeTracks
|
||||||
else downloadFile(
|
}
|
||||||
subFolder,
|
|
||||||
type,
|
@SuppressLint("DefaultLocale")
|
||||||
track,
|
fun sortByBestMatch(ytTracks:List<YoutubeTrack>,
|
||||||
ytDownloader,
|
trackName:String,
|
||||||
id = youtubeTracks[0].videoId.toString()
|
trackArtists:List<String>,
|
||||||
)
|
trackDurationSec:Int,
|
||||||
Log.i("DHelper YT ID", youtubeTracks.firstOrNull()?.videoId ?: "Not Found")
|
):Map<String,Int>{
|
||||||
SpotifyDownloadHelper.updateStatusBar()
|
/*
|
||||||
|
* "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value
|
||||||
|
**/
|
||||||
|
val linksWithMatchValue = mutableMapOf<String,Int>()
|
||||||
|
|
||||||
|
for (result in ytTracks){
|
||||||
|
|
||||||
|
// LoweCasing Name to match Properly
|
||||||
|
// most song results on youtube go by $artist - $songName or artist1/artist2
|
||||||
|
var hasCommonWord = false
|
||||||
|
|
||||||
|
val resultName = result.name?.toLowerCase()?.replace("-"," ")?.replace("/"," ") ?: ""
|
||||||
|
val trackNameWords = trackName.toLowerCase().split(" ")
|
||||||
|
|
||||||
|
for (nameWord in trackNameWords){
|
||||||
|
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord,resultName) > 85) hasCommonWord = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip this Result if No Word is Common in Name
|
||||||
|
if (!hasCommonWord) {
|
||||||
|
Log.i("YT Api Removing", result.toString())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find artist match
|
||||||
|
// Will Be Using Fuzzy Search Because YT Spelling might be mucked up
|
||||||
|
// match = (no of artist names in result) / (no. of artist names on spotify) * 100
|
||||||
|
var artistMatchNumber = 0
|
||||||
|
|
||||||
|
if(result.type == "Song"){
|
||||||
|
for (artist in trackArtists){
|
||||||
|
if(FuzzySearch.ratio(artist.toLowerCase(),result.artist?.toLowerCase()) > 85)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}else{//i.e. is a Video
|
||||||
|
for (artist in trackArtists) {
|
||||||
|
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(artistMatchNumber == 0) {
|
||||||
|
Log.i("YT Api Removing", result.toString())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
|
||||||
|
|
||||||
|
// Duration Match
|
||||||
|
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
||||||
|
! difference in song duration (delta) is usually of the magnitude of a few
|
||||||
|
! seconds, we need to amplify the delta if it is to have any meaningful impact
|
||||||
|
! wen we calculate the avg match value*/
|
||||||
|
val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60)
|
||||||
|
?.plus(result.duration?.split(":")?.get(1)?.toInt()?:0)
|
||||||
|
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
||||||
|
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
|
||||||
|
val durationMatch = 100 - (nonMatchValue*100)
|
||||||
|
|
||||||
|
val avgMatch = (artistMatch + durationMatch)/2
|
||||||
|
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
||||||
|
}
|
||||||
|
Log.i("YT Api Result", "$trackName - $linksWithMatchValue")
|
||||||
|
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
|
||||||
}
|
}
|
||||||
|
@ -19,11 +19,32 @@ package com.shabinder.spotiflyer.models
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class DownloadObject(
|
data class DownloadObject(
|
||||||
var ytVideo: YTTrack?=null,
|
var trackDetails: TrackDetails,
|
||||||
var track: Track?=null,
|
var ytVideoId:String,
|
||||||
var url:String,
|
var outputFile:String
|
||||||
var outputDir:String
|
|
||||||
):Parcelable
|
):Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class TrackDetails(
|
||||||
|
var title:String,
|
||||||
|
var artists:List<String>,
|
||||||
|
var durationSec:Int,
|
||||||
|
var albumName:String?=null,
|
||||||
|
var year:String?=null,
|
||||||
|
var comment:String?=null,
|
||||||
|
var lyrics:String?=null,
|
||||||
|
var trackUrl:String?=null,
|
||||||
|
var albumArt: File,
|
||||||
|
var source:Source,
|
||||||
|
var downloaded:DownloadStatus = DownloadStatus.NotDownloaded
|
||||||
|
):Parcelable
|
||||||
|
|
||||||
|
enum class DownloadStatus{
|
||||||
|
Downloaded,
|
||||||
|
Downloading,
|
||||||
|
NotDownloaded
|
||||||
|
}
|
@ -31,7 +31,6 @@ data class Track(
|
|||||||
var explicit: Boolean? = null,
|
var explicit: Boolean? = null,
|
||||||
var external_urls: Map<String?, String?>? = null,
|
var external_urls: Map<String?, String?>? = null,
|
||||||
var href: String? = null,
|
var href: String? = null,
|
||||||
var id: String? = null,
|
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var preview_url: String? = null,
|
var preview_url: String? = null,
|
||||||
var track_number: Int = 0,
|
var track_number: Int = 0,
|
||||||
@ -40,5 +39,5 @@ data class Track(
|
|||||||
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,
|
var popularity: Int? = null,
|
||||||
var ytCoverUrl:String? = null,
|
var downloaded:DownloadStatus? = DownloadStatus.NotDownloaded):Parcelable
|
||||||
var downloaded:String? = "notDownloaded"):Parcelable
|
|
||||||
|
@ -25,5 +25,6 @@ data class YoutubeTrack(
|
|||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var type: String? = null, // Song / Video
|
var type: String? = null, // Song / Video
|
||||||
var artist: String? = null,
|
var artist: String? = null,
|
||||||
|
var duration:String? = null,
|
||||||
var videoId: String? = null
|
var videoId: String? = null
|
||||||
):Parcelable
|
):Parcelable
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.recyclerView
|
package com.shabinder.spotiflyer.recyclerView
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -24,13 +25,14 @@ import android.widget.Toast
|
|||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.context
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.downloadAllTracks
|
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.downloadAllTracks
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.Source
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Track
|
||||||
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
|
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.activity
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.utils.bindImage
|
||||||
import com.shabinder.spotiflyer.utils.rotateAnim
|
import com.shabinder.spotiflyer.utils.rotateAnim
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -40,7 +42,6 @@ class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
|
|||||||
|
|
||||||
var spotifyViewModel : SpotifyViewModel? = null
|
var spotifyViewModel : SpotifyViewModel? = null
|
||||||
var isAlbum:Boolean = false
|
var isAlbum:Boolean = false
|
||||||
var ytDownloader: YoutubeDownloader? = null
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@ -49,13 +50,14 @@ class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
|
|||||||
return ViewHolder(binding)
|
return ViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
if(itemCount ==1 || isAlbum){
|
if(itemCount ==1 || isAlbum){
|
||||||
holder.binding.imageUrl.visibility = View.GONE}else{
|
holder.binding.imageUrl.visibility = View.GONE}else{
|
||||||
spotifyViewModel!!.uiScope.launch {
|
spotifyViewModel!!.uiScope.launch {
|
||||||
//Placeholder Set
|
//Placeholder Set
|
||||||
bindImage(holder.binding.imageUrl, item.album!!.images?.get(0)?.url)
|
bindImage(holder.binding.imageUrl, item.album?.images?.get(0)?.url,Source.Spotify)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,26 +65,26 @@ class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
|
|||||||
holder.binding.artist.text = "${item.artists?.get(0)?.name?:""}..."
|
holder.binding.artist.text = "${item.artists?.get(0)?.name?:""}..."
|
||||||
holder.binding.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
holder.binding.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
||||||
when (item.downloaded) {
|
when (item.downloaded) {
|
||||||
"Downloaded" -> {
|
DownloadStatus.Downloaded -> {
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
|
||||||
holder.binding.btnDownload.clearAnimation()
|
holder.binding.btnDownload.clearAnimation()
|
||||||
}
|
}
|
||||||
"Downloading" -> {
|
DownloadStatus.Downloading -> {
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
||||||
rotateAnim(holder.binding.btnDownload)
|
rotateAnim(holder.binding.btnDownload)
|
||||||
}
|
}
|
||||||
"notDownloaded" -> {
|
DownloadStatus.NotDownloaded -> {
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
|
||||||
holder.binding.btnDownload.clearAnimation()
|
holder.binding.btnDownload.clearAnimation()
|
||||||
holder.binding.btnDownload.setOnClickListener{
|
holder.binding.btnDownload.setOnClickListener{
|
||||||
Toast.makeText(context,"Starting Download",Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity,"Processing!",Toast.LENGTH_SHORT).show()
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
||||||
rotateAnim(it)
|
rotateAnim(it)
|
||||||
item.downloaded = "Downloading"
|
item.downloaded = DownloadStatus.Downloading
|
||||||
spotifyViewModel!!.uiScope.launch {
|
spotifyViewModel!!.uiScope.launch {
|
||||||
val itemList = mutableListOf<Track>()
|
val itemList = mutableListOf<Track>()
|
||||||
itemList.add(item)
|
itemList.add(item)
|
||||||
downloadAllTracks(spotifyViewModel!!.folderType,spotifyViewModel!!.subFolder,itemList,ytDownloader)
|
downloadAllTracks(spotifyViewModel!!.folderType,spotifyViewModel!!.subFolder,itemList)
|
||||||
}
|
}
|
||||||
notifyItemChanged(position)//start showing anim!
|
notifyItemChanged(position)//start showing anim!
|
||||||
}
|
}
|
||||||
|
@ -22,18 +22,16 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
|
||||||
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
||||||
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
import com.shabinder.spotiflyer.models.Source
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.utils.bindImage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class YoutubeTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
|
class YoutubeTrackListAdapter: ListAdapter<TrackDetails,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
|
||||||
|
|
||||||
var format:Format? = null
|
|
||||||
private val adapterScope = CoroutineScope(Dispatchers.Default)
|
private val adapterScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
@ -51,26 +49,31 @@ class YoutubeTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
|
|||||||
if(itemCount == 1){
|
if(itemCount == 1){
|
||||||
holder.binding.imageUrl.visibility = View.GONE}else{
|
holder.binding.imageUrl.visibility = View.GONE}else{
|
||||||
adapterScope.launch {
|
adapterScope.launch {
|
||||||
bindImage(holder.binding.imageUrl, item.ytCoverUrl)
|
bindImage(holder.binding.imageUrl,
|
||||||
|
"https://i.ytimg.com/vi/${item.albumArt.absolutePath.substringAfterLast("/")
|
||||||
|
.substringBeforeLast(".")}/maxresdefault.jpg"
|
||||||
|
,
|
||||||
|
Source.YouTube
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.binding.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}"
|
holder.binding.trackName.text = "${if(item.title.length > 17){"${item.title.subSequence(0,16)}..."}else{item.title}}"
|
||||||
holder.binding.artist.text = "${item.artists?.get(0)?.name?:""}..."
|
holder.binding.artist.text = "${item.artists.get(0)}..."
|
||||||
holder.binding.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec"
|
||||||
holder.binding.btnDownload.setOnClickListener{
|
holder.binding.btnDownload.setOnClickListener{
|
||||||
adapterScope.launch {
|
adapterScope.launch {
|
||||||
YTDownloadHelper.downloadFile(null,"YT_Downloads",item,format)
|
// YTDownloadHelper.downloadFile(null,"YT_Downloads",item,format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<Track>(){
|
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){
|
||||||
override fun areItemsTheSame(oldItem: Track, newItem: Track): Boolean {
|
override fun areItemsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean {
|
||||||
return oldItem.name == newItem.name
|
return oldItem.title == newItem.title
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Track, newItem: Track): Boolean {
|
override fun areContentsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -24,41 +24,33 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
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.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
|
||||||
import com.bumptech.glide.request.RequestListener
|
|
||||||
import com.bumptech.glide.request.target.Target
|
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
|
||||||
import com.shabinder.spotiflyer.MainActivity
|
import com.shabinder.spotiflyer.MainActivity
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
import com.shabinder.spotiflyer.SharedViewModel
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
import com.shabinder.spotiflyer.databinding.SpotifyFragmentBinding
|
import com.shabinder.spotiflyer.databinding.SpotifyFragmentBinding
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.Source
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Track
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
|
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
|
||||||
import com.shabinder.spotiflyer.utils.YoutubeMusicApi
|
import com.shabinder.spotiflyer.utils.YoutubeMusicApi
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.utils.bindImage
|
||||||
import com.shabinder.spotiflyer.utils.copyTo
|
import com.shabinder.spotiflyer.utils.loadAllImages
|
||||||
import com.shabinder.spotiflyer.utils.rotateAnim
|
import com.shabinder.spotiflyer.utils.rotateAnim
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@ -69,7 +61,6 @@ class SpotifyFragment : Fragment() {
|
|||||||
private lateinit var spotifyViewModel: SpotifyViewModel
|
private lateinit var spotifyViewModel: SpotifyViewModel
|
||||||
private lateinit var sharedViewModel: SharedViewModel
|
private lateinit var sharedViewModel: SharedViewModel
|
||||||
private lateinit var adapterSpotify:SpotifyTrackListAdapter
|
private lateinit var adapterSpotify:SpotifyTrackListAdapter
|
||||||
@Inject lateinit var ytDownloader:YoutubeDownloader
|
|
||||||
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
|
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
|
||||||
private var intentFilter:IntentFilter? = null
|
private var intentFilter:IntentFilter? = null
|
||||||
private var updateUIReceiver: BroadcastReceiver? = null
|
private var updateUIReceiver: BroadcastReceiver? = null
|
||||||
@ -109,30 +100,34 @@ class SpotifyFragment : Fragment() {
|
|||||||
spotifyViewModel.spotifySearch(type,link)
|
spotifyViewModel.spotifySearch(type,link)
|
||||||
if(type=="album")adapterSpotify.isAlbum = true
|
if(type=="album")adapterSpotify.isAlbum = true
|
||||||
|
|
||||||
binding.btnDownloadAllSpotify.setOnClickListener {
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
for (track in spotifyViewModel.trackList.value!!){
|
|
||||||
if(track.downloaded != "Downloaded"){
|
|
||||||
track.downloaded = "Downloading"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.btnDownloadAllSpotify.visibility = View.GONE
|
|
||||||
binding.downloadingFabSpotify.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
|
binding.btnDownloadAll.visibility = View.GONE
|
||||||
|
binding.downloadingFab.visibility = View.VISIBLE
|
||||||
|
|
||||||
rotateAnim(binding.downloadingFabSpotify)
|
rotateAnim(binding.downloadingFab)
|
||||||
for (track in spotifyViewModel.trackList.value!!){
|
for (track in spotifyViewModel.trackList.value!!){
|
||||||
if(track.downloaded != "Downloaded"){
|
if(track.downloaded != DownloadStatus.Downloaded){
|
||||||
|
track.downloaded = DownloadStatus.Downloading
|
||||||
adapterSpotify.notifyItemChanged(spotifyViewModel.trackList.value!!.indexOf(track))
|
adapterSpotify.notifyItemChanged(spotifyViewModel.trackList.value!!.indexOf(track))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showToast("Starting Download in Few Seconds")
|
showToast("Processing!")
|
||||||
spotifyViewModel.uiScope.launch(Dispatchers.Default){loadAllImages(spotifyViewModel.trackList.value!!)}
|
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||||
|
val urlList = arrayListOf<String>()
|
||||||
|
spotifyViewModel.trackList.value?.forEach { urlList.add(it.album?.images?.get(0)?.url.toString()) }
|
||||||
|
//Appending Source
|
||||||
|
urlList.add("spotify")
|
||||||
|
loadAllImages(
|
||||||
|
requireActivity(),
|
||||||
|
urlList
|
||||||
|
)
|
||||||
|
}
|
||||||
spotifyViewModel.uiScope.launch {
|
spotifyViewModel.uiScope.launch {
|
||||||
SpotifyDownloadHelper.downloadAllTracks(
|
SpotifyDownloadHelper.downloadAllTracks(
|
||||||
spotifyViewModel.folderType,
|
spotifyViewModel.folderType,
|
||||||
spotifyViewModel.subFolder,
|
spotifyViewModel.subFolder,
|
||||||
spotifyViewModel.trackList.value!!,
|
spotifyViewModel.trackList.value!!,
|
||||||
ytDownloader
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,15 +149,18 @@ class SpotifyFragment : Fragment() {
|
|||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
//UI update here
|
//UI update here
|
||||||
if (intent != null){
|
if (intent != null){
|
||||||
val track = intent.getParcelableExtra<Track?>("track")
|
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
|
||||||
track?.let {
|
trackDetails?.let {
|
||||||
val position: Int = spotifyViewModel.trackList.value?.indexOf(track)!!
|
val position: Int = spotifyViewModel.trackList.value?.map { it.name }?.indexOf(trackDetails.title) ?: -1
|
||||||
Log.i("Track","Download Completed Intent :$position")
|
Log.i("Track","Download Completed Intent :$position")
|
||||||
track.downloaded = "Downloaded"
|
|
||||||
if(position != -1) {
|
if(position != -1) {
|
||||||
spotifyViewModel.trackList.value?.set(position, track)
|
val track = spotifyViewModel.trackList.value?.get(position)
|
||||||
adapterSpotify.notifyItemChanged(position)
|
track?.let{
|
||||||
checkIfAllDownloaded()
|
it.downloaded = DownloadStatus.Downloaded
|
||||||
|
spotifyViewModel.trackList.value?.set(position, it)
|
||||||
|
adapterSpotify.notifyItemChanged(position)
|
||||||
|
checkIfAllDownloaded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,7 +182,7 @@ class SpotifyFragment : Fragment() {
|
|||||||
* CoverUrl Binding Observer!
|
* CoverUrl Binding Observer!
|
||||||
**/
|
**/
|
||||||
spotifyViewModel.coverUrl.observe(viewLifecycleOwner, {
|
spotifyViewModel.coverUrl.observe(viewLifecycleOwner, {
|
||||||
if(it!="Loading") bindImage(binding.spotifyCoverImage,it)
|
if(it!="Loading") bindImage(binding.coverImage,it,Source.Spotify)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -202,7 +200,7 @@ class SpotifyFragment : Fragment() {
|
|||||||
* Title Binding Observer!
|
* Title Binding Observer!
|
||||||
**/
|
**/
|
||||||
spotifyViewModel.title.observe(viewLifecycleOwner, {
|
spotifyViewModel.title.observe(viewLifecycleOwner, {
|
||||||
binding.titleViewSpotify.text = it
|
binding.titleView.text = it
|
||||||
})
|
})
|
||||||
|
|
||||||
sharedViewModel.intentString.observe(viewLifecycleOwner,{
|
sharedViewModel.intentString.observe(viewLifecycleOwner,{
|
||||||
@ -215,10 +213,10 @@ class SpotifyFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkIfAllDownloaded() {
|
private fun checkIfAllDownloaded() {
|
||||||
if(!spotifyViewModel.trackList.value!!.any { it.downloaded != "Downloaded" }){
|
if(!spotifyViewModel.trackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
|
||||||
//All Tracks Downloaded
|
//All Tracks Downloaded
|
||||||
binding.btnDownloadAllSpotify.visibility = View.GONE
|
binding.btnDownloadAll.visibility = View.GONE
|
||||||
binding.downloadingFabSpotify.apply{
|
binding.downloadingFab.apply{
|
||||||
setImageResource(R.drawable.ic_tick)
|
setImageResource(R.drawable.ic_tick)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
clearAnimation()
|
clearAnimation()
|
||||||
@ -236,69 +234,17 @@ class SpotifyFragment : Fragment() {
|
|||||||
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
|
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
|
||||||
spotifyViewModel.spotifyService = it
|
spotifyViewModel.spotifyService = it
|
||||||
})
|
})
|
||||||
SpotifyDownloadHelper.context = requireContext()
|
|
||||||
SpotifyDownloadHelper.youtubeMusicApi = youtubeMusicApi
|
SpotifyDownloadHelper.youtubeMusicApi = youtubeMusicApi
|
||||||
SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel
|
SpotifyDownloadHelper.sharedViewModel = sharedViewModel
|
||||||
SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify
|
SpotifyDownloadHelper.statusBar = binding.statusBar
|
||||||
binding.trackListSpotify.adapter = adapterSpotify
|
binding.trackList.adapter = adapterSpotify
|
||||||
(binding.trackListSpotify.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to fetch all Images for using in mp3 tag.
|
|
||||||
**/
|
|
||||||
private suspend fun loadAllImages(trackList: List<Track>) {
|
|
||||||
trackList.forEach {
|
|
||||||
val imgUrl = it.album?.images?.get(0)?.url
|
|
||||||
imgUrl?.let {
|
|
||||||
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
|
||||||
Glide
|
|
||||||
.with(requireContext())
|
|
||||||
.asFile()
|
|
||||||
.load(imgUri)
|
|
||||||
.listener(object: RequestListener<File> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
Log.i("Glide","LoadFailed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: File?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
dataSource: DataSource?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
withContext(Dispatchers.IO){
|
|
||||||
try {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
|
|
||||||
)
|
|
||||||
resource?.copyTo(file)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}).submit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure Recycler View Adapter
|
* Configure Recycler View Adapter
|
||||||
**/
|
**/
|
||||||
private fun adapterConfig(trackList: List<Track>){
|
private fun adapterConfig(trackList: List<Track>){
|
||||||
adapterSpotify.ytDownloader = ytDownloader
|
|
||||||
adapterSpotify.spotifyViewModel = spotifyViewModel
|
adapterSpotify.spotifyViewModel = spotifyViewModel
|
||||||
adapterSpotify.submitList(trackList)
|
adapterSpotify.submitList(trackList)
|
||||||
}
|
}
|
||||||
@ -320,4 +266,5 @@ class SpotifyFragment : Fragment() {
|
|||||||
val netInfo = cm.activeNetworkInfo
|
val netInfo = cm.activeNetworkInfo
|
||||||
return netInfo != null && netInfo.isConnectedOrConnecting
|
return netInfo != null && netInfo.isConnectedOrConnecting
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -52,7 +52,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
val tempTrackList = mutableListOf<Track>()
|
val tempTrackList = mutableListOf<Track>()
|
||||||
if(File(finalOutputDir(trackObject?.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
if(File(finalOutputDir(trackObject?.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
trackObject.downloaded = "Downloaded"
|
trackObject.downloaded = DownloadStatus.Downloaded
|
||||||
}
|
}
|
||||||
tempTrackList.add(trackObject)
|
tempTrackList.add(trackObject)
|
||||||
trackList.value = tempTrackList
|
trackList.value = tempTrackList
|
||||||
@ -65,7 +65,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
link = "https://open.spotify.com/$type/$link",
|
link = "https://open.spotify.com/$type/$link",
|
||||||
coverUrl = coverUrl.value!!,
|
coverUrl = coverUrl.value!!,
|
||||||
totalFiles = tempTrackList.size,
|
totalFiles = tempTrackList.size,
|
||||||
downloaded = trackObject.downloaded =="Downloaded",
|
downloaded = trackObject.downloaded == DownloadStatus.Downloaded,
|
||||||
directory = finalOutputDir(trackObject.name!!,folderType,subFolder)
|
directory = finalOutputDir(trackObject.name!!,folderType,subFolder)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
val tempTrackList = mutableListOf<Track>()
|
val tempTrackList = mutableListOf<Track>()
|
||||||
albumObject?.tracks?.items?.forEach {
|
albumObject?.tracks?.items?.forEach {
|
||||||
if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
it.downloaded = "Downloaded"
|
it.downloaded = DownloadStatus.Downloaded
|
||||||
}
|
}
|
||||||
it.album = Album(images = listOf(Image(url = albumObject.images?.get(0)?.url)))
|
it.album = Album(images = listOf(Image(url = albumObject.images?.get(0)?.url)))
|
||||||
tempTrackList.add(it)
|
tempTrackList.add(it)
|
||||||
@ -112,7 +112,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
playlistObject?.tracks?.items?.forEach {
|
playlistObject?.tracks?.items?.forEach {
|
||||||
it.track?.let {
|
it.track?.let {
|
||||||
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
it1.downloaded = "Downloaded"
|
it1.downloaded = DownloadStatus.Downloaded
|
||||||
}
|
}
|
||||||
tempTrackList.add(it1)
|
tempTrackList.add(it1)
|
||||||
}
|
}
|
||||||
@ -130,13 +130,13 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
Log.i("Total Tracks Fetched",tempTrackList.size.toString())
|
Log.i("Total Tracks Fetched",tempTrackList.size.toString())
|
||||||
trackList.value = tempTrackList
|
trackList.value = tempTrackList
|
||||||
title.value = playlistObject?.name
|
title.value = playlistObject?.name
|
||||||
coverUrl.value = playlistObject?.images?.get(0)!!.url!!
|
coverUrl.value = playlistObject?.images?.get(0)?.url.toString()
|
||||||
withContext(Dispatchers.IO){
|
withContext(Dispatchers.IO){
|
||||||
databaseDAO.insert(DownloadRecord(
|
databaseDAO.insert(DownloadRecord(
|
||||||
type = "Playlist",
|
type = "Playlist",
|
||||||
name = title.value!!,
|
name = title.value.toString(),
|
||||||
link = "https://open.spotify.com/$type/$link",
|
link = "https://open.spotify.com/$type/$link",
|
||||||
coverUrl = coverUrl.value!!,
|
coverUrl = coverUrl.value.toString(),
|
||||||
totalFiles = tempTrackList.size,
|
totalFiles = tempTrackList.size,
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
||||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||||
|
@ -24,17 +24,22 @@ import android.view.ViewGroup
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
import com.shabinder.spotiflyer.SharedViewModel
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
import com.shabinder.spotiflyer.databinding.YoutubeFragmentBinding
|
import com.shabinder.spotiflyer.databinding.YoutubeFragmentBinding
|
||||||
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.Source
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter
|
import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.utils.bindImage
|
||||||
|
import com.shabinder.spotiflyer.utils.loadAllImages
|
||||||
|
import com.shabinder.spotiflyer.utils.rotateAnim
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@ -43,10 +48,10 @@ class YoutubeFragment : Fragment() {
|
|||||||
private lateinit var binding:YoutubeFragmentBinding
|
private lateinit var binding:YoutubeFragmentBinding
|
||||||
private lateinit var youtubeViewModel: YoutubeViewModel
|
private lateinit var youtubeViewModel: YoutubeViewModel
|
||||||
private lateinit var sharedViewModel: SharedViewModel
|
private lateinit var sharedViewModel: SharedViewModel
|
||||||
|
@Inject lateinit var ytDownloader: YoutubeDownloader
|
||||||
private lateinit var adapter : YoutubeTrackListAdapter
|
private lateinit var adapter : YoutubeTrackListAdapter
|
||||||
private val sampleDomain1 = "youtube.com"
|
private val sampleDomain1 = "youtube.com"
|
||||||
private val sampleDomain2 = "youtu.be"
|
private val sampleDomain2 = "youtu.be"
|
||||||
@Inject lateinit var ytDownloader: YoutubeDownloader
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
@ -56,9 +61,7 @@ class YoutubeFragment : Fragment() {
|
|||||||
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
||||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||||
adapter = YoutubeTrackListAdapter()
|
adapter = YoutubeTrackListAdapter()
|
||||||
YTDownloadHelper.context = requireContext()
|
binding.trackList.adapter = adapter
|
||||||
YTDownloadHelper.statusBar = binding.StatusBarYoutube
|
|
||||||
binding.trackListYoutube.adapter = adapter
|
|
||||||
|
|
||||||
initializeLiveDataObservers()
|
initializeLiveDataObservers()
|
||||||
|
|
||||||
@ -70,7 +73,11 @@ class YoutubeFragment : Fragment() {
|
|||||||
|
|
||||||
private fun youtubeSearch(linkSearch:String) {
|
private fun youtubeSearch(linkSearch:String) {
|
||||||
val link = linkSearch.removePrefix("https://").removePrefix("http://")
|
val link = linkSearch.removePrefix("https://").removePrefix("http://")
|
||||||
if(!link.contains("playlist",true)){
|
if(link.contains("playlist",true) || link.contains("list",true)){
|
||||||
|
// Given Link is of a Playlist
|
||||||
|
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
|
||||||
|
youtubeViewModel.getYTPlaylist(playlistId,ytDownloader)
|
||||||
|
}else{//Given Link is of a Video
|
||||||
var searchId = "error"
|
var searchId = "error"
|
||||||
if(link.contains(sampleDomain1,true) ){
|
if(link.contains(sampleDomain1,true) ){
|
||||||
searchId = link.substringAfterLast("=","error")
|
searchId = link.substringAfterLast("=","error")
|
||||||
@ -79,41 +86,63 @@ class YoutubeFragment : Fragment() {
|
|||||||
searchId = link.substringAfterLast("/","error")
|
searchId = link.substringAfterLast("/","error")
|
||||||
}
|
}
|
||||||
if(searchId != "error") {
|
if(searchId != "error") {
|
||||||
youtubeViewModel.getYTTrack(searchId,ytDownloader)
|
youtubeViewModel.getYTTrack(searchId,ytDownloader)
|
||||||
binding.btnDownloadAllYoutube.setOnClickListener {
|
|
||||||
YTDownloadHelper.downloadFile(null,"YT_Downloads",
|
|
||||||
youtubeViewModel.ytTrack.value!!,youtubeViewModel.format.value)
|
|
||||||
}
|
|
||||||
}else{showToast("Your Youtube Link is not of a Video!!")}
|
}else{showToast("Your Youtube Link is not of a Video!!")}
|
||||||
}else(showToast("Your Youtube Link is not of a Video!!"))
|
}
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
binding.btnDownloadAll.visibility = View.GONE
|
||||||
|
binding.downloadingFab.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
rotateAnim(binding.downloadingFab)
|
||||||
|
|
||||||
|
for (track in youtubeViewModel.ytTrackList.value?: listOf()){
|
||||||
|
if(track.downloaded != DownloadStatus.Downloaded){
|
||||||
|
track.downloaded = DownloadStatus.Downloading
|
||||||
|
adapter.notifyItemChanged(youtubeViewModel.ytTrackList.value!!.indexOf(track))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast("Processing!")
|
||||||
|
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||||
|
val urlList = arrayListOf<String>()
|
||||||
|
youtubeViewModel.ytTrackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
|
||||||
|
.substringBeforeLast(".")}/maxresdefault.jpg")}
|
||||||
|
//Appending Source
|
||||||
|
urlList.add("youtube")
|
||||||
|
loadAllImages(
|
||||||
|
requireActivity(),
|
||||||
|
urlList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
youtubeViewModel.uiScope.launch {
|
||||||
|
YTDownloadHelper.downloadYTTracks(
|
||||||
|
type = youtubeViewModel.folderType,
|
||||||
|
subFolder = youtubeViewModel.subFolder,
|
||||||
|
tracks = youtubeViewModel.ytTrackList.value ?: listOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeLiveDataObservers() {
|
private fun initializeLiveDataObservers() {
|
||||||
/**
|
/**
|
||||||
* CoverUrl Binding Observer!
|
* CoverUrl Binding Observer!
|
||||||
**/
|
**/
|
||||||
youtubeViewModel.coverUrl.observe(viewLifecycleOwner, Observer {
|
youtubeViewModel.coverUrl.observe(viewLifecycleOwner, {
|
||||||
if(it!="Loading") bindImage(binding.youtubeCoverImage,it)
|
if(it!="Loading") bindImage(binding.coverImage,it,Source.YouTube)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TrackList Binding Observer!
|
* TrackList Binding Observer!
|
||||||
**/
|
**/
|
||||||
youtubeViewModel.ytTrack.observe(viewLifecycleOwner, Observer {
|
youtubeViewModel.ytTrackList.observe(viewLifecycleOwner, {
|
||||||
val list = mutableListOf<Track>()
|
adapterConfig(it)
|
||||||
list.add(it)
|
|
||||||
adapterConfig(list)
|
|
||||||
})
|
|
||||||
|
|
||||||
youtubeViewModel.format.observe(viewLifecycleOwner, Observer {
|
|
||||||
adapter.format = it
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Title Binding Observer!
|
* Title Binding Observer!
|
||||||
**/
|
**/
|
||||||
youtubeViewModel.title.observe(viewLifecycleOwner, Observer {
|
youtubeViewModel.title.observe(viewLifecycleOwner, {
|
||||||
binding.titleViewYoutube.text = it
|
binding.titleView.text = it
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -121,7 +150,7 @@ class YoutubeFragment : Fragment() {
|
|||||||
/**
|
/**
|
||||||
* Configure Recycler View Adapter
|
* Configure Recycler View Adapter
|
||||||
**/
|
**/
|
||||||
private fun adapterConfig(list:List<Track>){
|
private fun adapterConfig(list:List<TrackDetails>){
|
||||||
adapter.submitList(list)
|
adapter.submitList(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,78 +17,119 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.ui.youtube
|
package com.shabinder.spotiflyer.ui.youtube
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.hilt.lifecycle.ViewModelInject
|
import androidx.hilt.lifecycle.ViewModelInject
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
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.shabinder.spotiflyer.database.DatabaseDAO
|
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||||
import com.shabinder.spotiflyer.models.Artist
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Source
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||||
|
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) :
|
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : ViewModel(){
|
||||||
ViewModel(){
|
|
||||||
|
|
||||||
val ytTrack = MutableLiveData<Track>()
|
/*
|
||||||
|
* YT Album Art Schema
|
||||||
|
* Normal Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||||
|
* */
|
||||||
|
|
||||||
|
val ytTrackList = MutableLiveData<List<TrackDetails>>()
|
||||||
val format = MutableLiveData<Format>()
|
val format = MutableLiveData<Format>()
|
||||||
private val loading = "Loading"
|
private val loading = "Loading"
|
||||||
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
|
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
|
||||||
var coverUrl = MutableLiveData<String>().apply { value = loading }
|
var coverUrl = MutableLiveData<String>().apply { value = loading }
|
||||||
|
val folderType = "YT_Downloads"
|
||||||
|
var subFolder = ""
|
||||||
private var viewModelJob = Job()
|
private var viewModelJob = Job()
|
||||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||||
|
|
||||||
|
|
||||||
fun getYTTrack(searchId:String,ytDownloader:YoutubeDownloader) {
|
fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){
|
||||||
uiScope.launch {
|
uiScope.launch(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO){
|
Log.i("YT Playlist",searchId)
|
||||||
Log.i("YT View Model",searchId)
|
val playlist = ytDownloader.getPlaylist(searchId)
|
||||||
val video = ytDownloader.getVideo(searchId)
|
val playlistDetails = playlist.details()
|
||||||
val detail = video?.details()
|
val name = playlistDetails.title()
|
||||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title()
|
subFolder = removeIllegalChars(name).toString()
|
||||||
Log.i("YT View Model",detail.toString())
|
val videos = playlist.videos()
|
||||||
ytTrack.postValue(
|
coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg")
|
||||||
Track(
|
title.postValue(
|
||||||
id = searchId,
|
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
|
||||||
name = name,
|
)
|
||||||
artists = listOf<Artist>(Artist(name = detail?.author())),
|
ytTrackList.postValue(videos.map {
|
||||||
duration_ms = detail?.lengthSeconds()?.times(1000)?.toLong()?:0,
|
TrackDetails(
|
||||||
ytCoverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
title = it.title(),
|
||||||
))
|
artists = listOf(it.author().toString()),
|
||||||
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg")
|
durationSec = it.lengthSeconds(),
|
||||||
title.postValue(
|
albumArt = File(
|
||||||
if(name?.length!! > 17){"${name.subSequence(0,16)}..."}else{name}
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + it.videoId() + ".jpeg"
|
||||||
|
),
|
||||||
|
source = Source.YouTube,
|
||||||
|
downloaded = if(File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists())
|
||||||
|
DownloadStatus.Downloaded
|
||||||
|
else DownloadStatus.NotDownloaded
|
||||||
)
|
)
|
||||||
format.postValue(try {
|
})
|
||||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
try {
|
|
||||||
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
try {
|
|
||||||
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
Log.i("YTDownloader", e.toString())
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
withContext(Dispatchers.IO){
|
withContext(Dispatchers.IO){
|
||||||
databaseDAO.insert(DownloadRecord(
|
databaseDAO.insert(DownloadRecord(
|
||||||
type = "Track",
|
type = "PlayList",
|
||||||
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
|
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
|
||||||
link = "https://www.youtube.com/watch?v=$searchId",
|
link = "https://www.youtube.com/playlist?list=$searchId",
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg",
|
coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg",
|
||||||
totalFiles = 1,
|
totalFiles = videos.size,
|
||||||
downloaded = false,
|
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
|
||||||
directory = finalOutputDir(type = "YT_Downloads")
|
downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) {
|
||||||
|
uiScope.launch(Dispatchers.IO) {
|
||||||
|
Log.i("YT Video",searchId)
|
||||||
|
val video = ytDownloader.getVideo(searchId)
|
||||||
|
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg")
|
||||||
|
val detail = video?.details()
|
||||||
|
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: ""
|
||||||
|
Log.i("YT View Model",detail.toString())
|
||||||
|
ytTrackList.postValue(listOf(TrackDetails(
|
||||||
|
title = name,
|
||||||
|
artists = listOf(detail?.author().toString()),
|
||||||
|
durationSec = detail?.lengthSeconds()?:0,
|
||||||
|
albumArt = File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
Provider.defaultDir +".Images/" + searchId + ".jpeg"
|
||||||
|
),
|
||||||
|
source = Source.YouTube
|
||||||
|
)))
|
||||||
|
title.postValue(
|
||||||
|
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
|
||||||
|
)
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
databaseDAO.insert(DownloadRecord(
|
||||||
|
type = "Track",
|
||||||
|
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
|
||||||
|
link = "https://www.youtube.com/watch?v=$searchId",
|
||||||
|
coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg",
|
||||||
|
totalFiles = 1,
|
||||||
|
downloaded = false,
|
||||||
|
directory = finalOutputDir(type = "YT_Downloads")
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,138 +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.os.Environment
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.view.animation.RotateAnimation
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.databinding.BindingAdapter
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
|
||||||
import com.bumptech.glide.request.RequestListener
|
|
||||||
import com.bumptech.glide.request.target.Target
|
|
||||||
import com.shabinder.spotiflyer.R
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{
|
|
||||||
return Environment.getExternalStorageDirectory().toString() + File.separator +
|
|
||||||
SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator +
|
|
||||||
(if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator}
|
|
||||||
+ itemName?.let { SpotifyDownloadHelper.removeIllegalChars(it) + extension})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rotateAnim(view: View){
|
|
||||||
val rotate = RotateAnimation(
|
|
||||||
0F, 360F,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
|
|
||||||
)
|
|
||||||
rotate.duration = 1000
|
|
||||||
rotate.repeatCount = Animation.INFINITE
|
|
||||||
rotate.repeatMode = Animation.INFINITE
|
|
||||||
rotate.interpolator = LinearInterpolator()
|
|
||||||
view.animation = rotate
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@BindingAdapter("imageUrl")
|
|
||||||
fun bindImage(imgView: ImageView, imgUrl: String?) {
|
|
||||||
imgUrl?.let {
|
|
||||||
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
|
||||||
Glide
|
|
||||||
.with(imgView)
|
|
||||||
.asFile()
|
|
||||||
.load(imgUri)
|
|
||||||
.placeholder(R.drawable.ic_song_placeholder)
|
|
||||||
.error(R.drawable.ic_musicplaceholder)
|
|
||||||
.listener(object:RequestListener<File>{
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
Log.i("Glide","LoadFailed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: File?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
dataSource: DataSource?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
try {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
|
|
||||||
) // the File to save , append increasing numeric counter to prevent files from getting overwritten.
|
|
||||||
resource?.copyTo(file)
|
|
||||||
withContext(Dispatchers.Main){
|
|
||||||
Glide.with(imgView)
|
|
||||||
.load(file)
|
|
||||||
.placeholder(R.drawable.ic_song_placeholder)
|
|
||||||
.into(imgView)
|
|
||||||
// Log.i("Glide","imageSaved")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}).submit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*Extension Function For Copying Files!
|
|
||||||
**/
|
|
||||||
fun File.copyTo(file: File) {
|
|
||||||
inputStream().use { input ->
|
|
||||||
file.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun createDirectory(dir:String){
|
|
||||||
val yourAppDir = File(Environment.getExternalStorageDirectory(),
|
|
||||||
dir)
|
|
||||||
|
|
||||||
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
|
||||||
{ // create empty directory
|
|
||||||
if (yourAppDir.mkdirs())
|
|
||||||
{Log.i("CreateDir","App dir created")}
|
|
||||||
else
|
|
||||||
{Log.w("CreateDir","Unable to create app dir!")}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{Log.i("CreateDir","App dir already exists")}
|
|
||||||
}
|
|
@ -18,6 +18,7 @@
|
|||||||
package com.shabinder.spotiflyer.utils
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Environment
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.shabinder.spotiflyer.App
|
import com.shabinder.spotiflyer.App
|
||||||
import com.shabinder.spotiflyer.MainActivity
|
import com.shabinder.spotiflyer.MainActivity
|
||||||
@ -38,18 +39,28 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@InstallIn(ApplicationComponent::class)
|
@InstallIn(ApplicationComponent::class)
|
||||||
@Module
|
@Module
|
||||||
object Provider {
|
object Provider {
|
||||||
|
|
||||||
|
lateinit var activity: MainActivity
|
||||||
|
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||||
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
|
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
|
||||||
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
|
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun getYTDownloader():YoutubeDownloader{
|
||||||
|
return YoutubeDownloader()
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -72,12 +83,6 @@ object Provider {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun getYTDownloader():YoutubeDownloader{
|
|
||||||
return YoutubeDownloader()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun getSpotifyTokenInterface():SpotifyServiceTokenRequest{
|
fun getSpotifyTokenInterface():SpotifyServiceTokenRequest{
|
||||||
|
202
app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
Normal file → Executable file
202
app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
Normal file → Executable file
@ -17,15 +17,203 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.utils
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Environment
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
|
import android.view.animation.RotateAnimation
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.databinding.BindingAdapter
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
|
import com.shabinder.spotiflyer.R
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
|
import com.shabinder.spotiflyer.models.Source
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
|
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
fun loadAllImages(context: Context?, images:ArrayList<String>? = null ) {
|
||||||
|
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||||
|
images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) }
|
||||||
|
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startService(context:Context?,objects:ArrayList<DownloadObject>? = null ) {
|
||||||
|
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||||
|
objects?.let { serviceIntent.putParcelableArrayListExtra("object",it) }
|
||||||
|
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{
|
||||||
|
return Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
|
defaultDir + removeIllegalChars(type) + File.separator +
|
||||||
|
(if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator}
|
||||||
|
+ itemName?.let { removeIllegalChars(it) + extension})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rotateAnim(view: View){
|
||||||
|
val rotate = RotateAnimation(
|
||||||
|
0F, 360F,
|
||||||
|
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
|
||||||
|
)
|
||||||
|
rotate.duration = 1000
|
||||||
|
rotate.repeatCount = Animation.INFINITE
|
||||||
|
rotate.repeatMode = Animation.INFINITE
|
||||||
|
rotate.interpolator = LinearInterpolator()
|
||||||
|
view.animation = rotate
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("imageUrl")
|
||||||
|
fun bindImage(imgView: ImageView, imgUrl: String?,source: Source) {
|
||||||
|
imgUrl?.let {
|
||||||
|
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
||||||
|
Glide
|
||||||
|
.with(imgView)
|
||||||
|
.asFile()
|
||||||
|
.load(imgUri)
|
||||||
|
.placeholder(R.drawable.ic_song_placeholder)
|
||||||
|
.error(R.drawable.ic_musicplaceholder)
|
||||||
|
.listener(object:RequestListener<File>{
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
Log.i("Glide","LoadFailed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: File?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
dataSource: DataSource?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
try {
|
||||||
|
val file = when(source){
|
||||||
|
Source.Spotify->{
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Source.YouTube->{
|
||||||
|
//Url Format: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||||
|
// We Are Naming using "$searchId"
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the File to save , append increasing numeric counter to prevent files from getting overwritten.
|
||||||
|
resource?.copyTo(file)
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
Glide.with(imgView)
|
||||||
|
.load(file)
|
||||||
|
.placeholder(R.drawable.ic_song_placeholder)
|
||||||
|
.into(imgView)
|
||||||
|
// Log.i("Glide","imageSaved")
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}).submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Extension Function For Copying Files!
|
||||||
|
**/
|
||||||
|
fun File.copyTo(file: File) {
|
||||||
|
inputStream().use { input ->
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun createDirectory(dir:String){
|
||||||
|
val yourAppDir = File(Environment.getExternalStorageDirectory(),
|
||||||
|
dir)
|
||||||
|
|
||||||
|
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
||||||
|
{ // create empty directory
|
||||||
|
if (yourAppDir.mkdirs())
|
||||||
|
{Log.i("CreateDir","App dir created")}
|
||||||
|
else
|
||||||
|
{Log.w("CreateDir","Unable to create app dir!")}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{Log.i("CreateDir","App dir already exists")}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removing Illegal Chars from File Name
|
||||||
|
* **/
|
||||||
|
fun removeIllegalChars(fileName: String): String? {
|
||||||
|
val illegalCharArray = charArrayOf(
|
||||||
|
'/',
|
||||||
|
'\n',
|
||||||
|
'\r',
|
||||||
|
'\t',
|
||||||
|
'\u0000',
|
||||||
|
'\u000C',
|
||||||
|
'`',
|
||||||
|
'?',
|
||||||
|
'*',
|
||||||
|
'\\',
|
||||||
|
'<',
|
||||||
|
'>',
|
||||||
|
'|',
|
||||||
|
'\"',
|
||||||
|
'.',
|
||||||
|
'-',
|
||||||
|
'\''
|
||||||
|
)
|
||||||
|
|
||||||
|
var name = fileName
|
||||||
|
for (c in illegalCharArray) {
|
||||||
|
name = fileName.replace(c, '_')
|
||||||
|
}
|
||||||
|
name = name.replace("\\s".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
|
||||||
|
}
|
||||||
|
|
||||||
fun createDirectories() {
|
fun createDirectories() {
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir)
|
createDirectory(defaultDir)
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + ".Images/")
|
createDirectory(defaultDir + ".Images/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Tracks/")
|
createDirectory(defaultDir + "Tracks/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Albums/")
|
createDirectory(defaultDir + "Albums/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Playlists/")
|
createDirectory(defaultDir + "Playlists/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "YT_Downloads/")
|
createDirectory(defaultDir + "YT_Downloads/")
|
||||||
}
|
}
|
||||||
fun getEmojiByUnicode(unicode: Int): String? {
|
fun getEmojiByUnicode(unicode: Int): String? {
|
||||||
return String(Character.toChars(unicode))
|
return String(Character.toChars(unicode))
|
||||||
|
@ -29,27 +29,36 @@ import android.os.*
|
|||||||
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
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.arthenica.mobileffmpeg.Config
|
import com.arthenica.mobileffmpeg.Config
|
||||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
||||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
||||||
import com.arthenica.mobileffmpeg.FFmpeg
|
import com.arthenica.mobileffmpeg.FFmpeg
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
|
import com.github.kiulian.downloader.model.formats.Format
|
||||||
|
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||||
import com.mpatric.mp3agic.ID3v1Tag
|
import com.mpatric.mp3agic.ID3v1Tag
|
||||||
import com.mpatric.mp3agic.ID3v24Tag
|
import com.mpatric.mp3agic.ID3v24Tag
|
||||||
import com.mpatric.mp3agic.Mp3File
|
import com.mpatric.mp3agic.Mp3File
|
||||||
import com.shabinder.spotiflyer.MainActivity
|
import com.shabinder.spotiflyer.MainActivity
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
|
||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.utils.copyTo
|
||||||
import com.tonyodev.fetch2.*
|
import com.tonyodev.fetch2.*
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
import com.tonyodev.fetch2core.DownloadBlock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
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
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
class ForegroundService : Service(){
|
class ForegroundService : Service(){
|
||||||
private val tag = "Foreground Service"
|
private val tag = "Foreground Service"
|
||||||
private val channelId = "ForegroundDownloaderService"
|
private val channelId = "ForegroundDownloaderService"
|
||||||
@ -57,22 +66,21 @@ class ForegroundService : Service(){
|
|||||||
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 downloaded = 0//Total Files downloaded
|
||||||
private var fetch:Fetch? = null
|
private lateinit var fetch:Fetch
|
||||||
private var downloadManager : DownloadManager? = null
|
private lateinit var ytDownloader: YoutubeDownloader
|
||||||
private var downloadList = mutableListOf<DownloadObject>()
|
private lateinit var downloadManager : DownloadManager
|
||||||
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,TrackDetails>()
|
||||||
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 var defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||||
private val parentDirectory = File(Environment.getExternalStorageDirectory(),
|
private val parentDirectory = File(Environment.getExternalStorageDirectory(),
|
||||||
defaultDirectory+File.separator
|
defaultDir +File.separator
|
||||||
)
|
)
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
var notificationLine = 0
|
var notificationLine = 0
|
||||||
val messageList = mutableListOf<String>("","","","")
|
val messageList = mutableListOf("","","","")
|
||||||
private var pendingIntent:PendingIntent? = null
|
private var pendingIntent:PendingIntent? = null
|
||||||
|
|
||||||
|
|
||||||
@ -89,7 +97,7 @@ class ForegroundService : Service(){
|
|||||||
0, notificationIntent, 0
|
0, notificationIntent, 0
|
||||||
)
|
)
|
||||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
ytDownloader = YoutubeDownloader()
|
||||||
val fetchConfiguration =
|
val fetchConfiguration =
|
||||||
FetchConfiguration.Builder(this)
|
FetchConfiguration.Builder(this)
|
||||||
.setDownloadConcurrentLimit(4)
|
.setDownloadConcurrentLimit(4)
|
||||||
@ -98,86 +106,41 @@ class ForegroundService : Service(){
|
|||||||
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||||
|
|
||||||
fetch = Fetch.getDefaultInstance()
|
fetch = Fetch.getDefaultInstance()
|
||||||
// fetch?.enableLogging(true)
|
fetch.addListener(fetchListener)
|
||||||
fetch?.addListener(fetchListener)
|
|
||||||
//clearing all not completed Downloads
|
//clearing all not completed Downloads
|
||||||
//Starting fresh
|
//Starting fresh
|
||||||
fetch?.removeAll()
|
fetch.removeAll()
|
||||||
|
|
||||||
startForeground()
|
startForeground()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*Starting Service with Notification as Foreground!
|
|
||||||
**/
|
|
||||||
private fun startForeground() {
|
|
||||||
val channelId =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
createNotificationChannel(channelId, "Downloader Service")
|
|
||||||
} else {
|
|
||||||
// If earlier version channel ID is not used
|
|
||||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, channelId)
|
|
||||||
.setSmallIcon(R.drawable.down_arrowbw)
|
|
||||||
.setNotificationSilent()
|
|
||||||
.setSubText("Total: $total Completed:$converted")
|
|
||||||
.setStyle(NotificationCompat.InboxStyle()
|
|
||||||
.setBigContentTitle("Speed: $speed KB/s")
|
|
||||||
.addLine(messageList[0])
|
|
||||||
.addLine(messageList[1])
|
|
||||||
.addLine(messageList[2])
|
|
||||||
.addLine(messageList[3]))
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.build()
|
|
||||||
startForeground(notificationId, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createNotificationChannel(channelId: String, channelName: String): String{
|
|
||||||
val chan = NotificationChannel(channelId,
|
|
||||||
channelName, NotificationManager.IMPORTANCE_DEFAULT)
|
|
||||||
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
||||||
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
service.createNotificationChannel(chan)
|
|
||||||
return channelId
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("WakelockTimeout")
|
@SuppressLint("WakelockTimeout")
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
// Send a notification that service is started
|
// Send a notification that service is started
|
||||||
Log.i(tag,"Service Started.")
|
Log.i(tag,"Service Started.")
|
||||||
startForeground()
|
startForeground()
|
||||||
val obj:DownloadObject? = intent.getParcelableExtra("object") ?: intent.extras?.getParcelable("object")
|
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
|
||||||
obj?.let {
|
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList"))
|
||||||
total ++
|
|
||||||
updateNotification()
|
|
||||||
serviceScope.launch {
|
|
||||||
val request= Request(obj.url, obj.outputDir)
|
|
||||||
request.priority = Priority.NORMAL
|
|
||||||
request.networkType = NetworkType.ALL
|
|
||||||
|
|
||||||
fetch!!.enqueue(request,
|
imagesList?.let{
|
||||||
{
|
serviceScope.launch {
|
||||||
obj.track?.let { it1 -> requestMap.put(it, it1) }
|
loadAllImages(it)
|
||||||
downloadList.remove(obj)
|
|
||||||
Log.i(tag, "Enqueuing Download")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadObjects?.let {
|
||||||
|
total += downloadObjects.size
|
||||||
|
updateNotification()
|
||||||
|
downloadAllTracks(downloadObjects)
|
||||||
|
}
|
||||||
|
|
||||||
//Wake locks and misc tasks from here :
|
//Wake locks and misc tasks from here :
|
||||||
return if (isServiceStarted){
|
return if (isServiceStarted){
|
||||||
|
//Service Already Started
|
||||||
START_STICKY
|
START_STICKY
|
||||||
} else{
|
} else{
|
||||||
Log.i(tag,"Starting the foreground service task")
|
Log.i(tag,"Starting the foreground service task")
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
|
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
||||||
@ -188,9 +151,53 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadAllTracks(downloadObjects: List<DownloadObject>){
|
||||||
|
serviceScope.launch(Dispatchers.IO) {
|
||||||
|
for(downloadObj in downloadObjects){
|
||||||
|
try {
|
||||||
|
val video = ytDownloader.getVideo(downloadObj.ytVideoId)
|
||||||
|
val format: Format? = try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||||
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
|
try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.high)?.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()
|
||||||
|
Log.i("DHelper Link Found", url)
|
||||||
|
serviceScope.launch {
|
||||||
|
val request= Request(url, downloadObj.outputFile)
|
||||||
|
request.priority = Priority.NORMAL
|
||||||
|
request.networkType = NetworkType.ALL
|
||||||
|
|
||||||
|
fetch.enqueue(request,
|
||||||
|
{
|
||||||
|
requestMap[it] = downloadObj.trackDetails
|
||||||
|
Log.i(tag, "Enqueuing Download")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
||||||
|
Log.i("Service YT Error", e.message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if(downloadMap.isEmpty() && converted == total){
|
if(converted == total){
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
Log.i(tag,"Service destroyed.")
|
Log.i(tag,"Service destroyed.")
|
||||||
deleteFile(parentDirectory)
|
deleteFile(parentDirectory)
|
||||||
@ -200,25 +207,11 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(converted == total ){
|
||||||
Log.i(tag,"Service Removed.")
|
Log.i(tag,"Service Removed.")
|
||||||
|
deleteFile(parentDirectory)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
} else {
|
} else {
|
||||||
@ -227,25 +220,6 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
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
|
* Fetch Listener/ Responsible for Fetch Behaviour
|
||||||
@ -274,23 +248,23 @@ class ForegroundService : Service(){
|
|||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
when(notificationLine){
|
when(notificationLine){
|
||||||
0 -> {
|
0 -> {
|
||||||
messageList[0] = "Downloading ${track?.name}"
|
messageList[0] = "Downloading ${track?.title}"
|
||||||
notificationLine = 1
|
notificationLine = 1
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
messageList[1] = "Downloading ${track?.name}"
|
messageList[1] = "Downloading ${track?.title}"
|
||||||
notificationLine = 2
|
notificationLine = 2
|
||||||
}
|
}
|
||||||
2-> {
|
2-> {
|
||||||
messageList[2] = "Downloading ${track?.name}"
|
messageList[2] = "Downloading ${track?.title}"
|
||||||
notificationLine = 3
|
notificationLine = 3
|
||||||
}
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
messageList[3] = "Downloading ${track?.name}"
|
messageList[3] = "Downloading ${track?.title}"
|
||||||
notificationLine = 0
|
notificationLine = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(tag,"${track?.name} Download Started")
|
Log.i(tag,"${track?.title} Download Started")
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,19 +283,20 @@ class ForegroundService : Service(){
|
|||||||
override fun onCompleted(download: Download) {
|
override fun onCompleted(download: Download) {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
for (message in messageList){
|
for (message in messageList){
|
||||||
if( message == "Downloading ${track?.name}"){
|
if( message == "Downloading ${track?.title}"){
|
||||||
|
//Remove Downloading Status from Notification
|
||||||
messageList[messageList.indexOf(message)] = ""
|
messageList[messageList.indexOf(message)] = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try{
|
try{
|
||||||
convertToMp3(download.file, track!!)
|
track?.let { convertToMp3(download.file, it) }
|
||||||
Log.i(tag,"${track.name} Download Completed")
|
Log.i(tag,"${track?.title} Download Completed")
|
||||||
}catch (e:KotlinNullPointerException
|
}catch (e:KotlinNullPointerException
|
||||||
){
|
){
|
||||||
Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!")
|
Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!")
|
||||||
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||||
downloaded++
|
downloaded++
|
||||||
requestMap.remove(download.request)
|
requestMap.remove(download.request)
|
||||||
@ -348,7 +323,7 @@ class ForegroundService : Service(){
|
|||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
downloaded++
|
downloaded++
|
||||||
Log.i(tag,download.error.throwable.toString())
|
Log.i(tag,download.error.throwable.toString())
|
||||||
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||||
requestMap.remove(download.request)
|
requestMap.remove(download.request)
|
||||||
}
|
}
|
||||||
@ -365,7 +340,7 @@ class ForegroundService : Service(){
|
|||||||
downloadedBytesPerSecond: Long
|
downloadedBytesPerSecond: Long
|
||||||
) {
|
) {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
Log.i(tag,"${track?.name} ETA: ${etaInMilliSeconds/1000} sec")
|
Log.i(tag,"${track?.title} ETA: ${etaInMilliSeconds/1000} sec")
|
||||||
speed = (downloadedBytesPerSecond/1000)
|
speed = (downloadedBytesPerSecond/1000)
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
@ -375,7 +350,7 @@ class ForegroundService : Service(){
|
|||||||
/**
|
/**
|
||||||
* If fetch Fails , Android Download Manager To RESCUE!!
|
* If fetch Fails , Android Download Manager To RESCUE!!
|
||||||
**/
|
**/
|
||||||
fun downloadUsingDM(url:String, outputDir:String, track: Track){
|
fun downloadUsingDM(url:String, outputDir:String, track: TrackDetails){
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
val request = DownloadManager.Request(uri)
|
val request = DownloadManager.Request(uri)
|
||||||
.setAllowedNetworkTypes(
|
.setAllowedNetworkTypes(
|
||||||
@ -383,14 +358,14 @@ class ForegroundService : Service(){
|
|||||||
DownloadManager.Request.NETWORK_MOBILE
|
DownloadManager.Request.NETWORK_MOBILE
|
||||||
)
|
)
|
||||||
.setAllowedOverRoaming(false)
|
.setAllowedOverRoaming(false)
|
||||||
.setTitle(track.name)
|
.setTitle(track.title)
|
||||||
.setDescription("Spotify Downloader Working Up here...")
|
.setDescription("Spotify Downloader Working Up here...")
|
||||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
|
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
|
||||||
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
|
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
|
||||||
))
|
))
|
||||||
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
//Start Download
|
//Start Download
|
||||||
val downloadID = downloadManager?.enqueue(request)
|
val downloadID = downloadManager.enqueue(request)
|
||||||
Log.i("DownloadManager", "Download Request Sent")
|
Log.i("DownloadManager", "Download Request Sent")
|
||||||
|
|
||||||
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
@ -412,7 +387,7 @@ class ForegroundService : Service(){
|
|||||||
/**
|
/**
|
||||||
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
|
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
|
||||||
**/
|
**/
|
||||||
fun convertToMp3(filePath: String, track: Track){
|
fun convertToMp3(filePath: String, track: TrackDetails){
|
||||||
val m4aFile = File(filePath)
|
val m4aFile = File(filePath)
|
||||||
|
|
||||||
FFmpeg.executeAsync(
|
FFmpeg.executeAsync(
|
||||||
@ -435,7 +410,7 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeMp3Tags(filePath:String, track: Track){
|
private fun writeMp3Tags(filePath:String, track: TrackDetails){
|
||||||
var mp3File = Mp3File(filePath)
|
var mp3File = Mp3File(filePath)
|
||||||
mp3File = removeAllTags(mp3File)
|
mp3File = removeAllTags(mp3File)
|
||||||
mp3File = setId3v1Tags(mp3File,track)
|
mp3File = setId3v1Tags(mp3File,track)
|
||||||
@ -485,62 +460,40 @@ class ForegroundService : Service(){
|
|||||||
/**
|
/**
|
||||||
*Modifying Mp3 Tags with MetaData!
|
*Modifying Mp3 Tags with MetaData!
|
||||||
**/
|
**/
|
||||||
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
|
private fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File {
|
||||||
val id3v1Tag = ID3v1Tag()
|
val id3v1Tag = ID3v1Tag().apply {
|
||||||
id3v1Tag.track = track.disc_number.toString()
|
artist = track.artists.joinToString(",")
|
||||||
val artistsList = mutableListOf<String>()
|
title = track.title
|
||||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
album = track.albumName
|
||||||
id3v1Tag.artist = artistsList.joinToString()
|
year = track.year
|
||||||
id3v1Tag.title = track.name
|
comment = "Genres:${track.comment}"
|
||||||
id3v1Tag.album = track.album?.name
|
}
|
||||||
id3v1Tag.year = track.album?.release_date
|
|
||||||
id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
|
||||||
mp3File.id3v1Tag = id3v1Tag
|
mp3File.id3v1Tag = id3v1Tag
|
||||||
return mp3File
|
return mp3File
|
||||||
}
|
}
|
||||||
private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File {
|
private fun setId3v2Tags(mp3file: Mp3File, track: TrackDetails): Mp3File {
|
||||||
val id3v2Tag = ID3v24Tag()
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
id3v2Tag.track = track.disc_number.toString()
|
artist = track.artists.joinToString(",")
|
||||||
val artistsList = mutableListOf<String>()
|
title = track.title
|
||||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
album = track.albumName
|
||||||
id3v2Tag.artist = artistsList.joinToString()
|
year = track.year
|
||||||
id3v2Tag.title = track.name
|
comment = "Genres:${track.comment}"
|
||||||
id3v2Tag.album = track.album?.name
|
lyrics = "Gonna Implement Soon"
|
||||||
id3v2Tag.year = track.album?.release_date
|
url = track.trackUrl
|
||||||
id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
}
|
||||||
id3v2Tag.lyrics = "Gonna Implement Soon"
|
val bytesArray = ByteArray(track.albumArt.length().toInt())
|
||||||
val copyrights = mutableListOf<String>()
|
try{
|
||||||
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
val fis = FileInputStream(track.albumArt)
|
||||||
id3v2Tag.copyright = copyrights.joinToString()
|
|
||||||
id3v2Tag.url = track.href
|
|
||||||
track.ytCoverUrl?.let {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.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.read(bytesArray) //read file into bytes[]
|
||||||
fis.close()
|
fis.close()
|
||||||
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
||||||
|
}catch (e:java.io.FileNotFoundException){
|
||||||
|
Log.i("Error","Couldn't Write Mp3 Album Art")
|
||||||
}
|
}
|
||||||
track.album?.let {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.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())
|
|
||||||
val fis = FileInputStream(file)
|
|
||||||
fis.read(bytesArray) //read file into bytes[]
|
|
||||||
fis.close()
|
|
||||||
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
|
||||||
}
|
|
||||||
id3v2Tag.albumImage
|
|
||||||
mp3file.id3v2Tag = id3v2Tag
|
mp3file.id3v2Tag = id3v2Tag
|
||||||
return mp3file
|
return mp3file
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeAllTags(mp3file: Mp3File): Mp3File {
|
private fun removeAllTags(mp3file: Mp3File): Mp3File {
|
||||||
if (mp3file.hasId3v1Tag()) {
|
if (mp3file.hasId3v1Tag()) {
|
||||||
mp3file.removeId3v1Tag()
|
mp3file.removeId3v1Tag()
|
||||||
@ -554,4 +507,141 @@ class ForegroundService : Service(){
|
|||||||
return mp3file
|
return mp3file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Starting Service with Notification as Foreground!
|
||||||
|
**/
|
||||||
|
private fun startForeground() {
|
||||||
|
val channelId =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
createNotificationChannel(channelId, "Downloader Service")
|
||||||
|
} else {
|
||||||
|
// If earlier version channel ID is not used
|
||||||
|
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(this, channelId)
|
||||||
|
.setSmallIcon(R.drawable.down_arrowbw)
|
||||||
|
.setNotificationSilent()
|
||||||
|
.setSubText("Total: $total Completed:$converted")
|
||||||
|
.setStyle(NotificationCompat.InboxStyle()
|
||||||
|
.setBigContentTitle("Speed: $speed KB/s")
|
||||||
|
.addLine(messageList[0])
|
||||||
|
.addLine(messageList[1])
|
||||||
|
.addLine(messageList[2])
|
||||||
|
.addLine(messageList[3]))
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SameParameterValue")
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun createNotificationChannel(channelId: String, channelName: String): String{
|
||||||
|
val chan = NotificationChannel(channelId,
|
||||||
|
channelName, NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
|
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
|
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
service.createNotificationChannel(chan)
|
||||||
|
return channelId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
deleteFile(file)
|
||||||
|
} else if(file.isFile) {
|
||||||
|
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
||||||
|
Log.i(tag,"deleting ${file.path}")
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to fetch all Images for use in mp3 tags.
|
||||||
|
**/
|
||||||
|
private suspend fun loadAllImages(urlList: ArrayList<String>) {
|
||||||
|
/*
|
||||||
|
* Last Element of this List defines Its Source
|
||||||
|
* */
|
||||||
|
val source = urlList.last()
|
||||||
|
for (url in urlList.subList(0,urlList.size-2)) {
|
||||||
|
val imgUri = url.toUri().buildUpon().scheme("https").build()
|
||||||
|
Glide
|
||||||
|
.with(this)
|
||||||
|
.asFile()
|
||||||
|
.load(imgUri)
|
||||||
|
.listener(object: RequestListener<File> {
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
Log.i("Glide","LoadFailed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: File?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
dataSource: DataSource?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
serviceScope.launch {
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
try {
|
||||||
|
val file = when(source){
|
||||||
|
"spotify" ->{
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"youtube" ->{
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg")
|
||||||
|
}
|
||||||
|
resource?.copyTo(file)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}).submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -20,7 +20,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:id="@+id/main_youtube"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="25dp"
|
android:layout_marginTop="25dp"
|
||||||
@ -28,7 +28,7 @@
|
|||||||
tools:context=".ui.spotify.SpotifyFragment">
|
tools:context=".ui.spotify.SpotifyFragment">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
android:id="@+id/btn_download_all_spotify"
|
android:id="@+id/btn_download_all"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:background="@drawable/btn_design"
|
android:background="@drawable/btn_design"
|
||||||
@ -39,11 +39,11 @@
|
|||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_anchor="@+id/appbar_spotify"
|
app:layout_anchor="@+id/appbar"
|
||||||
app:layout_anchorGravity="bottom|center" />
|
app:layout_anchorGravity="bottom|center" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/downloading_fab_spotify"
|
android:id="@+id/downloading_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:keepScreenOn="true"
|
android:keepScreenOn="true"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -51,7 +51,7 @@
|
|||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:borderWidth="0dp"
|
app:borderWidth="0dp"
|
||||||
app:layout_anchor="@+id/appbar_spotify"
|
app:layout_anchor="@+id/appbar"
|
||||||
app:layout_anchorGravity="bottom|center"
|
app:layout_anchorGravity="bottom|center"
|
||||||
app:maxImageSize="38dp"
|
app:maxImageSize="38dp"
|
||||||
app:rippleColor="@color/colorPrimaryDark"
|
app:rippleColor="@color/colorPrimaryDark"
|
||||||
@ -59,7 +59,7 @@
|
|||||||
app:tint="@null" />
|
app:tint="@null" />
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appbar_spotify"
|
android:id="@+id/appbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="280dp">
|
android:layout_height="280dp">
|
||||||
|
|
||||||
@ -72,14 +72,14 @@
|
|||||||
app:toolbarId="@+id/toolbar">
|
app:toolbarId="@+id/toolbar">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/TopLayout_spotify"
|
android:id="@+id/topLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:foreground="@drawable/gradient"
|
android:foreground="@drawable/gradient"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/spotify_cover_image"
|
android:id="@+id/cover_image"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="28dp"
|
android:layout_marginTop="28dp"
|
||||||
@ -89,13 +89,13 @@
|
|||||||
android:src="@drawable/spotify_download"
|
android:src="@drawable/spotify_download"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_collapseMode="parallax"
|
app:layout_collapseMode="parallax"
|
||||||
app:layout_constraintBottom_toTopOf="@id/title_view_spotify"
|
app:layout_constraintBottom_toTopOf="@id/title_view"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/StatusBar_spotify"
|
android:id="@+id/statusBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
@ -113,11 +113,11 @@
|
|||||||
android:textColor="@color/grey"
|
android:textColor="@color/grey"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/spotify_cover_image"
|
app:layout_constraintBottom_toTopOf="@+id/cover_image"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/title_view_spotify"
|
android:id="@+id/title_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/track_list_spotify"
|
android:id="@+id/track_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingTop="26dp"
|
android:paddingTop="26dp"
|
||||||
@ -152,7 +152,7 @@
|
|||||||
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/appbar_spotify" />
|
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -19,14 +19,14 @@
|
|||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:id="@+id/main_youtube"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="25dp"
|
android:layout_marginTop="25dp"
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
android:id="@+id/btn_download_all_youtube"
|
android:id="@+id/btn_download_all"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:background="@drawable/btn_design"
|
android:background="@drawable/btn_design"
|
||||||
@ -38,17 +38,17 @@
|
|||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_anchor="@+id/appbar_youtube"
|
app:layout_anchor="@+id/appbar"
|
||||||
app:layout_anchorGravity="bottom|center" />
|
app:layout_anchorGravity="bottom|center" />
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/downloading_fab_youtube"
|
android:id="@+id/downloading_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:backgroundTint="@color/black"
|
android:backgroundTint="@color/black"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:borderWidth="0dp"
|
app:borderWidth="0dp"
|
||||||
app:layout_anchor="@+id/appbar_youtube"
|
app:layout_anchor="@+id/appbar"
|
||||||
app:layout_anchorGravity="bottom|center"
|
app:layout_anchorGravity="bottom|center"
|
||||||
app:maxImageSize="38dp"
|
app:maxImageSize="38dp"
|
||||||
app:rippleColor="@color/colorPrimaryDark"
|
app:rippleColor="@color/colorPrimaryDark"
|
||||||
@ -56,7 +56,7 @@
|
|||||||
app:tint="@null" />
|
app:tint="@null" />
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appbar_youtube"
|
android:id="@+id/appbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="230dp">
|
android:layout_height="230dp">
|
||||||
|
|
||||||
@ -69,14 +69,14 @@
|
|||||||
app:toolbarId="@+id/toolbar">
|
app:toolbarId="@+id/toolbar">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/TopLayout_youtube"
|
android:id="@+id/topLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:foreground="@drawable/gradient"
|
android:foreground="@drawable/gradient"
|
||||||
>
|
>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/youtube_cover_image"
|
android:id="@+id/cover_image"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="23dp"
|
android:layout_marginTop="23dp"
|
||||||
@ -86,13 +86,13 @@
|
|||||||
android:src="@drawable/spotify_download"
|
android:src="@drawable/spotify_download"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_collapseMode="parallax"
|
app:layout_collapseMode="parallax"
|
||||||
app:layout_constraintBottom_toTopOf="@id/title_view_youtube"
|
app:layout_constraintBottom_toTopOf="@id/title_view"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/StatusBar_youtube"
|
android:id="@+id/statusBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
@ -109,11 +109,11 @@
|
|||||||
android:textColor="@color/grey"
|
android:textColor="@color/grey"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/youtube_cover_image"
|
app:layout_constraintBottom_toTopOf="@+id/cover_image"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/title_view_youtube"
|
android:id="@+id/title_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
@ -136,7 +136,7 @@
|
|||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/track_list_youtube"
|
android:id="@+id/track_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingTop="26dp"
|
android:paddingTop="26dp"
|
||||||
@ -146,7 +146,7 @@
|
|||||||
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/appbar_youtube" />
|
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</layout>
|
</layout>
|
||||||
|
Loading…
Reference in New Issue
Block a user