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">
|
||||
<dictionary name="shabinder">
|
||||
<words>
|
||||
<w>cherrypick</w>
|
||||
<w>downloadrecord</w>
|
||||
<w>emoji</w>
|
||||
<w>ffmpeg</w>
|
||||
|
@ -118,6 +118,7 @@ dependencies {
|
||||
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.beust:klaxon:5.4'
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
|
||||
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
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.enums.UpdateFrom
|
||||
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.SpotifyServiceTokenRequest
|
||||
import com.shabinder.spotiflyer.utils.createDirectories
|
||||
import com.shabinder.spotiflyer.utils.startService
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
@ -84,7 +85,7 @@ class MainActivity : AppCompatActivity(){
|
||||
Log.i("Connection Status", isConnected.toString())
|
||||
|
||||
//starting Notification and Downloader Service!
|
||||
SpotifyDownloadHelper.startService(this)
|
||||
startService(this)
|
||||
|
||||
handleIntentFromExternalActivity()
|
||||
}
|
||||
@ -227,5 +228,6 @@ class MainActivity : AppCompatActivity(){
|
||||
}
|
||||
init {
|
||||
instance = this
|
||||
activity = this
|
||||
}
|
||||
}
|
@ -17,25 +17,20 @@
|
||||
|
||||
package com.shabinder.spotiflyer.downloadHelper
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
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 android.widget.Toast
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.models.*
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.shabinder.spotiflyer.utils.Provider.activity
|
||||
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -45,13 +40,13 @@ import retrofit2.Response
|
||||
import java.io.File
|
||||
|
||||
object SpotifyDownloadHelper {
|
||||
var context : Context? = null
|
||||
|
||||
var statusBar:TextView? = null
|
||||
var youtubeMusicApi:YoutubeMusicApi? = null
|
||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||
var spotifyViewModel: SpotifyViewModel? = null
|
||||
var total = 0
|
||||
var Processed = 0
|
||||
var sharedViewModel: SharedViewModel? = null
|
||||
|
||||
private var total = 0
|
||||
private var processed = 0
|
||||
var notFound = 0
|
||||
|
||||
/**
|
||||
@ -60,16 +55,94 @@ object SpotifyDownloadHelper {
|
||||
suspend fun downloadAllTracks(
|
||||
type:String,
|
||||
subFolder: String?,
|
||||
trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
|
||||
trackList: List<Track>) {
|
||||
val downloadList = ArrayList<DownloadObject>()
|
||||
|
||||
withContext(Dispatchers.Main){
|
||||
total += trackList.size // Adding New Download List Count to StatusBar
|
||||
trackList.forEach {
|
||||
if(it.downloaded == "Downloaded"){//Download Already Present!!
|
||||
Processed++
|
||||
trackList.forEachIndexed { index, it ->
|
||||
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
|
||||
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{
|
||||
val artistsList = mutableListOf<String>()
|
||||
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()
|
||||
}
|
||||
@ -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() {
|
||||
val anim: Animation = AlphaAnimation(0.0f, 0.9f)
|
||||
anim.duration = 650 //You can manage the blinking time with this parameter
|
||||
val anim: Animation = AlphaAnimation(0.3f, 0.9f)
|
||||
anim.duration = 1500 //You can manage the blinking time with this parameter
|
||||
anim.startOffset = 20
|
||||
anim.repeatMode = Animation.REVERSE
|
||||
anim.repeatCount = Animation.INFINITE
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import android.widget.Toast
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
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
|
||||
|
||||
object YTDownloadHelper {
|
||||
var context : Context? = null
|
||||
var statusBar: TextView? = null
|
||||
|
||||
fun downloadFile(subFolder: String?, type: String,ytTrack: Track,format: Format?) {
|
||||
format?.let {
|
||||
val url:String = format.url()
|
||||
// Log.i("DHelper Link Found", url)
|
||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||
SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator} + SpotifyDownloadHelper.removeIllegalChars(
|
||||
ytTrack.name!!
|
||||
) +".m4a")
|
||||
suspend fun downloadYTTracks(
|
||||
type:String,
|
||||
subFolder: String?,
|
||||
tracks:List<TrackDetails>,
|
||||
){
|
||||
val downloadList = ArrayList<DownloadObject>()
|
||||
tracks.forEach {
|
||||
val outputFile: String =
|
||||
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||
defaultDir +
|
||||
removeIllegalChars(type) + File.separator +
|
||||
(if (subFolder == null) { "" }
|
||||
else { removeIllegalChars(subFolder) + File.separator }
|
||||
+ removeIllegalChars(it.title) + ".m4a")
|
||||
|
||||
val downloadObject = DownloadObject(
|
||||
track = ytTrack,
|
||||
url = url,
|
||||
outputDir = outputFile
|
||||
trackDetails = it,
|
||||
ytVideoId = "https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
|
||||
.substringBeforeLast(".")}/maxresdefault.jpg",
|
||||
outputFile = outputFile
|
||||
)
|
||||
Log.i("DH",outputFile)
|
||||
startService(context!!, downloadObject)
|
||||
statusBar?.visibility= View.VISIBLE
|
||||
|
||||
downloadList.add(downloadObject)
|
||||
}
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import com.beust.klaxon.JsonArray
|
||||
import com.beust.klaxon.JsonObject
|
||||
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 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,
|
||||
subFolder:String?,
|
||||
ytDownloader: YoutubeDownloader?,
|
||||
response: String,
|
||||
track: Track
|
||||
){
|
||||
//TODO Download File
|
||||
fun getYTTracks(response: String):List<YoutubeTrack>{
|
||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||
val parser: Parser = Parser.default()
|
||||
|
||||
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 resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
||||
if (contentBlocks != null) {
|
||||
Log.i("Total Content Blocks:", contentBlocks.size.toString())
|
||||
for (cBlock in contentBlocks){
|
||||
/**
|
||||
*Ignore user-suggestion
|
||||
@ -109,8 +101,6 @@ fun getYTLink(type:String,
|
||||
! we do so only if their Type is 'Song' or 'Video
|
||||
*/
|
||||
|
||||
val simplifiedResults = mutableListOf<JsonObject>()
|
||||
|
||||
for(result in resultBlocks){
|
||||
|
||||
// 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
|
||||
! 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 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
|
||||
! 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") ){
|
||||
|
||||
// skip if result is in hours instead of minutes (no song is that long)
|
||||
// if(availableDetails[4].split(':').size != 2) continue TODO
|
||||
|
||||
/*
|
||||
! 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)
|
||||
if(availableDetails[4].split(':').size != 2) continue //Has Been Giving Issues
|
||||
|
||||
/*
|
||||
! grab Video ID
|
||||
@ -168,23 +151,86 @@ fun getYTLink(type:String,
|
||||
name = availableDetails[0],
|
||||
type = availableDetails[1],
|
||||
artist = availableDetails[2],
|
||||
duration = availableDetails[4],
|
||||
videoId = videoId
|
||||
)
|
||||
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++
|
||||
else downloadFile(
|
||||
subFolder,
|
||||
type,
|
||||
track,
|
||||
ytDownloader,
|
||||
id = youtubeTracks[0].videoId.toString()
|
||||
)
|
||||
Log.i("DHelper YT ID", youtubeTracks.firstOrNull()?.videoId ?: "Not Found")
|
||||
SpotifyDownloadHelper.updateStatusBar()
|
||||
return youtubeTracks
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun sortByBestMatch(ytTracks:List<YoutubeTrack>,
|
||||
trackName:String,
|
||||
trackArtists:List<String>,
|
||||
trackDurationSec:Int,
|
||||
):Map<String,Int>{
|
||||
/*
|
||||
* "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 kotlinx.android.parcel.Parcelize
|
||||
import java.io.File
|
||||
|
||||
@Parcelize
|
||||
data class DownloadObject(
|
||||
var ytVideo: YTTrack?=null,
|
||||
var track: Track?=null,
|
||||
var url:String,
|
||||
var outputDir:String
|
||||
var trackDetails: TrackDetails,
|
||||
var ytVideoId:String,
|
||||
var outputFile:String
|
||||
):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 external_urls: Map<String?, String?>? = null,
|
||||
var href: String? = null,
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
var preview_url: String? = null,
|
||||
var track_number: Int = 0,
|
||||
@ -40,5 +39,5 @@ data class Track(
|
||||
var album: Album? = null,
|
||||
var external_ids: Map<String?, String?>? = null,
|
||||
var popularity: Int? = null,
|
||||
var ytCoverUrl:String? = null,
|
||||
var downloaded:String? = "notDownloaded"):Parcelable
|
||||
var downloaded:DownloadStatus? = DownloadStatus.NotDownloaded):Parcelable
|
||||
|
||||
|
@ -25,5 +25,6 @@ data class YoutubeTrack(
|
||||
var name: String? = null,
|
||||
var type: String? = null, // Song / Video
|
||||
var artist: String? = null,
|
||||
var duration:String? = null,
|
||||
var videoId: String? = null
|
||||
):Parcelable
|
@ -17,6 +17,7 @@
|
||||
|
||||
package com.shabinder.spotiflyer.recyclerView
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -24,13 +25,14 @@ import android.widget.Toast
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.context
|
||||
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.ui.spotify.SpotifyViewModel
|
||||
import com.shabinder.spotiflyer.utils.Provider.activity
|
||||
import com.shabinder.spotiflyer.utils.bindImage
|
||||
import com.shabinder.spotiflyer.utils.rotateAnim
|
||||
import kotlinx.coroutines.launch
|
||||
@ -40,7 +42,6 @@ class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
|
||||
|
||||
var spotifyViewModel : SpotifyViewModel? = null
|
||||
var isAlbum:Boolean = false
|
||||
var ytDownloader: YoutubeDownloader? = null
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
@ -49,13 +50,14 @@ class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
if(itemCount ==1 || isAlbum){
|
||||
holder.binding.imageUrl.visibility = View.GONE}else{
|
||||
spotifyViewModel!!.uiScope.launch {
|
||||
//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.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
||||
when (item.downloaded) {
|
||||
"Downloaded" -> {
|
||||
DownloadStatus.Downloaded -> {
|
||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
|
||||
holder.binding.btnDownload.clearAnimation()
|
||||
}
|
||||
"Downloading" -> {
|
||||
DownloadStatus.Downloading -> {
|
||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
||||
rotateAnim(holder.binding.btnDownload)
|
||||
}
|
||||
"notDownloaded" -> {
|
||||
DownloadStatus.NotDownloaded -> {
|
||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
|
||||
holder.binding.btnDownload.clearAnimation()
|
||||
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)
|
||||
rotateAnim(it)
|
||||
item.downloaded = "Downloading"
|
||||
item.downloaded = DownloadStatus.Downloading
|
||||
spotifyViewModel!!.uiScope.launch {
|
||||
val itemList = mutableListOf<Track>()
|
||||
itemList.add(item)
|
||||
downloadAllTracks(spotifyViewModel!!.folderType,spotifyViewModel!!.subFolder,itemList,ytDownloader)
|
||||
downloadAllTracks(spotifyViewModel!!.folderType,spotifyViewModel!!.subFolder,itemList)
|
||||
}
|
||||
notifyItemChanged(position)//start showing anim!
|
||||
}
|
||||
|
@ -22,18 +22,16 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.models.Source
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.utils.bindImage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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)
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
@ -51,26 +49,31 @@ class YoutubeTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
|
||||
if(itemCount == 1){
|
||||
holder.binding.imageUrl.visibility = View.GONE}else{
|
||||
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.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.trackName.text = "${if(item.title.length > 17){"${item.title.subSequence(0,16)}..."}else{item.title}}"
|
||||
holder.binding.artist.text = "${item.artists.get(0)}..."
|
||||
holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec"
|
||||
holder.binding.btnDownload.setOnClickListener{
|
||||
adapterScope.launch {
|
||||
YTDownloadHelper.downloadFile(null,"YT_Downloads",item,format)
|
||||
// YTDownloadHelper.downloadFile(null,"YT_Downloads",item,format)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<Track>(){
|
||||
override fun areItemsTheSame(oldItem: Track, newItem: Track): Boolean {
|
||||
return oldItem.name == newItem.name
|
||||
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){
|
||||
override fun areItemsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean {
|
||||
return oldItem.title == newItem.title
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Track, newItem: Track): Boolean {
|
||||
override fun areContentsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
@ -24,41 +24,33 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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.R
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.databinding.SpotifyFragmentBinding
|
||||
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.TrackDetails
|
||||
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
|
||||
import com.shabinder.spotiflyer.utils.YoutubeMusicApi
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@ -69,7 +61,6 @@ class SpotifyFragment : Fragment() {
|
||||
private lateinit var spotifyViewModel: SpotifyViewModel
|
||||
private lateinit var sharedViewModel: SharedViewModel
|
||||
private lateinit var adapterSpotify:SpotifyTrackListAdapter
|
||||
@Inject lateinit var ytDownloader:YoutubeDownloader
|
||||
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
|
||||
private var intentFilter:IntentFilter? = null
|
||||
private var updateUIReceiver: BroadcastReceiver? = null
|
||||
@ -109,30 +100,34 @@ class SpotifyFragment : Fragment() {
|
||||
spotifyViewModel.spotifySearch(type,link)
|
||||
if(type=="album")adapterSpotify.isAlbum = true
|
||||
|
||||
binding.btnDownloadAllSpotify.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.setOnClickListener {
|
||||
|
||||
binding.btnDownloadAll.visibility = View.GONE
|
||||
binding.downloadingFab.visibility = View.VISIBLE
|
||||
|
||||
rotateAnim(binding.downloadingFabSpotify)
|
||||
rotateAnim(binding.downloadingFab)
|
||||
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))
|
||||
}
|
||||
}
|
||||
showToast("Starting Download in Few Seconds")
|
||||
spotifyViewModel.uiScope.launch(Dispatchers.Default){loadAllImages(spotifyViewModel.trackList.value!!)}
|
||||
showToast("Processing!")
|
||||
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 {
|
||||
SpotifyDownloadHelper.downloadAllTracks(
|
||||
spotifyViewModel.folderType,
|
||||
spotifyViewModel.subFolder,
|
||||
spotifyViewModel.trackList.value!!,
|
||||
ytDownloader
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -154,15 +149,18 @@ class SpotifyFragment : Fragment() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
//UI update here
|
||||
if (intent != null){
|
||||
val track = intent.getParcelableExtra<Track?>("track")
|
||||
track?.let {
|
||||
val position: Int = spotifyViewModel.trackList.value?.indexOf(track)!!
|
||||
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
|
||||
trackDetails?.let {
|
||||
val position: Int = spotifyViewModel.trackList.value?.map { it.name }?.indexOf(trackDetails.title) ?: -1
|
||||
Log.i("Track","Download Completed Intent :$position")
|
||||
track.downloaded = "Downloaded"
|
||||
if(position != -1) {
|
||||
spotifyViewModel.trackList.value?.set(position, track)
|
||||
adapterSpotify.notifyItemChanged(position)
|
||||
checkIfAllDownloaded()
|
||||
val track = spotifyViewModel.trackList.value?.get(position)
|
||||
track?.let{
|
||||
it.downloaded = DownloadStatus.Downloaded
|
||||
spotifyViewModel.trackList.value?.set(position, it)
|
||||
adapterSpotify.notifyItemChanged(position)
|
||||
checkIfAllDownloaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,7 +182,7 @@ class SpotifyFragment : Fragment() {
|
||||
* CoverUrl Binding Observer!
|
||||
**/
|
||||
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!
|
||||
**/
|
||||
spotifyViewModel.title.observe(viewLifecycleOwner, {
|
||||
binding.titleViewSpotify.text = it
|
||||
binding.titleView.text = it
|
||||
})
|
||||
|
||||
sharedViewModel.intentString.observe(viewLifecycleOwner,{
|
||||
@ -215,10 +213,10 @@ class SpotifyFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun checkIfAllDownloaded() {
|
||||
if(!spotifyViewModel.trackList.value!!.any { it.downloaded != "Downloaded" }){
|
||||
if(!spotifyViewModel.trackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
|
||||
//All Tracks Downloaded
|
||||
binding.btnDownloadAllSpotify.visibility = View.GONE
|
||||
binding.downloadingFabSpotify.apply{
|
||||
binding.btnDownloadAll.visibility = View.GONE
|
||||
binding.downloadingFab.apply{
|
||||
setImageResource(R.drawable.ic_tick)
|
||||
visibility = View.VISIBLE
|
||||
clearAnimation()
|
||||
@ -236,69 +234,17 @@ class SpotifyFragment : Fragment() {
|
||||
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
|
||||
spotifyViewModel.spotifyService = it
|
||||
})
|
||||
SpotifyDownloadHelper.context = requireContext()
|
||||
SpotifyDownloadHelper.youtubeMusicApi = youtubeMusicApi
|
||||
SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel
|
||||
SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify
|
||||
binding.trackListSpotify.adapter = adapterSpotify
|
||||
(binding.trackListSpotify.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()
|
||||
}
|
||||
}
|
||||
SpotifyDownloadHelper.sharedViewModel = sharedViewModel
|
||||
SpotifyDownloadHelper.statusBar = binding.statusBar
|
||||
binding.trackList.adapter = adapterSpotify
|
||||
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Recycler View Adapter
|
||||
**/
|
||||
private fun adapterConfig(trackList: List<Track>){
|
||||
adapterSpotify.ytDownloader = ytDownloader
|
||||
adapterSpotify.spotifyViewModel = spotifyViewModel
|
||||
adapterSpotify.submitList(trackList)
|
||||
}
|
||||
@ -320,4 +266,5 @@ class SpotifyFragment : Fragment() {
|
||||
val netInfo = cm.activeNetworkInfo
|
||||
return netInfo != null && netInfo.isConnectedOrConnecting
|
||||
}
|
||||
|
||||
}
|
@ -52,7 +52,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
folderType = "Tracks"
|
||||
val tempTrackList = mutableListOf<Track>()
|
||||
if(File(finalOutputDir(trackObject?.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||
trackObject.downloaded = "Downloaded"
|
||||
trackObject.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
tempTrackList.add(trackObject)
|
||||
trackList.value = tempTrackList
|
||||
@ -65,7 +65,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl.value!!,
|
||||
totalFiles = tempTrackList.size,
|
||||
downloaded = trackObject.downloaded =="Downloaded",
|
||||
downloaded = trackObject.downloaded == DownloadStatus.Downloaded,
|
||||
directory = finalOutputDir(trackObject.name!!,folderType,subFolder)
|
||||
))
|
||||
}
|
||||
@ -80,7 +80,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
val tempTrackList = mutableListOf<Track>()
|
||||
albumObject?.tracks?.items?.forEach {
|
||||
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)))
|
||||
tempTrackList.add(it)
|
||||
@ -112,7 +112,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
playlistObject?.tracks?.items?.forEach {
|
||||
it.track?.let {
|
||||
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||
it1.downloaded = "Downloaded"
|
||||
it1.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
tempTrackList.add(it1)
|
||||
}
|
||||
@ -130,13 +130,13 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
Log.i("Total Tracks Fetched",tempTrackList.size.toString())
|
||||
trackList.value = tempTrackList
|
||||
title.value = playlistObject?.name
|
||||
coverUrl.value = playlistObject?.images?.get(0)!!.url!!
|
||||
coverUrl.value = playlistObject?.images?.get(0)?.url.toString()
|
||||
withContext(Dispatchers.IO){
|
||||
databaseDAO.insert(DownloadRecord(
|
||||
type = "Playlist",
|
||||
name = title.value!!,
|
||||
name = title.value.toString(),
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl.value!!,
|
||||
coverUrl = coverUrl.value.toString(),
|
||||
totalFiles = tempTrackList.size,
|
||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||
|
@ -24,17 +24,22 @@ import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.databinding.YoutubeFragmentBinding
|
||||
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.utils.bindImage
|
||||
import com.shabinder.spotiflyer.utils.loadAllImages
|
||||
import com.shabinder.spotiflyer.utils.rotateAnim
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@ -43,10 +48,10 @@ class YoutubeFragment : Fragment() {
|
||||
private lateinit var binding:YoutubeFragmentBinding
|
||||
private lateinit var youtubeViewModel: YoutubeViewModel
|
||||
private lateinit var sharedViewModel: SharedViewModel
|
||||
@Inject lateinit var ytDownloader: YoutubeDownloader
|
||||
private lateinit var adapter : YoutubeTrackListAdapter
|
||||
private val sampleDomain1 = "youtube.com"
|
||||
private val sampleDomain2 = "youtu.be"
|
||||
@Inject lateinit var ytDownloader: YoutubeDownloader
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
@ -56,9 +61,7 @@ class YoutubeFragment : Fragment() {
|
||||
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
adapter = YoutubeTrackListAdapter()
|
||||
YTDownloadHelper.context = requireContext()
|
||||
YTDownloadHelper.statusBar = binding.StatusBarYoutube
|
||||
binding.trackListYoutube.adapter = adapter
|
||||
binding.trackList.adapter = adapter
|
||||
|
||||
initializeLiveDataObservers()
|
||||
|
||||
@ -70,7 +73,11 @@ class YoutubeFragment : Fragment() {
|
||||
|
||||
private fun youtubeSearch(linkSearch:String) {
|
||||
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"
|
||||
if(link.contains(sampleDomain1,true) ){
|
||||
searchId = link.substringAfterLast("=","error")
|
||||
@ -79,41 +86,63 @@ class YoutubeFragment : Fragment() {
|
||||
searchId = link.substringAfterLast("/","error")
|
||||
}
|
||||
if(searchId != "error") {
|
||||
youtubeViewModel.getYTTrack(searchId,ytDownloader)
|
||||
binding.btnDownloadAllYoutube.setOnClickListener {
|
||||
YTDownloadHelper.downloadFile(null,"YT_Downloads",
|
||||
youtubeViewModel.ytTrack.value!!,youtubeViewModel.format.value)
|
||||
}
|
||||
youtubeViewModel.getYTTrack(searchId,ytDownloader)
|
||||
}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() {
|
||||
/**
|
||||
* CoverUrl Binding Observer!
|
||||
**/
|
||||
youtubeViewModel.coverUrl.observe(viewLifecycleOwner, Observer {
|
||||
if(it!="Loading") bindImage(binding.youtubeCoverImage,it)
|
||||
youtubeViewModel.coverUrl.observe(viewLifecycleOwner, {
|
||||
if(it!="Loading") bindImage(binding.coverImage,it,Source.YouTube)
|
||||
})
|
||||
|
||||
/**
|
||||
* TrackList Binding Observer!
|
||||
**/
|
||||
youtubeViewModel.ytTrack.observe(viewLifecycleOwner, Observer {
|
||||
val list = mutableListOf<Track>()
|
||||
list.add(it)
|
||||
adapterConfig(list)
|
||||
})
|
||||
|
||||
youtubeViewModel.format.observe(viewLifecycleOwner, Observer {
|
||||
adapter.format = it
|
||||
youtubeViewModel.ytTrackList.observe(viewLifecycleOwner, {
|
||||
adapterConfig(it)
|
||||
})
|
||||
|
||||
/**
|
||||
* Title Binding Observer!
|
||||
**/
|
||||
youtubeViewModel.title.observe(viewLifecycleOwner, Observer {
|
||||
binding.titleViewYoutube.text = it
|
||||
youtubeViewModel.title.observe(viewLifecycleOwner, {
|
||||
binding.titleView.text = it
|
||||
})
|
||||
|
||||
}
|
||||
@ -121,7 +150,7 @@ class YoutubeFragment : Fragment() {
|
||||
/**
|
||||
* Configure Recycler View Adapter
|
||||
**/
|
||||
private fun adapterConfig(list:List<Track>){
|
||||
private fun adapterConfig(list:List<TrackDetails>){
|
||||
adapter.submitList(list)
|
||||
}
|
||||
|
||||
|
@ -17,78 +17,119 @@
|
||||
|
||||
package com.shabinder.spotiflyer.ui.youtube
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.hilt.lifecycle.ViewModelInject
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.Artist
|
||||
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.utils.Provider
|
||||
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) :
|
||||
ViewModel(){
|
||||
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : 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>()
|
||||
private val loading = "Loading"
|
||||
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
|
||||
var coverUrl = MutableLiveData<String>().apply { value = loading }
|
||||
|
||||
val folderType = "YT_Downloads"
|
||||
var subFolder = ""
|
||||
private var viewModelJob = Job()
|
||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||
|
||||
|
||||
fun getYTTrack(searchId:String,ytDownloader:YoutubeDownloader) {
|
||||
uiScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
Log.i("YT View Model",searchId)
|
||||
val video = ytDownloader.getVideo(searchId)
|
||||
val detail = video?.details()
|
||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title()
|
||||
Log.i("YT View Model",detail.toString())
|
||||
ytTrack.postValue(
|
||||
Track(
|
||||
id = searchId,
|
||||
name = name,
|
||||
artists = listOf<Artist>(Artist(name = detail?.author())),
|
||||
duration_ms = detail?.lengthSeconds()?.times(1000)?.toLong()?:0,
|
||||
ytCoverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||
))
|
||||
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg")
|
||||
title.postValue(
|
||||
if(name?.length!! > 17){"${name.subSequence(0,16)}..."}else{name}
|
||||
fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){
|
||||
uiScope.launch(Dispatchers.IO) {
|
||||
Log.i("YT Playlist",searchId)
|
||||
val playlist = ytDownloader.getPlaylist(searchId)
|
||||
val playlistDetails = playlist.details()
|
||||
val name = playlistDetails.title()
|
||||
subFolder = removeIllegalChars(name).toString()
|
||||
val videos = playlist.videos()
|
||||
coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg")
|
||||
title.postValue(
|
||||
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
|
||||
)
|
||||
ytTrackList.postValue(videos.map {
|
||||
TrackDetails(
|
||||
title = it.title(),
|
||||
artists = listOf(it.author().toString()),
|
||||
durationSec = it.lengthSeconds(),
|
||||
albumArt = File(
|
||||
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){
|
||||
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")
|
||||
))
|
||||
}
|
||||
databaseDAO.insert(DownloadRecord(
|
||||
type = "PlayList",
|
||||
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
|
||||
link = "https://www.youtube.com/playlist?list=$searchId",
|
||||
coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg",
|
||||
totalFiles = videos.size,
|
||||
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.spotiflyer.App
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
@ -38,18 +39,28 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import java.io.File
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(ApplicationComponent::class)
|
||||
@Module
|
||||
object Provider {
|
||||
|
||||
lateinit var activity: MainActivity
|
||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||
|
||||
|
||||
@Provides
|
||||
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
|
||||
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getYTDownloader():YoutubeDownloader{
|
||||
return YoutubeDownloader()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@ -72,12 +83,6 @@ object Provider {
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getYTDownloader():YoutubeDownloader{
|
||||
return YoutubeDownloader()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
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
|
||||
|
||||
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() {
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir)
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + ".Images/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Tracks/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Albums/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Playlists/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "YT_Downloads/")
|
||||
createDirectory(defaultDir)
|
||||
createDirectory(defaultDir + ".Images/")
|
||||
createDirectory(defaultDir + "Tracks/")
|
||||
createDirectory(defaultDir + "Albums/")
|
||||
createDirectory(defaultDir + "Playlists/")
|
||||
createDirectory(defaultDir + "YT_Downloads/")
|
||||
}
|
||||
fun getEmojiByUnicode(unicode: Int): String? {
|
||||
return String(Character.toChars(unicode))
|
||||
|
@ -29,27 +29,36 @@ import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.arthenica.mobileffmpeg.Config
|
||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
||||
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.ID3v24Tag
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
||||
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.fetch2core.DownloadBlock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class ForegroundService : Service(){
|
||||
private val tag = "Foreground Service"
|
||||
private val channelId = "ForegroundDownloaderService"
|
||||
@ -57,22 +66,21 @@ class ForegroundService : Service(){
|
||||
private var total = 0 //Total Downloads Requested
|
||||
private var converted = 0//Total Files Converted
|
||||
private var downloaded = 0//Total Files downloaded
|
||||
private var fetch:Fetch? = null
|
||||
private var downloadManager : DownloadManager? = null
|
||||
private var downloadList = mutableListOf<DownloadObject>()
|
||||
private lateinit var fetch:Fetch
|
||||
private lateinit var ytDownloader: YoutubeDownloader
|
||||
private lateinit var downloadManager : DownloadManager
|
||||
private var serviceJob = Job()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||
private val requestMap = mutableMapOf<Request,Track>()
|
||||
private val downloadMap = mutableMapOf<String,Track>()
|
||||
private val requestMap = mutableMapOf<Request,TrackDetails>()
|
||||
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(),
|
||||
defaultDirectory+File.separator
|
||||
defaultDir +File.separator
|
||||
)
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var isServiceStarted = false
|
||||
var notificationLine = 0
|
||||
val messageList = mutableListOf<String>("","","","")
|
||||
val messageList = mutableListOf("","","","")
|
||||
private var pendingIntent:PendingIntent? = null
|
||||
|
||||
|
||||
@ -89,7 +97,7 @@ class ForegroundService : Service(){
|
||||
0, notificationIntent, 0
|
||||
)
|
||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
ytDownloader = YoutubeDownloader()
|
||||
val fetchConfiguration =
|
||||
FetchConfiguration.Builder(this)
|
||||
.setDownloadConcurrentLimit(4)
|
||||
@ -98,86 +106,41 @@ class ForegroundService : Service(){
|
||||
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||
|
||||
fetch = Fetch.getDefaultInstance()
|
||||
// fetch?.enableLogging(true)
|
||||
fetch?.addListener(fetchListener)
|
||||
fetch.addListener(fetchListener)
|
||||
//clearing all not completed Downloads
|
||||
//Starting fresh
|
||||
fetch?.removeAll()
|
||||
fetch.removeAll()
|
||||
|
||||
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")
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
// Send a notification that service is started
|
||||
Log.i(tag,"Service Started.")
|
||||
startForeground()
|
||||
val obj:DownloadObject? = intent.getParcelableExtra("object") ?: intent.extras?.getParcelable("object")
|
||||
obj?.let {
|
||||
total ++
|
||||
updateNotification()
|
||||
serviceScope.launch {
|
||||
val request= Request(obj.url, obj.outputDir)
|
||||
request.priority = Priority.NORMAL
|
||||
request.networkType = NetworkType.ALL
|
||||
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
|
||||
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList"))
|
||||
|
||||
fetch!!.enqueue(request,
|
||||
{
|
||||
obj.track?.let { it1 -> requestMap.put(it, it1) }
|
||||
downloadList.remove(obj)
|
||||
Log.i(tag, "Enqueuing Download")
|
||||
},
|
||||
{
|
||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
||||
)
|
||||
imagesList?.let{
|
||||
serviceScope.launch {
|
||||
loadAllImages(it)
|
||||
}
|
||||
}
|
||||
|
||||
downloadObjects?.let {
|
||||
total += downloadObjects.size
|
||||
updateNotification()
|
||||
downloadAllTracks(downloadObjects)
|
||||
}
|
||||
|
||||
//Wake locks and misc tasks from here :
|
||||
return if (isServiceStarted){
|
||||
//Service Already Started
|
||||
START_STICKY
|
||||
} else{
|
||||
Log.i(tag,"Starting the foreground service task")
|
||||
isServiceStarted = true
|
||||
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
||||
@ -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() {
|
||||
super.onDestroy()
|
||||
if(downloadMap.isEmpty() && converted == total){
|
||||
if(converted == total){
|
||||
Handler().postDelayed({
|
||||
Log.i(tag,"Service destroyed.")
|
||||
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?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
if(downloadMap.isEmpty() && converted == total ){
|
||||
if(converted == total ){
|
||||
Log.i(tag,"Service Removed.")
|
||||
deleteFile(parentDirectory)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stopForeground(true)
|
||||
} 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
|
||||
@ -274,23 +248,23 @@ class ForegroundService : Service(){
|
||||
val track = requestMap[download.request]
|
||||
when(notificationLine){
|
||||
0 -> {
|
||||
messageList[0] = "Downloading ${track?.name}"
|
||||
messageList[0] = "Downloading ${track?.title}"
|
||||
notificationLine = 1
|
||||
}
|
||||
1 -> {
|
||||
messageList[1] = "Downloading ${track?.name}"
|
||||
messageList[1] = "Downloading ${track?.title}"
|
||||
notificationLine = 2
|
||||
}
|
||||
2-> {
|
||||
messageList[2] = "Downloading ${track?.name}"
|
||||
messageList[2] = "Downloading ${track?.title}"
|
||||
notificationLine = 3
|
||||
}
|
||||
3 -> {
|
||||
messageList[3] = "Downloading ${track?.name}"
|
||||
messageList[3] = "Downloading ${track?.title}"
|
||||
notificationLine = 0
|
||||
}
|
||||
}
|
||||
Log.i(tag,"${track?.name} Download Started")
|
||||
Log.i(tag,"${track?.title} Download Started")
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
@ -309,19 +283,20 @@ class ForegroundService : Service(){
|
||||
override fun onCompleted(download: Download) {
|
||||
val track = requestMap[download.request]
|
||||
for (message in messageList){
|
||||
if( message == "Downloading ${track?.name}"){
|
||||
if( message == "Downloading ${track?.title}"){
|
||||
//Remove Downloading Status from Notification
|
||||
messageList[messageList.indexOf(message)] = ""
|
||||
}
|
||||
}
|
||||
|
||||
serviceScope.launch {
|
||||
try{
|
||||
convertToMp3(download.file, track!!)
|
||||
Log.i(tag,"${track.name} Download Completed")
|
||||
track?.let { convertToMp3(download.file, it) }
|
||||
Log.i(tag,"${track?.title} Download Completed")
|
||||
}catch (e:KotlinNullPointerException
|
||||
){
|
||||
Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!")
|
||||
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
||||
Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!")
|
||||
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||
downloaded++
|
||||
requestMap.remove(download.request)
|
||||
@ -348,7 +323,7 @@ class ForegroundService : Service(){
|
||||
val track = requestMap[download.request]
|
||||
downloaded++
|
||||
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!!)
|
||||
requestMap.remove(download.request)
|
||||
}
|
||||
@ -365,7 +340,7 @@ class ForegroundService : Service(){
|
||||
downloadedBytesPerSecond: Long
|
||||
) {
|
||||
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)
|
||||
updateNotification()
|
||||
}
|
||||
@ -375,7 +350,7 @@ class ForegroundService : Service(){
|
||||
/**
|
||||
* 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 request = DownloadManager.Request(uri)
|
||||
.setAllowedNetworkTypes(
|
||||
@ -383,14 +358,14 @@ class ForegroundService : Service(){
|
||||
DownloadManager.Request.NETWORK_MOBILE
|
||||
)
|
||||
.setAllowedOverRoaming(false)
|
||||
.setTitle(track.name)
|
||||
.setTitle(track.title)
|
||||
.setDescription("Spotify Downloader Working Up here...")
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
|
||||
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
|
||||
))
|
||||
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
//Start Download
|
||||
val downloadID = downloadManager?.enqueue(request)
|
||||
val downloadID = downloadManager.enqueue(request)
|
||||
Log.i("DownloadManager", "Download Request Sent")
|
||||
|
||||
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
@ -412,7 +387,7 @@ class ForegroundService : Service(){
|
||||
/**
|
||||
*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)
|
||||
|
||||
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)
|
||||
mp3File = removeAllTags(mp3File)
|
||||
mp3File = setId3v1Tags(mp3File,track)
|
||||
@ -485,62 +460,40 @@ class ForegroundService : Service(){
|
||||
/**
|
||||
*Modifying Mp3 Tags with MetaData!
|
||||
**/
|
||||
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
|
||||
val id3v1Tag = ID3v1Tag()
|
||||
id3v1Tag.track = track.disc_number.toString()
|
||||
val artistsList = mutableListOf<String>()
|
||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
||||
id3v1Tag.artist = artistsList.joinToString()
|
||||
id3v1Tag.title = track.name
|
||||
id3v1Tag.album = track.album?.name
|
||||
id3v1Tag.year = track.album?.release_date
|
||||
id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
||||
private fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File {
|
||||
val id3v1Tag = ID3v1Tag().apply {
|
||||
artist = track.artists.joinToString(",")
|
||||
title = track.title
|
||||
album = track.albumName
|
||||
year = track.year
|
||||
comment = "Genres:${track.comment}"
|
||||
}
|
||||
mp3File.id3v1Tag = id3v1Tag
|
||||
return mp3File
|
||||
}
|
||||
private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File {
|
||||
val id3v2Tag = ID3v24Tag()
|
||||
id3v2Tag.track = track.disc_number.toString()
|
||||
val artistsList = mutableListOf<String>()
|
||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
||||
id3v2Tag.artist = artistsList.joinToString()
|
||||
id3v2Tag.title = track.name
|
||||
id3v2Tag.album = track.album?.name
|
||||
id3v2Tag.year = track.album?.release_date
|
||||
id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
||||
id3v2Tag.lyrics = "Gonna Implement Soon"
|
||||
val copyrights = mutableListOf<String>()
|
||||
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
||||
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)
|
||||
private fun setId3v2Tags(mp3file: Mp3File, track: TrackDetails): Mp3File {
|
||||
val id3v2Tag = ID3v24Tag().apply {
|
||||
artist = track.artists.joinToString(",")
|
||||
title = track.title
|
||||
album = track.albumName
|
||||
year = track.year
|
||||
comment = "Genres:${track.comment}"
|
||||
lyrics = "Gonna Implement Soon"
|
||||
url = track.trackUrl
|
||||
}
|
||||
val bytesArray = ByteArray(track.albumArt.length().toInt())
|
||||
try{
|
||||
val fis = FileInputStream(track.albumArt)
|
||||
fis.read(bytesArray) //read file into bytes[]
|
||||
fis.close()
|
||||
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
|
||||
return mp3file
|
||||
}
|
||||
|
||||
private fun removeAllTags(mp3file: Mp3File): Mp3File {
|
||||
if (mp3file.hasId3v1Tag()) {
|
||||
mp3file.removeId3v1Tag()
|
||||
@ -554,4 +507,141 @@ class ForegroundService : Service(){
|
||||
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:tools="http://schemas.android.com/tools">
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/main_youtube"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="25dp"
|
||||
@ -28,7 +28,7 @@
|
||||
tools:context=".ui.spotify.SpotifyFragment">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btn_download_all_spotify"
|
||||
android:id="@+id/btn_download_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:background="@drawable/btn_design"
|
||||
@ -39,11 +39,11 @@
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp"
|
||||
android:visibility="visible"
|
||||
app:layout_anchor="@+id/appbar_spotify"
|
||||
app:layout_anchor="@+id/appbar"
|
||||
app:layout_anchorGravity="bottom|center" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/downloading_fab_spotify"
|
||||
android:id="@+id/downloading_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:keepScreenOn="true"
|
||||
android:layout_height="wrap_content"
|
||||
@ -51,7 +51,7 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
app:borderWidth="0dp"
|
||||
app:layout_anchor="@+id/appbar_spotify"
|
||||
app:layout_anchor="@+id/appbar"
|
||||
app:layout_anchorGravity="bottom|center"
|
||||
app:maxImageSize="38dp"
|
||||
app:rippleColor="@color/colorPrimaryDark"
|
||||
@ -59,7 +59,7 @@
|
||||
app:tint="@null" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_spotify"
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="280dp">
|
||||
|
||||
@ -72,14 +72,14 @@
|
||||
app:toolbarId="@+id/toolbar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/TopLayout_spotify"
|
||||
android:id="@+id/topLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:foreground="@drawable/gradient"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/spotify_cover_image"
|
||||
android:id="@+id/cover_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="28dp"
|
||||
@ -89,13 +89,13 @@
|
||||
android:src="@drawable/spotify_download"
|
||||
android:visibility="visible"
|
||||
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_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/StatusBar_spotify"
|
||||
android:id="@+id/statusBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
@ -113,11 +113,11 @@
|
||||
android:textColor="@color/grey"
|
||||
android:textSize="16sp"
|
||||
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_constraintStart_toStartOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/title_view_spotify"
|
||||
android:id="@+id/title_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
@ -142,7 +142,7 @@
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/track_list_spotify"
|
||||
android:id="@+id/track_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="26dp"
|
||||
@ -152,7 +152,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar_spotify" />
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
|
@ -19,14 +19,14 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/main_youtube"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="25dp"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btn_download_all_youtube"
|
||||
android:id="@+id/btn_download_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:background="@drawable/btn_design"
|
||||
@ -38,17 +38,17 @@
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp"
|
||||
android:visibility="visible"
|
||||
app:layout_anchor="@+id/appbar_youtube"
|
||||
app:layout_anchor="@+id/appbar"
|
||||
app:layout_anchorGravity="bottom|center" />
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/downloading_fab_youtube"
|
||||
android:id="@+id/downloading_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
app:borderWidth="0dp"
|
||||
app:layout_anchor="@+id/appbar_youtube"
|
||||
app:layout_anchor="@+id/appbar"
|
||||
app:layout_anchorGravity="bottom|center"
|
||||
app:maxImageSize="38dp"
|
||||
app:rippleColor="@color/colorPrimaryDark"
|
||||
@ -56,7 +56,7 @@
|
||||
app:tint="@null" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_youtube"
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="230dp">
|
||||
|
||||
@ -69,14 +69,14 @@
|
||||
app:toolbarId="@+id/toolbar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/TopLayout_youtube"
|
||||
android:id="@+id/topLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="@drawable/gradient"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/youtube_cover_image"
|
||||
android:id="@+id/cover_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="23dp"
|
||||
@ -86,13 +86,13 @@
|
||||
android:src="@drawable/spotify_download"
|
||||
android:visibility="visible"
|
||||
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_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/StatusBar_youtube"
|
||||
android:id="@+id/statusBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
@ -109,11 +109,11 @@
|
||||
android:textColor="@color/grey"
|
||||
android:textSize="16sp"
|
||||
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_constraintStart_toStartOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/title_view_youtube"
|
||||
android:id="@+id/title_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
@ -136,7 +136,7 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/track_list_youtube"
|
||||
android:id="@+id/track_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="26dp"
|
||||
@ -146,7 +146,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar_youtube" />
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
|
Loading…
Reference in New Issue
Block a user