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>ffmpeg</w>
<w>flyer</w>
<w>hqdefault</w>
<w>insta</w>
<w>instagram</w>
<w>jetbrains</w>

View File

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

View File

@ -44,7 +44,7 @@ class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.Vi
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
adapterScope.launch {
bindImage(holder.binding.coverUrl,item.coverUrl)
bindImage(holder.binding.coverUrl,item.coverUrl,null)
}
holder.binding.itemName.text = item.name
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.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.shabinder.spotiflyer.R
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.TrackDetails
import com.shabinder.spotiflyer.ui.youtube.YoutubeViewModel
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.bindImage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import com.shabinder.spotiflyer.utils.rotateAnim
import kotlinx.coroutines.launch
class YoutubeTrackListAdapter: ListAdapter<TrackDetails,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
private val adapterScope = CoroutineScope(Dispatchers.Default)
class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): ListAdapter<TrackDetails,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
@ -48,24 +51,50 @@ class YoutubeTrackListAdapter: ListAdapter<TrackDetails,SpotifyTrackListAdapter.
val item = getItem(position)
if(itemCount == 1){
holder.binding.imageUrl.visibility = View.GONE}else{
adapterScope.launch {
youtubeViewModel.uiScope.launch {
bindImage(holder.binding.imageUrl,
"https://i.ytimg.com/vi/${item.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast(".")}/maxresdefault.jpg"
.substringBeforeLast(".")}/hqdefault.jpg"
,
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.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)
}
}
}
}
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){

View File

@ -17,7 +17,12 @@
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.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -52,6 +57,8 @@ class YoutubeFragment : Fragment() {
private lateinit var adapter : YoutubeTrackListAdapter
private val sampleDomain1 = "youtube.com"
private val sampleDomain2 = "youtu.be"
private var intentFilter: IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@ -60,10 +67,11 @@ class YoutubeFragment : Fragment() {
binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false)
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
adapter = YoutubeTrackListAdapter()
adapter = YoutubeTrackListAdapter(youtubeViewModel)
binding.trackList.adapter = adapter
initializeLiveDataObservers()
initializeBroadcast()
val args = YoutubeFragmentArgs.fromBundle(requireArguments())
val link = args.link
@ -89,6 +97,10 @@ class YoutubeFragment : Fragment() {
youtubeViewModel.getYTTrack(searchId,ytDownloader)
}else{showToast("Your Youtube Link is not of a Video!!")}
}
/*
* Download All Tracks
* */
binding.btnDownloadAll.setOnClickListener {
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.visibility = View.VISIBLE
@ -105,7 +117,7 @@ class YoutubeFragment : Fragment() {
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")}
.substringBeforeLast(".")}/hqdefault.jpg")}
//Appending Source
urlList.add("youtube")
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() {
/**
* 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.utils.Provider
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.removeIllegalChars
import kotlinx.coroutines.*
@ -41,10 +42,11 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
/*
* 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>()
private val loading = "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){
try{
uiScope.launch(Dispatchers.IO) {
Log.i("YT Playlist",searchId)
val playlist = ytDownloader.getPlaylist(searchId)
@ -63,7 +66,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
val name = playlistDetails.title()
subFolder = removeIllegalChars(name).toString()
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(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
)
@ -77,36 +80,51 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
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
downloaded = if (File(
finalOutputDir(
itemName = it.title(),
type = folderType,
subFolder = subFolder
)
})
).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
}
)
}.toMutableList())
withContext(Dispatchers.IO){
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",
coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg",
totalFiles = videos.size,
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
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")
fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) {
try{
uiScope.launch(Dispatchers.IO) {
Log.i("YT Video",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 name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: ""
Log.i("YT View Model",detail.toString())
ytTrackList.postValue(listOf(TrackDetails(
ytTrackList.postValue(
listOf(TrackDetails(
title = name,
artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds()?:0,
@ -115,7 +133,8 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
Provider.defaultDir +".Images/" + searchId + ".jpeg"
),
source = Source.YouTube
)))
)).toMutableList()
)
title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
)
@ -125,13 +144,16 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
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",
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1,
downloaded = false,
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.os.Environment
import android.widget.Toast
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.App
import com.shabinder.spotiflyer.MainActivity
@ -117,4 +118,7 @@ object Provider {
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 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
@ -77,8 +76,7 @@ fun rotateAnim(view: View){
view.animation = rotate
}
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?,source: Source) {
fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide
@ -122,6 +120,10 @@ fun bindImage(imgView: ImageView, imgUrl: String?,source: Source) {
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.
resource?.copyTo(file)