YT Image URL Schema Change from HI_RES->HQ-Default

This commit is contained in:
Shabinder 2020-11-08 13:10:46 +05:30
parent c0e3a35898
commit a5793cc72c
8 changed files with 205 additions and 86 deletions

View File

@ -6,6 +6,7 @@
<w>emoji</w> <w>emoji</w>
<w>ffmpeg</w> <w>ffmpeg</w>
<w>flyer</w> <w>flyer</w>
<w>hqdefault</w>
<w>insta</w> <w>insta</w>
<w>instagram</w> <w>instagram</w>
<w>jetbrains</w> <w>jetbrains</w>

View File

@ -48,8 +48,8 @@ object YTDownloadHelper {
val downloadObject = DownloadObject( val downloadObject = DownloadObject(
trackDetails = it, trackDetails = it,
ytVideoId = "https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/") ytVideoId = it.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast(".")}/maxresdefault.jpg", .substringBeforeLast("."),
outputFile = outputFile outputFile = outputFile
) )

View File

@ -44,7 +44,7 @@ class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.Vi
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
adapterScope.launch { adapterScope.launch {
bindImage(holder.binding.coverUrl,item.coverUrl) bindImage(holder.binding.coverUrl,item.coverUrl,null)
} }
holder.binding.itemName.text = item.name holder.binding.itemName.text = item.name
holder.binding.totalItems.text = "Tracks: ${item.totalFiles}" holder.binding.totalItems.text = "Tracks: ${item.totalFiles}"

View File

