mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-25 02:14:32 +01:00
No More Web Scraping!,Speed Boosted& Less Crashes.
This commit is contained in:
parent
923e28617a
commit
099a103e98
@ -25,6 +25,7 @@
|
|||||||
<w>spotifydownloader</w>
|
<w>spotifydownloader</w>
|
||||||
<w>spotifyler</w>
|
<w>spotifyler</w>
|
||||||
<w>thru</w>
|
<w>thru</w>
|
||||||
|
<w>weyfdnx</w>
|
||||||
<w>youtu</w>
|
<w>youtu</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
|
@ -115,10 +115,13 @@ dependencies {
|
|||||||
implementation 'com.squareup.moshi:moshi:1.11.0'
|
implementation 'com.squareup.moshi:moshi:1.11.0'
|
||||||
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
||||||
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
|
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
|
||||||
|
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 'com.mpatric:mp3agic:0.9.1'
|
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||||
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
|
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
|
||||||
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.2'
|
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.3'
|
||||||
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
|
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
|
||||||
implementation 'com.github.javiersantos:AppUpdater:2.7'
|
implementation 'com.github.javiersantos:AppUpdater:2.7'
|
||||||
|
|
||||||
|
@ -69,9 +69,6 @@ class MainActivity : AppCompatActivity(){
|
|||||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||||
sharedPref = this.getPreferences(Context.MODE_PRIVATE)
|
sharedPref = this.getPreferences(Context.MODE_PRIVATE)
|
||||||
|
|
||||||
//starting Notification and Downloader Service!
|
|
||||||
SpotifyDownloadHelper.startService(this)
|
|
||||||
|
|
||||||
if(sharedViewModel.spotifyService.value == null){
|
if(sharedViewModel.spotifyService.value == null){
|
||||||
authenticateSpotify()
|
authenticateSpotify()
|
||||||
}else{
|
}else{
|
||||||
@ -86,6 +83,9 @@ class MainActivity : AppCompatActivity(){
|
|||||||
sharedViewModel.isConnected.value = isConnected
|
sharedViewModel.isConnected.value = isConnected
|
||||||
Log.i("Connection Status", isConnected.toString())
|
Log.i("Connection Status", isConnected.toString())
|
||||||
|
|
||||||
|
//starting Notification and Downloader Service!
|
||||||
|
SpotifyDownloadHelper.startService(this)
|
||||||
|
|
||||||
handleIntentFromExternalActivity()
|
handleIntentFromExternalActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,18 +17,13 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.downloadHelper
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Handler
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AlphaAnimation
|
import android.view.animation.AlphaAnimation
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
@ -37,25 +32,27 @@ import com.github.kiulian.downloader.model.quality.AudioQuality
|
|||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Track
|
||||||
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
|
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
|
||||||
|
import com.shabinder.spotiflyer.utils.YoutubeMusicApi
|
||||||
import com.shabinder.spotiflyer.utils.getEmojiByUnicode
|
import com.shabinder.spotiflyer.utils.getEmojiByUnicode
|
||||||
|
import com.shabinder.spotiflyer.utils.makeJsonBody
|
||||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object SpotifyDownloadHelper {
|
object SpotifyDownloadHelper {
|
||||||
var webView:WebView? = null
|
|
||||||
var context : Context? = null
|
var context : Context? = null
|
||||||
var statusBar:TextView? = null
|
var statusBar:TextView? = null
|
||||||
|
var youtubeMusicApi:YoutubeMusicApi? = null
|
||||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||||
var spotifyViewModel: SpotifyViewModel? = null
|
var spotifyViewModel: SpotifyViewModel? = null
|
||||||
private var isBrowserLoading = false
|
var total = 0
|
||||||
private var total = 0
|
var Processed = 0
|
||||||
private var Processed = 0
|
var notFound = 0
|
||||||
private var notFound = 0
|
|
||||||
private var listProcessed:Boolean = false
|
|
||||||
var youtubeList = mutableListOf<YoutubeRequest>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function To Download All Tracks Available in a List
|
* Function To Download All Tracks Available in a List
|
||||||
@ -70,16 +67,9 @@ object SpotifyDownloadHelper {
|
|||||||
if(it.downloaded == "Downloaded"){//Download Already Present!!
|
if(it.downloaded == "Downloaded"){//Download Already Present!!
|
||||||
Processed++
|
Processed++
|
||||||
}else{
|
}else{
|
||||||
if(isBrowserLoading){//WebView Busy!!
|
val artistsList = mutableListOf<String>()
|
||||||
if (listProcessed){//Previous List request progress check
|
it.artists?.forEach { artist -> artistsList.add(artist!!.name!!) }
|
||||||
getYTLink(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)
|
searchYTMusic(type,subFolder,ytDownloader,"${it.name} - ${artistsList.joinToString(",")}", it)
|
||||||
listProcessed = false//Notifying A list Processing Started
|
|
||||||
}else{//Adding Requests to a Queue
|
|
||||||
youtubeList.add(YoutubeRequest(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it))
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
getYTLink(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updateStatusBar()
|
updateStatusBar()
|
||||||
}
|
}
|
||||||
@ -88,66 +78,32 @@ object SpotifyDownloadHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun searchYTMusic(type:String,
|
||||||
//TODO CleanUp here and there!!
|
subFolder:String?,
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
ytDownloader: YoutubeDownloader?,
|
||||||
suspend fun getYTLink(type:String,
|
searchQuery: String,
|
||||||
subFolder:String?,
|
track: Track){
|
||||||
ytDownloader: YoutubeDownloader?,
|
val jsonBody = makeJsonBody(searchQuery.trim())
|
||||||
searchQuery: String,
|
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
|
||||||
track: Track){
|
object : Callback<String>{
|
||||||
isBrowserLoading = true // Notify Web View Started Loading
|
override fun onResponse(call: Call<String>, response: Response<String>) {
|
||||||
val searchText = searchQuery.replace("\\s".toRegex(), "+")
|
spotifyViewModel?.uiScope?.launch {
|
||||||
val url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=$searchText"
|
Log.i("YT API BODY",response.body().toString())
|
||||||
Log.i("DH YT LINK ",url)
|
Log.i("YT Search Query",searchQuery)
|
||||||
applyWebViewSettings(webView!!)
|
getYTLink(type,subFolder,ytDownloader,response.body().toString(),track)
|
||||||
withContext(Dispatchers.Main){
|
|
||||||
webView!!.loadUrl(url)
|
|
||||||
webView!!.webViewClient = object : WebViewClient() {
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
|
||||||
super.onPageFinished(view, url)
|
|
||||||
view?.evaluateJavascript(
|
|
||||||
"document.getElementsByClassName(\"yt-simple-endpoint style-scope ytd-video-renderer\")[0].href"
|
|
||||||
) { value ->
|
|
||||||
Log.i("YT-id Link", value.toString().replace("\"", ""))
|
|
||||||
val id = value!!.substringAfterLast("=", "error").replace("\"", "")
|
|
||||||
Log.i("YT-ID", id)
|
|
||||||
if (id != "error") {//Link extracting error
|
|
||||||
Processed++
|
|
||||||
downloadFile(subFolder, type, track, ytDownloader, id)
|
|
||||||
}else notFound++
|
|
||||||
updateStatusBar()
|
|
||||||
if (youtubeList.isNotEmpty()) {
|
|
||||||
val request = youtubeList[0]
|
|
||||||
spotifyViewModel!!.uiScope.launch {
|
|
||||||
getYTLink(
|
|
||||||
request.type,
|
|
||||||
request.subFolder,
|
|
||||||
request.ytDownloader,
|
|
||||||
request.searchQuery,
|
|
||||||
request.track
|
|
||||||
)
|
|
||||||
}
|
|
||||||
youtubeList.remove(request)
|
|
||||||
if (youtubeList.size == 0) {//list processing completed , webView is free again!
|
|
||||||
isBrowserLoading = false
|
|
||||||
listProcessed = true
|
|
||||||
}
|
|
||||||
} else {//YT List Empty....Maybe it was one Single Download
|
|
||||||
Handler().postDelayed({//Delay of 1.5 sec
|
|
||||||
if (youtubeList.isEmpty()) {//Lets Make It sure , There are No more Downloads In Queue.....
|
|
||||||
isBrowserLoading = false
|
|
||||||
listProcessed = true
|
|
||||||
}
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<String>, t: Throwable) {
|
||||||
|
Log.i("YT API Fail",t.message.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatusBar() {
|
|
||||||
|
fun updateStatusBar() {
|
||||||
statusBar!!.visibility = View.VISIBLE
|
statusBar!!.visibility = View.VISIBLE
|
||||||
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $Processed ${getEmojiByUnicode(0x274C)}: $notFound"
|
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $Processed ${getEmojiByUnicode(0x274C)}: $notFound"
|
||||||
}
|
}
|
||||||
@ -158,7 +114,6 @@ object SpotifyDownloadHelper {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val video = ytDownloader?.getVideo(id)
|
val video = ytDownloader?.getVideo(id)
|
||||||
val detail = video?.details()
|
|
||||||
val format: Format? = try {
|
val format: Format? = try {
|
||||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
@ -191,18 +146,21 @@ object SpotifyDownloadHelper {
|
|||||||
)
|
)
|
||||||
Log.i("DH", outputFile)
|
Log.i("DH", outputFile)
|
||||||
startService(context!!, downloadObject)
|
startService(context!!, downloadObject)
|
||||||
|
Processed++
|
||||||
|
spotifyViewModel?.uiScope?.launch(Dispatchers.Main) {
|
||||||
|
updateStatusBar()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
||||||
Log.i("DH", "Error- Maybe Network")
|
Log.i("DH", e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun startService(context:Context,obj:DownloadObject? = null ) {
|
fun startService(context:Context,obj:DownloadObject? = null ) {
|
||||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||||
serviceIntent.putExtra("object",obj)
|
obj?.let { serviceIntent.putExtra("object",it) }
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
ContextCompat.startForegroundService(context, serviceIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,35 +213,4 @@ object SpotifyDownloadHelper {
|
|||||||
anim.repeatCount = Animation.INFINITE
|
anim.repeatCount = Animation.INFINITE
|
||||||
statusBar?.animation = anim
|
statusBar?.animation = anim
|
||||||
}
|
}
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
fun applyWebViewSettings(webView: WebView) {
|
|
||||||
val desktopUserAgent =
|
|
||||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0"
|
|
||||||
val mobileUserAgent =
|
|
||||||
"Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"
|
|
||||||
|
|
||||||
//Choose Mobile/Desktop client.
|
|
||||||
webView.settings.userAgentString = desktopUserAgent
|
|
||||||
webView.settings.loadWithOverviewMode = true
|
|
||||||
webView.settings.builtInZoomControls = true
|
|
||||||
webView.settings.setSupportZoom(true)
|
|
||||||
webView.isScrollbarFadingEnabled = false
|
|
||||||
webView.scrollBarStyle = WebView.SCROLLBARS_OUTSIDE_OVERLAY
|
|
||||||
webView.settings.displayZoomControls = false
|
|
||||||
webView.settings.useWideViewPort = true
|
|
||||||
webView.settings.javaScriptEnabled = true
|
|
||||||
webView.settings.loadsImagesAutomatically = false
|
|
||||||
webView.settings.blockNetworkImage = true
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
webView.settings.safeBrowsingEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
data class YoutubeRequest(
|
|
||||||
val type:String,
|
|
||||||
val subFolder:String?,
|
|
||||||
val ytDownloader: YoutubeDownloader?,
|
|
||||||
val searchQuery: String,
|
|
||||||
val track: Track,
|
|
||||||
val index: Int? = null
|
|
||||||
)
|
|
@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
* 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.downloadHelper
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Thanks and credits To https://github.com/spotDL/spotify-downloader
|
||||||
|
* */
|
||||||
|
fun getYTLink(type:String,
|
||||||
|
subFolder:String?,
|
||||||
|
ytDownloader: YoutubeDownloader?,
|
||||||
|
response: String,
|
||||||
|
track: Track
|
||||||
|
){
|
||||||
|
//TODO Download File
|
||||||
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
|
val parser: Parser = Parser.default()
|
||||||
|
val stringBuilder: StringBuilder = StringBuilder(response)
|
||||||
|
val responseObj: JsonObject = parser.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
|
||||||
|
*The 'itemSectionRenderer' field is for user notices (stuff like - 'showing
|
||||||
|
*results for xyz, search for abc instead') we have no use for them, the for
|
||||||
|
*loop below if throw a keyError if we don't ignore them
|
||||||
|
*/
|
||||||
|
if(cBlock.containsKey("itemSectionRenderer")){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for(contents in cBlock.obj("musicShelfRenderer")?.array<JsonObject>("contents") ?: listOf()){
|
||||||
|
/**
|
||||||
|
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
||||||
|
* I have no clue what they are and why there even exist
|
||||||
|
*
|
||||||
|
if(!contents.containsKey("overlay")){
|
||||||
|
println(contents)
|
||||||
|
continue
|
||||||
|
TODO check and correct
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val result = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.array<JsonObject>("flexColumns")
|
||||||
|
|
||||||
|
//Add the linkBlock
|
||||||
|
val linkBlock = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.obj("overlay")
|
||||||
|
?.obj("musicItemThumbnailOverlayRenderer")
|
||||||
|
?.obj("content")
|
||||||
|
?.obj("musicPlayButtonRenderer")
|
||||||
|
?.obj("playNavigationEndpoint")
|
||||||
|
|
||||||
|
// detailsBlock is always a list, so we just append the linkBlock to it
|
||||||
|
// instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
|
||||||
|
linkBlock?.let { result?.add(it) }
|
||||||
|
result?.let { resultBlocks.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We only need results that are Songs or Videos, so we filter out the rest, since
|
||||||
|
! Songs and Videos are supplied with different details, extracting all details from
|
||||||
|
! both is just carrying on redundant data, so we also have to selectively extract
|
||||||
|
! relevant details. What you need to know to understand how we do that here:
|
||||||
|
!
|
||||||
|
! Songs details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Song)
|
||||||
|
! 2 - Artist
|
||||||
|
! 3 - Album
|
||||||
|
! 4 - Duration (mm:ss)
|
||||||
|
!
|
||||||
|
! Video details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Video)
|
||||||
|
! 2 - Channel
|
||||||
|
! 3 - Viewers
|
||||||
|
! 4 - Duration (hh:mm:ss)
|
||||||
|
!
|
||||||
|
! We blindly gather all the details we get our hands on, then
|
||||||
|
! cherrypick the details we need based on their index numbers,
|
||||||
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
|
*/
|
||||||
|
|
||||||
|
val simplifiedResults = mutableListOf<JsonObject>()
|
||||||
|
|
||||||
|
for(result in resultBlocks){
|
||||||
|
|
||||||
|
// Blindly gather available details
|
||||||
|
val availableDetails = mutableListOf<String>()
|
||||||
|
|
||||||
|
/*
|
||||||
|
Filter Out dummies here itself
|
||||||
|
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
||||||
|
! sub-block, if not its a dummy, why does the YTM response contain dummies?
|
||||||
|
! I have no clue. We skip these.
|
||||||
|
|
||||||
|
! Remember that we appended the linkBlock to result, treating that like the
|
||||||
|
! 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)){
|
||||||
|
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
||||||
|
|
||||||
|
// if not a dummy, collect All Variables
|
||||||
|
detail.obj("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
?.obj("text")
|
||||||
|
?.array<JsonObject>("runs")?.get(0)?.get("text")?.let {
|
||||||
|
availableDetails.add(
|
||||||
|
it.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
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
|
||||||
|
/*
|
||||||
|
! grab Video ID
|
||||||
|
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
||||||
|
! so hardcoding the dict keys for data look up is an ardours process, since
|
||||||
|
! the sub-block pattern is fixed even though the key isn't, we just
|
||||||
|
! reference the dict keys by index
|
||||||
|
*/
|
||||||
|
|
||||||
|
val videoId:String = result.last().obj("watchEndpoint")?.get("videoId") as String
|
||||||
|
val ytTrack = YoutubeTrack(
|
||||||
|
name = availableDetails[0],
|
||||||
|
type = availableDetails[1],
|
||||||
|
artist = availableDetails[2],
|
||||||
|
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()
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class YoutubeTrack(
|
||||||
|
var name: String? = null,
|
||||||
|
var type: String? = null, // Song / Video
|
||||||
|
var artist: String? = null,
|
||||||
|
var videoId: String? = null
|
||||||
|
):Parcelable
|
@ -29,7 +29,6 @@ import android.util.Log
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.WebView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
@ -50,6 +49,7 @@ import com.shabinder.spotiflyer.databinding.SpotifyFragmentBinding
|
|||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.Track
|
||||||
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
|
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
|
||||||
|
import com.shabinder.spotiflyer.utils.YoutubeMusicApi
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.utils.bindImage
|
||||||
import com.shabinder.spotiflyer.utils.copyTo
|
import com.shabinder.spotiflyer.utils.copyTo
|
||||||
import com.shabinder.spotiflyer.utils.rotateAnim
|
import com.shabinder.spotiflyer.utils.rotateAnim
|
||||||
@ -70,7 +70,7 @@ class SpotifyFragment : Fragment() {
|
|||||||
private lateinit var sharedViewModel: SharedViewModel
|
private lateinit var sharedViewModel: SharedViewModel
|
||||||
private lateinit var adapterSpotify:SpotifyTrackListAdapter
|
private lateinit var adapterSpotify:SpotifyTrackListAdapter
|
||||||
@Inject lateinit var ytDownloader:YoutubeDownloader
|
@Inject lateinit var ytDownloader:YoutubeDownloader
|
||||||
private var webView: WebView? = null
|
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
|
||||||
private var intentFilter:IntentFilter? = null
|
private var intentFilter:IntentFilter? = null
|
||||||
private var updateUIReceiver: BroadcastReceiver? = null
|
private var updateUIReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
@ -231,14 +231,13 @@ class SpotifyFragment : Fragment() {
|
|||||||
* Basic Initialization
|
* Basic Initialization
|
||||||
**/
|
**/
|
||||||
private fun initializeAll() {
|
private fun initializeAll() {
|
||||||
webView = binding.webViewSpotify
|
|
||||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||||
spotifyViewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
|
spotifyViewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
|
||||||
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
|
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
|
||||||
spotifyViewModel.spotifyService = it
|
spotifyViewModel.spotifyService = it
|
||||||
})
|
})
|
||||||
SpotifyDownloadHelper.webView = binding.webViewSpotify
|
|
||||||
SpotifyDownloadHelper.context = requireContext()
|
SpotifyDownloadHelper.context = requireContext()
|
||||||
|
SpotifyDownloadHelper.youtubeMusicApi = youtubeMusicApi
|
||||||
SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel
|
SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel
|
||||||
SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify
|
SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify
|
||||||
binding.trackListSpotify.adapter = adapterSpotify
|
binding.trackListSpotify.adapter = adapterSpotify
|
||||||
|
@ -35,7 +35,9 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@InstallIn(ApplicationComponent::class)
|
@InstallIn(ApplicationComponent::class)
|
||||||
@ -97,4 +99,17 @@ object Provider {
|
|||||||
return retrofit.create(SpotifyServiceTokenRequest::class.java)
|
return retrofit.create(SpotifyServiceTokenRequest::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun getYoutubeMusicApi():YoutubeMusicApi{
|
||||||
|
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl("https://music.youtube.com/youtubei/v1/")
|
||||||
|
.addConverterFactory(ScalarsConverterFactory.create())
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return retrofit.create(YoutubeMusicApi::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.beust.klaxon.JsonObject
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
|
||||||
|
const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
||||||
|
/*val body = """{
|
||||||
|
"context": {
|
||||||
|
"client": {
|
||||||
|
"clientName": "WEB_REMIX",
|
||||||
|
"clientVersion": "0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "songSearchQuery"
|
||||||
|
}"""*/
|
||||||
|
interface YoutubeMusicApi {
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/json", "Referer: https://music.youtube.com/search")
|
||||||
|
@POST("search?alt=json&key=$apiKey")
|
||||||
|
fun getYoutubeMusicResponse(@Body text: JsonObject): Call<String>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeJsonBody(query: String):JsonObject{
|
||||||
|
val client = JsonObject()
|
||||||
|
client["clientName"] = "WEB_REMIX"
|
||||||
|
client["clientVersion"] = "0.1"
|
||||||
|
|
||||||
|
val context = JsonObject()
|
||||||
|
context["client"] = client
|
||||||
|
|
||||||
|
val mainObject = JsonObject()
|
||||||
|
mainObject["context"] = context
|
||||||
|
mainObject["query"] = query
|
||||||
|
|
||||||
|
return mainObject
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.worker
|
package com.shabinder.spotiflyer.worker
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@ -144,33 +145,29 @@ class ForegroundService : Service(){
|
|||||||
return channelId
|
return channelId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WakelockTimeout")
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
// Send a notification that service is started
|
// Send a notification that service is started
|
||||||
Log.i(tag,"Service Started.")
|
Log.i(tag,"Service Started.")
|
||||||
startForeground()
|
startForeground()
|
||||||
//do heavy work on a background thread
|
val obj:DownloadObject? = intent.getParcelableExtra("object") ?: intent.extras?.getParcelable("object")
|
||||||
//val list = intent.getSerializableExtra("list") as List<Any?>
|
|
||||||
// val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
|
|
||||||
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
|
||||||
val obj = intent.getParcelableExtra<DownloadObject>("object") ?: intent.extras?.getParcelable<DownloadObject>("object")
|
|
||||||
obj?.let {
|
obj?.let {
|
||||||
total ++
|
total ++
|
||||||
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
|
||||||
updateNotification()
|
updateNotification()
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val request= Request(obj.url, obj.outputDir)
|
val request= Request(obj.url, obj.outputDir)
|
||||||
request.priority = Priority.NORMAL
|
request.priority = Priority.NORMAL
|
||||||
request.networkType = NetworkType.ALL
|
request.networkType = NetworkType.ALL
|
||||||
|
|
||||||
fetch!!.enqueue(request,
|
fetch!!.enqueue(request,
|
||||||
{
|
{
|
||||||
obj.track?.let { it1 -> requestMap.put(it, it1) }
|
obj.track?.let { it1 -> requestMap.put(it, it1) }
|
||||||
downloadList.remove(obj)
|
downloadList.remove(obj)
|
||||||
Log.i(tag, "Enqueuing Download")
|
Log.i(tag, "Enqueuing Download")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,12 +313,6 @@ class ForegroundService : Service(){
|
|||||||
messageList[messageList.indexOf(message)] = ""
|
messageList[messageList.indexOf(message)] = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Notify Download Completed
|
|
||||||
val intent = Intent()
|
|
||||||
.setAction("track_download_completed")
|
|
||||||
.putExtra("track",track)
|
|
||||||
this@ForegroundService.sendBroadcast(intent)
|
|
||||||
|
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try{
|
try{
|
||||||
@ -457,11 +448,17 @@ class ForegroundService : Service(){
|
|||||||
newFile.renameTo(file)
|
newFile.renameTo(file)
|
||||||
converted++
|
converted++
|
||||||
updateNotification()
|
updateNotification()
|
||||||
|
|
||||||
|
//Notify Download Completed
|
||||||
|
val intent = Intent()
|
||||||
|
.setAction("track_download_completed")
|
||||||
|
.putExtra("track",track)
|
||||||
|
this@ForegroundService.sendBroadcast(intent)
|
||||||
|
|
||||||
//All tasks completed (REST IN PEACE)
|
//All tasks completed (REST IN PEACE)
|
||||||
if(converted == total){
|
if(converted == total){
|
||||||
onDestroy()
|
onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -154,14 +154,5 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/appbar_spotify" />
|
app:layout_constraintTop_toBottomOf="@id/appbar_spotify" />
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:id="@+id/webView_spotify"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="300dp"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_anchorGravity="bottom"/>
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</layout>
|
</layout>
|
||||||
|
Loading…
Reference in New Issue
Block a user