mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 09:04:32 +01:00
Caching Fixes
This commit is contained in:
parent
7f279f2602
commit
f7e38c2c6e
@ -124,8 +124,9 @@ dependencies {
|
|||||||
implementation(storage.chooser)
|
implementation(storage.chooser)
|
||||||
|
|
||||||
with(bundles) {
|
with(bundles) {
|
||||||
implementation(androidx.lifecycle)
|
implementation(ktor)
|
||||||
implementation(mviKotlin)
|
implementation(mviKotlin)
|
||||||
|
implementation(androidx.lifecycle)
|
||||||
implementation(accompanist.inset)
|
implementation(accompanist.inset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ import com.shabinder.common.models.event.coroutines.failure
|
|||||||
import com.shabinder.common.providers.FetchPlatformQueryResult
|
import com.shabinder.common.providers.FetchPlatformQueryResult
|
||||||
import com.shabinder.common.translations.Strings
|
import com.shabinder.common.translations.Strings
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@ -62,6 +63,7 @@ class ForegroundService : LifecycleService() {
|
|||||||
private val fetcher: FetchPlatformQueryResult by inject()
|
private val fetcher: FetchPlatformQueryResult by inject()
|
||||||
private val logger: Kermit by inject()
|
private val logger: Kermit by inject()
|
||||||
private val dir: FileManager by inject()
|
private val dir: FileManager by inject()
|
||||||
|
private val httpClient: HttpClient by inject()
|
||||||
|
|
||||||
private var messageList =
|
private var messageList =
|
||||||
java.util.Collections.synchronizedList(MutableList(5) { emptyMessage })
|
java.util.Collections.synchronizedList(MutableList(5) { emptyMessage })
|
||||||
@ -170,7 +172,7 @@ class ForegroundService : LifecycleService() {
|
|||||||
trackStatusFlowMap[track.title] = DownloadStatus.Downloading()
|
trackStatusFlowMap[track.title] = DownloadStatus.Downloading()
|
||||||
|
|
||||||
// Enqueueing Download
|
// Enqueueing Download
|
||||||
downloadFile(url).collect {
|
httpClient.downloadFile(url).collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is DownloadResult.Error -> {
|
is DownloadResult.Error -> {
|
||||||
logger.d(TAG) { it.message }
|
logger.d(TAG) { it.message }
|
||||||
|
@ -15,12 +15,19 @@ fun cleanFiles(dir: File) {
|
|||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
cleanFiles(file)
|
cleanFiles(file)
|
||||||
} else if (file.isFile) {
|
} else if (file.isFile) {
|
||||||
if (file.path.toString().substringAfterLast(".") != "mp3") {
|
val filePath = file.path.toString()
|
||||||
Log.d("Files Cleaning", "Cleaning ${file.path}")
|
if (filePath.substringAfterLast(".") != "mp3" || filePath.isTempFile()) {
|
||||||
|
Log.d("Files Cleaning", "Cleaning $filePath")
|
||||||
file.delete()
|
file.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { e.printStackTrace() }
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.isTempFile(): Boolean {
|
||||||
|
return substringBeforeLast(".").takeLast(5) == ".temp"
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ class AndroidFileManager(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Add Mp3 Tags and Add to Library
|
// Add Mp3 Tags and Add to Library
|
||||||
if(trackDetails.audioFormat != AudioFormat.MP3)
|
if (trackDetails.audioFormat != AudioFormat.MP3)
|
||||||
throw InvalidDataException("Audio Format is ${trackDetails.audioFormat}, Needs Conversion!")
|
throw InvalidDataException("Audio Format is ${trackDetails.audioFormat}, Needs Conversion!")
|
||||||
|
|
||||||
Mp3File(File(songFile.absolutePath))
|
Mp3File(File(songFile.absolutePath))
|
||||||
@ -166,7 +166,7 @@ class AndroidFileManager(
|
|||||||
|
|
||||||
override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture =
|
override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture =
|
||||||
withContext(dispatcherIO) {
|
withContext(dispatcherIO) {
|
||||||
val cachePath = imageCacheDir() + getNameURL(url)
|
val cachePath = getImageCachePath(url)
|
||||||
Picture(
|
Picture(
|
||||||
image = (loadCachedImage(cachePath, reqWidth, reqHeight) ?: freshImage(
|
image = (loadCachedImage(cachePath, reqWidth, reqHeight) ?: freshImage(
|
||||||
url,
|
url,
|
||||||
@ -214,7 +214,7 @@ class AndroidFileManager(
|
|||||||
// Decode and Cache Full Sized Image in Background
|
// Decode and Cache Full Sized Image in Background
|
||||||
cacheImage(
|
cacheImage(
|
||||||
BitmapFactory.decodeByteArray(input, 0, input.size),
|
BitmapFactory.decodeByteArray(input, 0, input.size),
|
||||||
imageCacheDir() + getNameURL(url)
|
getImageCachePath(url)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bitmap // return Memory Efficient Bitmap
|
bitmap // return Memory Efficient Bitmap
|
||||||
|
@ -25,10 +25,14 @@ import com.shabinder.common.models.DownloadResult
|
|||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
import com.shabinder.common.utils.removeIllegalChars
|
import com.shabinder.common.utils.removeIllegalChars
|
||||||
|
import com.shabinder.common.utils.requireNotNull
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.http.*
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.HttpStatement
|
||||||
|
import io.ktor.http.contentLength
|
||||||
|
import io.ktor.http.isSuccess
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
@ -80,12 +84,13 @@ fun FileManager.createDirectories() {
|
|||||||
if (!defaultDir().contains("null${fileSeparator()}SpotiFlyer")) {
|
if (!defaultDir().contains("null${fileSeparator()}SpotiFlyer")) {
|
||||||
createDirectory(defaultDir())
|
createDirectory(defaultDir())
|
||||||
createDirectory(imageCacheDir())
|
createDirectory(imageCacheDir())
|
||||||
createDirectory(defaultDir() + "Tracks/")
|
createDirectory(defaultDir() + "Tracks" + fileSeparator())
|
||||||
createDirectory(defaultDir() + "Albums/")
|
createDirectory(defaultDir() + "Albums" + fileSeparator())
|
||||||
createDirectory(defaultDir() + "Playlists/")
|
createDirectory(defaultDir() + "Playlists" + fileSeparator())
|
||||||
createDirectory(defaultDir() + "YT_Downloads/")
|
createDirectory(defaultDir() + "YT_Downloads" + fileSeparator())
|
||||||
}
|
}
|
||||||
} catch (ignored: Exception) { }
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun FileManager.finalOutputDir(
|
fun FileManager.finalOutputDir(
|
||||||
@ -100,24 +105,50 @@ fun FileManager.finalOutputDir(
|
|||||||
removeIllegalChars(subFolder) + this.fileSeparator()
|
removeIllegalChars(subFolder) + this.fileSeparator()
|
||||||
} +
|
} +
|
||||||
removeIllegalChars(itemName) + extension
|
removeIllegalChars(itemName) + extension
|
||||||
/*DIR Specific Operation End*/
|
|
||||||
|
|
||||||
fun getNameURL(url: String): String {
|
fun FileManager.getImageCachePath(
|
||||||
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length)
|
url: String
|
||||||
.replace('/', '_')
|
): String = imageCacheDir() + getNameFromURL(url, isImage = true)
|
||||||
|
|
||||||
|
/*DIR Specific Operation End*/
|
||||||
|
private fun getNameFromURL(url: String, isImage: Boolean = false): String {
|
||||||
|
val startIndex = url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1
|
||||||
|
|
||||||
|
var fileName = if (startIndex != -1)
|
||||||
|
url.substring(startIndex).replace('/', '_')
|
||||||
|
else url.substringAfterLast("/")
|
||||||
|
|
||||||
|
// Generify File Extensions
|
||||||
|
if (isImage) {
|
||||||
|
if (fileName.length - fileName.lastIndexOf(".") > 5) {
|
||||||
|
fileName += ".jpeg"
|
||||||
|
} else {
|
||||||
|
if (fileName.endsWith(".jpg"))
|
||||||
|
fileName = fileName.substringBeforeLast(".") + ".jpeg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
suspend fun HttpClient.downloadFile(url: String) = downloadFile(url, this)
|
||||||
|
|
||||||
|
suspend fun downloadFile(url: String, client: HttpClient? = null): Flow<DownloadResult> {
|
||||||
return flow {
|
return flow {
|
||||||
val client = createHttpClient()
|
val httpClient = client ?: createHttpClient()
|
||||||
val response = client.get<HttpStatement>(url).execute()
|
val response = httpClient.get<HttpStatement>(url).execute()
|
||||||
val data = ByteArray(response.contentLength()!!.toInt())
|
// Not all requests return Content Length
|
||||||
|
val data = kotlin.runCatching {
|
||||||
|
ByteArray(response.contentLength().requireNotNull().toInt())
|
||||||
|
}.getOrNull() ?: byteArrayOf()
|
||||||
var offset = 0
|
var offset = 0
|
||||||
do {
|
do {
|
||||||
// Set Length optimally, after how many kb you want a progress update, now it 0.25mb
|
// Set Length optimally, after how many kb you want a progress update, now it 0.25mb
|
||||||
val currentRead = response.content.readAvailable(data, offset, 2_50_000)
|
val currentRead = response.content.readAvailable(data, offset, 2_50_000)
|
||||||
offset += currentRead
|
offset += currentRead
|
||||||
val progress = (offset * 100f / data.size).roundToInt()
|
val progress = data.size.takeIf { it != 0 }?.let { fileSize ->
|
||||||
|
(offset * 100f / fileSize).roundToInt()
|
||||||
|
} ?: 0
|
||||||
emit(DownloadResult.Progress(progress))
|
emit(DownloadResult.Progress(progress))
|
||||||
} while (currentRead > 0)
|
} while (currentRead > 0)
|
||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
@ -125,7 +156,10 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
|||||||
} else {
|
} else {
|
||||||
emit(DownloadResult.Error("File not downloaded"))
|
emit(DownloadResult.Error("File not downloaded"))
|
||||||
}
|
}
|
||||||
client.close()
|
|
||||||
|
// Close Client if We Created One
|
||||||
|
if (client == null)
|
||||||
|
httpClient.close()
|
||||||
}.catch { e ->
|
}.catch { e ->
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
emit(DownloadResult.Error(e.message ?: "File not downloaded"))
|
emit(DownloadResult.Error(e.message ?: "File not downloaded"))
|
||||||
|
@ -178,8 +178,7 @@ class DesktopFileManager(
|
|||||||
override fun addToLibrary(path: String) {}
|
override fun addToLibrary(path: String) {}
|
||||||
|
|
||||||
override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
||||||
val cachePath = imageCacheDir() + getNameURL(url)
|
var picture: ImageBitmap? = loadCachedImage(getImageCachePath(url), reqWidth, reqHeight)
|
||||||
var picture: ImageBitmap? = loadCachedImage(cachePath, reqWidth, reqHeight)
|
|
||||||
if (picture == null) picture = freshImage(url, reqWidth, reqHeight)
|
if (picture == null) picture = freshImage(url, reqWidth, reqHeight)
|
||||||
return Picture(image = picture)
|
return Picture(image = picture)
|
||||||
}
|
}
|
||||||
@ -208,7 +207,7 @@ class DesktopFileManager(
|
|||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
GlobalScope.launch(Dispatchers.IO) { // TODO Refactor
|
GlobalScope.launch(Dispatchers.IO) { // TODO Refactor
|
||||||
cacheImage(result, imageCacheDir() + getNameURL(url))
|
cacheImage(result, getImageCachePath(url))
|
||||||
}
|
}
|
||||||
result.toImageBitmap()
|
result.toImageBitmap()
|
||||||
} else null
|
} else null
|
||||||
|
@ -20,7 +20,7 @@ data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor(
|
|||||||
// val explicit_content: Int = 0,
|
// val explicit_content: Int = 0,
|
||||||
val has_lyrics: Boolean = false,
|
val has_lyrics: Boolean = false,
|
||||||
val id: String,
|
val id: String,
|
||||||
val image: String,
|
val image: String = "",
|
||||||
val label: String? = null,
|
val label: String? = null,
|
||||||
val label_url: String? = null,
|
val label_url: String? = null,
|
||||||
val language: String,
|
val language: String,
|
||||||
|
@ -19,6 +19,7 @@ package com.shabinder.common.providers.gaana
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.core_components.file_manager.FileManager
|
import com.shabinder.common.core_components.file_manager.FileManager
|
||||||
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
||||||
|
import com.shabinder.common.core_components.file_manager.getImageCachePath
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.common.models.SpotiFlyerException
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
@ -124,8 +125,7 @@ class GaanaProvider(
|
|||||||
title = it.track_title,
|
title = it.track_title,
|
||||||
artists = it.artist.map { artist -> artist?.name.toString() },
|
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||||
durationSec = it.duration,
|
durationSec = it.duration,
|
||||||
albumArtPath = fileManager.imageCacheDir() + (it.artworkLink.substringBeforeLast('/')
|
albumArtPath = fileManager.getImageCachePath(it.artworkLink),
|
||||||
.substringAfterLast('/')) + ".jpeg",
|
|
||||||
albumName = it.album_title,
|
albumName = it.album_title,
|
||||||
genre = it.genre?.mapNotNull { genre -> genre?.name } ?: emptyList(),
|
genre = it.genre?.mapNotNull { genre -> genre?.name } ?: emptyList(),
|
||||||
year = it.release_date,
|
year = it.release_date,
|
||||||
|
@ -3,6 +3,7 @@ package com.shabinder.common.providers.saavn
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.core_components.file_manager.FileManager
|
import com.shabinder.common.core_components.file_manager.FileManager
|
||||||
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
||||||
|
import com.shabinder.common.core_components.file_manager.getImageCachePath
|
||||||
import com.shabinder.common.models.*
|
import com.shabinder.common.models.*
|
||||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
import com.shabinder.common.models.saavn.SaavnSong
|
import com.shabinder.common.models.saavn.SaavnSong
|
||||||
@ -68,7 +69,7 @@ class SaavnProvider(
|
|||||||
artists = it.artistMap.keys.toMutableSet().apply { addAll(it.singers.split(",")) }.toList(),
|
artists = it.artistMap.keys.toMutableSet().apply { addAll(it.singers.split(",")) }.toList(),
|
||||||
durationSec = it.duration.toInt(),
|
durationSec = it.duration.toInt(),
|
||||||
albumName = it.album,
|
albumName = it.album,
|
||||||
albumArtPath = fileManager.imageCacheDir() + (it.image.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
albumArtPath = fileManager.getImageCachePath(it.image),
|
||||||
year = it.year,
|
year = it.year,
|
||||||
comment = it.copyright_text,
|
comment = it.copyright_text,
|
||||||
trackUrl = it.perma_url,
|
trackUrl = it.perma_url,
|
||||||
|
@ -3,6 +3,7 @@ package com.shabinder.common.providers.sound_cloud
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.core_components.file_manager.FileManager
|
import com.shabinder.common.core_components.file_manager.FileManager
|
||||||
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
||||||
|
import com.shabinder.common.core_components.file_manager.getImageCachePath
|
||||||
import com.shabinder.common.models.AudioFormat
|
import com.shabinder.common.models.AudioFormat
|
||||||
import com.shabinder.common.models.AudioQuality
|
import com.shabinder.common.models.AudioQuality
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
@ -70,9 +71,7 @@ class SoundCloudProvider(
|
|||||||
artists = /*it.artists?.map { artist -> artist?.name.toString() } ?:*/ listOf(it.user.username.ifBlank { it.genre }),
|
artists = /*it.artists?.map { artist -> artist?.name.toString() } ?:*/ listOf(it.user.username.ifBlank { it.genre }),
|
||||||
albumArtists = /*it.album?.artists?.mapNotNull { artist -> artist?.name } ?:*/ emptyList(),
|
albumArtists = /*it.album?.artists?.mapNotNull { artist -> artist?.name } ?:*/ emptyList(),
|
||||||
durationSec = (it.duration / 1000),
|
durationSec = (it.duration / 1000),
|
||||||
albumArtPath = fileManager.imageCacheDir() + (it.artworkUrl.formatArtworkUrl()).substringAfterLast(
|
albumArtPath = fileManager.getImageCachePath(it.artworkUrl.formatArtworkUrl()),
|
||||||
'/'
|
|
||||||
) + ".jpeg",
|
|
||||||
albumName = "", //it.album?.name,
|
albumName = "", //it.album?.name,
|
||||||
year = runCatching { it.displayDate.substring(0, 4) }.getOrNull(),
|
year = runCatching { it.displayDate.substring(0, 4) }.getOrNull(),
|
||||||
comment = it.caption,
|
comment = it.caption,
|
||||||
|
@ -19,6 +19,7 @@ package com.shabinder.common.providers.spotify
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.core_components.file_manager.FileManager
|
import com.shabinder.common.core_components.file_manager.FileManager
|
||||||
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
||||||
|
import com.shabinder.common.core_components.file_manager.getImageCachePath
|
||||||
import com.shabinder.common.core_components.utils.createHttpClient
|
import com.shabinder.common.core_components.utils.createHttpClient
|
||||||
import com.shabinder.common.models.*
|
import com.shabinder.common.models.*
|
||||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
@ -201,9 +202,7 @@ class SpotifyProvider(
|
|||||||
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
||||||
albumArtists = it.album?.artists?.mapNotNull { artist -> artist?.name } ?: emptyList(),
|
albumArtists = it.album?.artists?.mapNotNull { artist -> artist?.name } ?: emptyList(),
|
||||||
durationSec = (it.duration_ms / 1000).toInt(),
|
durationSec = (it.duration_ms / 1000).toInt(),
|
||||||
albumArtPath = fileManager.imageCacheDir() + (it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast(
|
albumArtPath = fileManager.getImageCachePath(it.album?.images?.firstOrNull()?.url ?: ""),
|
||||||
'/'
|
|
||||||
) + ".jpeg",
|
|
||||||
albumName = it.album?.name,
|
albumName = it.album?.name,
|
||||||
year = it.album?.release_date,
|
year = it.album?.release_date,
|
||||||
comment = "Genres:${it.album?.genres?.joinToString()}",
|
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||||
|
@ -19,6 +19,7 @@ package com.shabinder.common.providers.youtube
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.core_components.file_manager.FileManager
|
import com.shabinder.common.core_components.file_manager.FileManager
|
||||||
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
import com.shabinder.common.core_components.file_manager.finalOutputDir
|
||||||
|
import com.shabinder.common.core_components.file_manager.getImageCachePath
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.common.models.SpotiFlyerException
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
@ -30,7 +31,7 @@ import io.github.shabinder.YoutubeDownloader
|
|||||||
import io.github.shabinder.models.YoutubeVideo
|
import io.github.shabinder.models.YoutubeVideo
|
||||||
import io.github.shabinder.models.formats.Format
|
import io.github.shabinder.models.formats.Format
|
||||||
import io.github.shabinder.models.quality.AudioQuality
|
import io.github.shabinder.models.quality.AudioQuality
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
|
|
||||||
class YoutubeProvider(
|
class YoutubeProvider(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
@ -108,13 +109,14 @@ class YoutubeProvider(
|
|||||||
title = name
|
title = name
|
||||||
|
|
||||||
trackList = videos.map {
|
trackList = videos.map {
|
||||||
|
val imageURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg"
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = it.title ?: "N/A",
|
title = it.title ?: "N/A",
|
||||||
artists = listOf(it.author ?: "N/A"),
|
artists = listOf(it.author ?: "N/A"),
|
||||||
durationSec = it.lengthSeconds,
|
durationSec = it.lengthSeconds,
|
||||||
albumArtPath = fileManager.imageCacheDir() + it.videoId + ".jpeg",
|
albumArtPath = fileManager.getImageCachePath(imageURL),
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
|
albumArtURL = imageURL,
|
||||||
downloaded = if (fileManager.isPresent(
|
downloaded = if (fileManager.isPresent(
|
||||||
fileManager.finalOutputDir(
|
fileManager.finalOutputDir(
|
||||||
itemName = it.title ?: "N/A",
|
itemName = it.title ?: "N/A",
|
||||||
@ -155,7 +157,7 @@ class YoutubeProvider(
|
|||||||
val video = ytDownloader.getVideo(searchId)
|
val video = ytDownloader.getVideo(searchId)
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||||
val detail = video.videoDetails
|
val detail = video.videoDetails
|
||||||
val name = detail.title?.replace(detail.author?.uppercase() ?: "", "", true)
|
val name = detail.title?.replace(detail.author?.toUpperCase() ?: "", "", true)
|
||||||
?: detail.title ?: ""
|
?: detail.title ?: ""
|
||||||
// logger.i{ detail.toString() }
|
// logger.i{ detail.toString() }
|
||||||
trackList = listOf(
|
trackList = listOf(
|
||||||
@ -163,9 +165,9 @@ class YoutubeProvider(
|
|||||||
title = name,
|
title = name,
|
||||||
artists = listOf(detail.author ?: "N/A"),
|
artists = listOf(detail.author ?: "N/A"),
|
||||||
durationSec = detail.lengthSeconds,
|
durationSec = detail.lengthSeconds,
|
||||||
albumArtPath = fileManager.imageCacheDir() + "$searchId.jpeg",
|
albumArtPath = fileManager.getImageCachePath(coverUrl),
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
albumArtURL = coverUrl,
|
||||||
downloaded = if (fileManager.isPresent(
|
downloaded = if (fileManager.isPresent(
|
||||||
fileManager.finalOutputDir(
|
fileManager.finalOutputDir(
|
||||||
itemName = name,
|
itemName = name,
|
||||||
@ -179,7 +181,12 @@ class YoutubeProvider(
|
|||||||
else {
|
else {
|
||||||
DownloadStatus.NotDownloaded
|
DownloadStatus.NotDownloaded
|
||||||
},
|
},
|
||||||
outputFilePath = fileManager.finalOutputDir(name, folderType, subFolder, fileManager.defaultDir()/*,".m4a"*/),
|
outputFilePath = fileManager.finalOutputDir(
|
||||||
|
name,
|
||||||
|
folderType,
|
||||||
|
subFolder,
|
||||||
|
fileManager.defaultDir()/*,".m4a"*/
|
||||||
|
),
|
||||||
videoID = searchId
|
videoID = searchId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -18,19 +18,33 @@ package com.shabinder.common.providers.youtube_music
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.core_components.file_manager.FileManager
|
import com.shabinder.common.core_components.file_manager.FileManager
|
||||||
import com.shabinder.common.models.*
|
import com.shabinder.common.models.AudioFormat
|
||||||
|
import com.shabinder.common.models.AudioQuality
|
||||||
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
import com.shabinder.common.models.YoutubeTrack
|
||||||
|
import com.shabinder.common.models.corsApi
|
||||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
import com.shabinder.common.models.event.coroutines.flatMap
|
import com.shabinder.common.models.event.coroutines.flatMap
|
||||||
import com.shabinder.common.models.event.coroutines.flatMapError
|
|
||||||
import com.shabinder.common.models.event.coroutines.map
|
import com.shabinder.common.models.event.coroutines.map
|
||||||
import com.shabinder.common.providers.youtube.YoutubeProvider
|
import com.shabinder.common.providers.youtube.YoutubeProvider
|
||||||
import com.shabinder.common.providers.youtube.get
|
|
||||||
import com.shabinder.common.providers.youtube_to_mp3.requests.YoutubeMp3
|
import com.shabinder.common.providers.youtube_to_mp3.requests.YoutubeMp3
|
||||||
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
|
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.headers
|
||||||
import io.ktor.http.*
|
import io.ktor.client.request.post
|
||||||
import kotlinx.serialization.json.*
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@ -50,7 +64,7 @@ class YoutubeMusic constructor(
|
|||||||
suspend fun findMp3SongDownloadURLYT(
|
suspend fun findMp3SongDownloadURLYT(
|
||||||
trackDetails: TrackDetails,
|
trackDetails: TrackDetails,
|
||||||
preferredQuality: AudioQuality = fileManager.preferenceManager.audioQuality
|
preferredQuality: AudioQuality = fileManager.preferenceManager.audioQuality
|
||||||
): SuspendableEvent<Pair<String,AudioQuality>, Throwable> {
|
): SuspendableEvent<Pair<String, AudioQuality>, Throwable> {
|
||||||
return getYTIDBestMatch(trackDetails).flatMap { videoID ->
|
return getYTIDBestMatch(trackDetails).flatMap { videoID ->
|
||||||
// As YT compress Audio hence there is no benefit of quality for more than 192
|
// As YT compress Audio hence there is no benefit of quality for more than 192
|
||||||
val optimalQuality =
|
val optimalQuality =
|
||||||
@ -69,7 +83,7 @@ class YoutubeMusic constructor(
|
|||||||
}
|
}
|
||||||
}*/.map {
|
}*/.map {
|
||||||
trackDetails.audioFormat = AudioFormat.MP3
|
trackDetails.audioFormat = AudioFormat.MP3
|
||||||
Pair(it,optimalQuality)
|
Pair(it, optimalQuality)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,7 +182,7 @@ class YoutubeMusic constructor(
|
|||||||
! 4 - Duration (hh:mm:ss)
|
! 4 - Duration (hh:mm:ss)
|
||||||
!
|
!
|
||||||
! We blindly gather all the details we get our hands on, then
|
! We blindly gather all the details we get our hands on, then
|
||||||
! cherry pick the details we need based on their index numbers,
|
! cherry-pick the details we need based on their index numbers,
|
||||||
! we do so only if their Type is 'Song' or 'Video
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -180,7 +194,7 @@ class YoutubeMusic constructor(
|
|||||||
/*
|
/*
|
||||||
Filter Out dummies here itself
|
Filter Out dummies here itself
|
||||||
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
||||||
! sub-block, if not its a dummy, why does the YTM response contain dummies?
|
! sub-block, if not it is a dummy, why does the YTM response contain dummies?
|
||||||
! I have no clue. We skip these.
|
! I have no clue. We skip these.
|
||||||
|
|
||||||
! Remember that we appended the linkBlock to result, treating that like the
|
! Remember that we appended the linkBlock to result, treating that like the
|
||||||
@ -189,7 +203,7 @@ class YoutubeMusic constructor(
|
|||||||
*/
|
*/
|
||||||
for (detailArray in result.subList(0, result.size - 1)) {
|
for (detailArray in result.subList(0, result.size - 1)) {
|
||||||
for (detail in detailArray.jsonArray) {
|
for (detail in detailArray.jsonArray) {
|
||||||
if (detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0 < 2) continue
|
if ((detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0) < 2) continue
|
||||||
|
|
||||||
// if not a dummy, collect All Variables
|
// if not a dummy, collect All Variables
|
||||||
val details =
|
val details =
|
||||||
@ -262,8 +276,8 @@ class YoutubeMusic constructor(
|
|||||||
// most song results on youtube go by $artist - $songName or artist1/artist2
|
// most song results on youtube go by $artist - $songName or artist1/artist2
|
||||||
var hasCommonWord = false
|
var hasCommonWord = false
|
||||||
|
|
||||||
val resultName = result.name?.lowercase()?.replace("-", " ")?.replace("/", " ") ?: ""
|
val resultName = result.name?.toLowerCase()?.replace("-", " ")?.replace("/", " ") ?: ""
|
||||||
val trackNameWords = trackName.lowercase().split(" ")
|
val trackNameWords = trackName.toLowerCase().split(" ")
|
||||||
|
|
||||||
for (nameWord in trackNameWords) {
|
for (nameWord in trackNameWords) {
|
||||||
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(
|
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(
|
||||||
@ -287,8 +301,8 @@ class YoutubeMusic constructor(
|
|||||||
if (result.type == "Song") {
|
if (result.type == "Song") {
|
||||||
for (artist in trackArtists) {
|
for (artist in trackArtists) {
|
||||||
if (FuzzySearch.ratio(
|
if (FuzzySearch.ratio(
|
||||||
artist.lowercase(),
|
artist.toLowerCase(),
|
||||||
result.artist?.lowercase() ?: ""
|
result.artist?.toLowerCase() ?: ""
|
||||||
) > 85
|
) > 85
|
||||||
)
|
)
|
||||||
artistMatchNumber++
|
artistMatchNumber++
|
||||||
@ -296,8 +310,8 @@ class YoutubeMusic constructor(
|
|||||||
} else { // i.e. is a Video
|
} else { // i.e. is a Video
|
||||||
for (artist in trackArtists) {
|
for (artist in trackArtists) {
|
||||||
if (FuzzySearch.partialRatio(
|
if (FuzzySearch.partialRatio(
|
||||||
artist.lowercase(),
|
artist.toLowerCase(),
|
||||||
result.name?.lowercase() ?: ""
|
result.name?.toLowerCase() ?: ""
|
||||||
) > 85
|
) > 85
|
||||||
)
|
)
|
||||||
artistMatchNumber++
|
artistMatchNumber++
|
||||||
|
Loading…
Reference in New Issue
Block a user