@ -20,19 +20,22 @@ package com.shabinder.spotiflyer.recyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.TrackListItemBinding import com.shabinder.spotiflyer.databinding.TrackListItemBinding
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.Source import com.shabinder.spotiflyer.models.Source
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.ui.youtube.YoutubeViewModel
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.bindImage import com.shabinder.spotiflyer.utils.bindImage
import kotlinx.coroutines.CoroutineScope import com.shabinder.spotiflyer.utils.rotateAnim
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class YoutubeTrackListAdapter: ListAdapter<TrackDetails,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) { class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): ListAdapter<TrackDetails,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
private val adapterScope = CoroutineScope(Dispatchers.Default)
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
@ -48,24 +51,50 @@ class YoutubeTrackListAdapter: ListAdapter<TrackDetails,SpotifyTrackListAdapter.
val item = getItem(position) val item = getItem(position)
if(itemCount == 1){ if(itemCount == 1){
holder.binding.imageUrl.visibility = View.GONE}else{ holder.binding.imageUrl.visibility = View.GONE}else{
adapterScope.launch { youtubeViewModel.uiScope.launch {
bindImage(holder.binding.imageUrl, bindImage(holder.binding.imageUrl,
"https://i.ytimg.com/vi/${item.albumArt.absolutePath.substringAfterLast("/") "https://i.ytimg.com/vi/${item.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast(".")}/maxresdefault.jpg" .substringBeforeLast(".")}/hqdefault.jpg"
, ,
Source.YouTube Source.YouTube
) )
} }
} }
when (item.downloaded) {
DownloadStatus.Downloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
holder.binding.btnDownload.clearAnimation()
}
DownloadStatus.Downloading -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
rotateAnim(holder.binding.btnDownload)
}
DownloadStatus.NotDownloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
holder.binding.btnDownload.clearAnimation()
holder.binding.btnDownload.setOnClickListener{
Toast.makeText(Provider.activity,"Processing!", Toast.LENGTH_SHORT).show()
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
rotateAnim(it)
item.downloaded = DownloadStatus.Downloading
youtubeViewModel.uiScope.launch {
val itemList = mutableListOf<TrackDetails>()
itemList.add(item)
YTDownloadHelper.downloadYTTracks(
youtubeViewModel.folderType,
youtubeViewModel.subFolder,
itemList
)
}
notifyItemChanged(position)//start showing anim!
}
}
}
holder.binding.trackName.text = "${if(item.title.length > 17){"${item.title.subSequence(0,16)}..."}else{item.title}}" 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.artist.text = "${item.artists.get(0)}..."
holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec" 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)
}
}
} }
} }
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){ class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){

View File

@ -17,7 +17,12 @@
package com.shabinder.spotiflyer.ui.youtube package com.shabinder.spotiflyer.ui.youtube
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
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
@ -52,6 +57,8 @@ class YoutubeFragment : Fragment() {
private lateinit var adapter : YoutubeTrackListAdapter private lateinit var adapter : YoutubeTrackListAdapter
private val sampleDomain1 = "youtube.com" private val sampleDomain1 = "youtube.com"
private val sampleDomain2 = "youtu.be" private val sampleDomain2 = "youtu.be"
private var intentFilter: IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@ -60,10 +67,11 @@ class YoutubeFragment : Fragment() {
binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false) binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false)
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java) youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
adapter = YoutubeTrackListAdapter() adapter = YoutubeTrackListAdapter(youtubeViewModel)
binding.trackList.adapter = adapter binding.trackList.adapter = adapter
initializeLiveDataObservers() initializeLiveDataObservers()
initializeBroadcast()
val args = YoutubeFragmentArgs.fromBundle(requireArguments()) val args = YoutubeFragmentArgs.fromBundle(requireArguments())
val link = args.link val link = args.link
@ -89,6 +97,10 @@ class YoutubeFragment : Fragment() {
youtubeViewModel.getYTTrack(searchId,ytDownloader) youtubeViewModel.getYTTrack(searchId,ytDownloader)
}else{showToast("Your Youtube Link is not of a Video!!")} }else{showToast("Your Youtube Link is not of a Video!!")}
} }
/*
* Download All Tracks
* */
binding.btnDownloadAll.setOnClickListener { binding.btnDownloadAll.setOnClickListener {
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.visibility = View.VISIBLE binding.downloadingFab.visibility = View.VISIBLE
@ -105,7 +117,7 @@ class YoutubeFragment : Fragment() {
sharedViewModel.uiScope.launch(Dispatchers.Default){ sharedViewModel.uiScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>() val urlList = arrayListOf<String>()
youtubeViewModel.ytTrackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/") youtubeViewModel.ytTrackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast(".")}/maxresdefault.jpg")} .substringBeforeLast(".")}/hqdefault.jpg")}
//Appending Source //Appending Source
urlList.add("youtube") urlList.add("youtube")
loadAllImages( loadAllImages(
@ -122,7 +134,56 @@ class YoutubeFragment : Fragment() {
} }
} }
} }
override fun onResume() {
super.onResume()
initializeBroadcast()
}
private fun initializeBroadcast() {
intentFilter = IntentFilter()
intentFilter?.addAction("track_download_completed")
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let {
val position: Int = youtubeViewModel.ytTrackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
Log.i("Track","Download Completed Intent :$position")
if(position != -1) {
val track = youtubeViewModel.ytTrackList.value?.get(position)
track?.let{
it.downloaded = DownloadStatus.Downloaded
youtubeViewModel.ytTrackList.value?.set(position, it)
adapter.notifyItemChanged(position)
checkIfAllDownloaded()
}
}
}
}
}
}
requireActivity().registerReceiver(updateUIReceiver, intentFilter)
}
override fun onPause() {
super.onPause()
requireActivity().unregisterReceiver(updateUIReceiver)
}
private fun checkIfAllDownloaded() {
if(!youtubeViewModel.ytTrackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
//All Tracks Downloaded
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.apply{
setImageResource(R.drawable.ic_tick)
visibility = View.VISIBLE
clearAnimation()
keepScreenOn = false
}
}
}
private fun initializeLiveDataObservers() { private fun initializeLiveDataObservers() {
/** /**
* CoverUrl Binding Observer! * CoverUrl Binding Observer!

View File

@ -32,6 +32,7 @@ import com.shabinder.spotiflyer.models.Source
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.Provider import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.Provider.defaultDir import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.showToast
import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.removeIllegalChars import com.shabinder.spotiflyer.utils.removeIllegalChars
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -41,10 +42,11 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
/* /*
* YT Album Art Schema * YT Album Art Schema
* Normal Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" * HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
* */ * */
val ytTrackList = MutableLiveData<List<TrackDetails>>() val ytTrackList = MutableLiveData<MutableList<TrackDetails>>()
val format = MutableLiveData<Format>() val format = MutableLiveData<Format>()
private val loading = "Loading" private val loading = "Loading"
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" } var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
@ -56,6 +58,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){ fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){
try{
uiScope.launch(Dispatchers.IO) { uiScope.launch(Dispatchers.IO) {
Log.i("YT Playlist",searchId) Log.i("YT Playlist",searchId)
val playlist = ytDownloader.getPlaylist(searchId) val playlist = ytDownloader.getPlaylist(searchId)
@ -63,7 +66,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
val name = playlistDetails.title() val name = playlistDetails.title()
subFolder = removeIllegalChars(name).toString() subFolder = removeIllegalChars(name).toString()
val videos = playlist.videos() val videos = playlist.videos()
coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg") coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg")
title.postValue( title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
) )
@ -74,39 +77,54 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
durationSec = it.lengthSeconds(), durationSec = it.lengthSeconds(),
albumArt = File( albumArt = File(
Environment.getExternalStorageDirectory(), Environment.getExternalStorageDirectory(),
defaultDir +".Images/" + it.videoId() + ".jpeg" defaultDir + ".Images/" + it.videoId() + ".jpeg"
), ),
source = Source.YouTube, source = Source.YouTube,
downloaded = if(File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()) downloaded = if (File(
DownloadStatus.Downloaded finalOutputDir(
else DownloadStatus.NotDownloaded itemName = it.title(),
type = folderType,
subFolder = subFolder
) )
}) ).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
}
)
}.toMutableList())
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
databaseDAO.insert(DownloadRecord( databaseDAO.insert(DownloadRecord(
type = "PlayList", type = "PlayList",
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
link = "https://www.youtube.com/playlist?list=$searchId", link = "https://www.youtube.com/playlist?list=$searchId",
coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg", coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg",
totalFiles = videos.size, totalFiles = videos.size,
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder), directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists() downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()
)) ))
} }
} }
}catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){
showToast("An Error Occurred While Processing!")
}
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) { fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) {
try{
uiScope.launch(Dispatchers.IO) { uiScope.launch(Dispatchers.IO) {
Log.i("YT Video",searchId) Log.i("YT Video",searchId)
val video = ytDownloader.getVideo(searchId) val video = ytDownloader.getVideo(searchId)
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg") coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg")
val detail = video?.details() val detail = video?.details()
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: "" val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: ""
Log.i("YT View Model",detail.toString()) Log.i("YT View Model",detail.toString())
ytTrackList.postValue(listOf(TrackDetails( ytTrackList.postValue(
listOf(TrackDetails(
title = name, title = name,
artists = listOf(detail?.author().toString()), artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds()?:0, durationSec = detail?.lengthSeconds()?:0,
@ -115,7 +133,8 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
Provider.defaultDir +".Images/" + searchId + ".jpeg" Provider.defaultDir +".Images/" + searchId + ".jpeg"
), ),
source = Source.YouTube source = Source.YouTube
))) )).toMutableList()
)
title.postValue( title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
) )
@ -125,13 +144,16 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
type = "Track", type = "Track",
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
link = "https://www.youtube.com/watch?v=$searchId", link = "https://www.youtube.com/watch?v=$searchId",
coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg", coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1, totalFiles = 1,
downloaded = false, downloaded = false,
directory = finalOutputDir(type = "YT_Downloads") directory = finalOutputDir(type = "YT_Downloads")
)) ))
} }
} }
} catch (e:com.github.kiulian.downloader.YoutubeException){
showToast("An Error Occurred While Processing!")
}
} }
} }

View File

@ -19,6 +19,7 @@ package com.shabinder.spotiflyer.utils
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import android.widget.Toast
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.App import com.shabinder.spotiflyer.App
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
@ -117,4 +118,7 @@ object Provider {
return retrofit.create(YoutubeMusicApi::class.java) return retrofit.create(YoutubeMusicApi::class.java)
} }
fun showToast(string: String,long:Boolean=false){
Toast.makeText(activity,string,if(long)Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show()
}
} }

View File

@ -28,7 +28,6 @@ import android.view.animation.RotateAnimation
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.engine.GlideException
@ -77,8 +76,7 @@ fun rotateAnim(view: View){
view.animation = rotate view.animation = rotate
} }
@BindingAdapter("imageUrl") fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) {
fun bindImage(imgView: ImageView, imgUrl: String?,source: Source) {
imgUrl?.let { imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide Glide
@ -122,6 +120,10 @@ fun bindImage(imgView: ImageView, imgUrl: String?,source: Source) {
defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg" defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg"
) )
} }
else -> File(
Environment.getExternalStorageDirectory(),
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
)
} }
// the File to save , append increasing numeric counter to prevent files from getting overwritten. // the File to save , append increasing numeric counter to prevent files from getting overwritten.
resource?.copyTo(file) resource?.copyTo(file)