Models, Business Logic Added.

This commit is contained in:
shabinder 2020-12-29 23:36:12 +05:30
parent 38618bd2b1
commit dcfca42c40
63 changed files with 3439 additions and 25 deletions

View File

@ -1,7 +1,10 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-parcelize'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
id 'kotlinx-serialization'
} }
kapt { kapt {
@ -98,6 +101,15 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
//Hilt
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
//FFmpeg
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
//Okhttp //Okhttp
implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
@ -106,11 +118,15 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//Json //Json
implementation 'com.beust:klaxon:5.4'
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-scalars:2.9.0"
implementation 'com.beust:klaxon:5.4' implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
//Glide-Image Loading
implementation "dev.chrisbanes.accompanist:accompanist-glide:0.4.1"
//Coil-Image Loading //Coil-Image Loading
implementation "dev.chrisbanes.accompanist:accompanist-coil:$coil_version" implementation "dev.chrisbanes.accompanist:accompanist-coil:$coil_version"

BIN
app/libs/mobile-ffmpeg.aar Normal file

Binary file not shown.

View File

@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application <application
android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

View File

@ -0,0 +1,46 @@
/*
* 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
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
/*
* 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/>.
*/
@HiltAndroidApp
class App:Application(){
companion object{
const val clientId:String = "694d8bf4f6ec420fa66ea7fb4c68f89d"
const val clientSecret:String = "02ca2d4021a7452dae2328b47a6e8fe8"
}
}

View File

@ -16,18 +16,28 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.setContent import androidx.compose.ui.platform.setContent
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.shabinder.spotiflyer.home.Home import androidx.lifecycle.ViewModelProvider
import com.shabinder.spotiflyer.navigation.ComposeNavigation import com.shabinder.spotiflyer.navigation.ComposeNavigation
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.ui.ComposeLearnTheme import com.shabinder.spotiflyer.ui.ComposeLearnTheme
import com.shabinder.spotiflyer.ui.appNameStyle import com.shabinder.spotiflyer.ui.appNameStyle
import com.shabinder.spotiflyer.utils.requestStoragePermission import com.shabinder.spotiflyer.utils.requestStoragePermission
import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.accompanist.insets.ProvideWindowInsets import dev.chrisbanes.accompanist.insets.ProvideWindowInsets
import dev.chrisbanes.accompanist.insets.statusBarsHeight import dev.chrisbanes.accompanist.insets.statusBarsHeight
import javax.inject.Inject
/*
* This is App's God Activity
* */
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private var spotifyService : SpotifyService? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
// This app draws behind the system bars, so we want to handle fitting system windows // This app draws behind the system bars, so we want to handle fitting system windows
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@ -56,7 +66,9 @@ class MainActivity : AppCompatActivity() {
companion object{ companion object{
private lateinit var instance: MainActivity private lateinit var instance: MainActivity
private lateinit var sharedViewModel: SharedViewModel
fun getInstance():MainActivity = this.instance fun getInstance():MainActivity = this.instance
fun getSharedViewModel():SharedViewModel = this.sharedViewModel
} }
init { init {
@ -100,6 +112,20 @@ fun AppBar(
@Composable @Composable
fun DefaultPreview() { fun DefaultPreview() {
ComposeLearnTheme { ComposeLearnTheme {
ProvideWindowInsets {
Column {
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.87f)
// Draw a scrim over the status bar which matches the app bar
Spacer(Modifier.background(appBarColor).fillMaxWidth().statusBarsHeight())
AppBar(
backgroundColor = appBarColor,
modifier = Modifier.fillMaxWidth()
)
ComposeNavigation()
}
}
} }
} }

View File

@ -0,0 +1,27 @@
/*
* 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
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.networking.SpotifyService
class SharedViewModel : ViewModel() {
var intentString = MutableLiveData<String>()
var spotifyService = MutableLiveData<SpotifyService>()
}

View File

@ -0,0 +1,35 @@
/*
* 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.database
import androidx.room.*
@Dao
interface DatabaseDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(record: DownloadRecord)
@Update
suspend fun update(record: DownloadRecord)
@Query("SELECT * from download_record_table ORDER BY id DESC")
suspend fun getRecord():List<DownloadRecord>
@Query("DELETE FROM download_record_table")
suspend fun deleteAll()
}

View File

@ -0,0 +1,51 @@
/*
* 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.database
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(
tableName = "download_record_table",
indices = [Index(value = ["link"], unique = true)]
)
data class DownloadRecord(
@PrimaryKey(autoGenerate = true)
var id:Int = 0,
@ColumnInfo(name = "type")
var type:String,
@ColumnInfo(name = "name")
var name:String,
@ColumnInfo(name = "link")
var link:String,
@ColumnInfo(name = "coverUrl")
var coverUrl:String,
@ColumnInfo(name = "totalFiles")
var totalFiles:Int = 1,
):Parcelable

View File

@ -0,0 +1,56 @@
/*
* 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.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [DownloadRecord::class], version = 2, exportSchema = false)
abstract class DownloadRecordDatabase:RoomDatabase() {
abstract val databaseDAO: DatabaseDAO
companion object {
@Volatile
private var INSTANCE: DownloadRecordDatabase? = null
fun getInstance(context: Context): DownloadRecordDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
DownloadRecordDatabase::class.java,
"download_record_database")
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}

View File

@ -0,0 +1,235 @@
/*
* 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.annotation.SuppressLint
import com.beust.klaxon.JsonArray
import com.beust.klaxon.JsonObject
import com.beust.klaxon.Parser
import com.shabinder.spotiflyer.models.YoutubeTrack
import me.xdrop.fuzzywuzzy.FuzzySearch
import kotlin.math.absoluteValue
/*
* Thanks To https://github.com/spotDL/spotify-downloader
* */
fun getYTTracks(response: String):List<YoutubeTrack>{
val youtubeTracks = mutableListOf<YoutubeTrack>()
val stringBuilder: StringBuilder = StringBuilder(response)
val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
if (contentBlocks != null) {
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 - com.shabinder.spotiflyer.models.gaana.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
*/
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-1)){
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()
)
}
}
//log("Text Api",availableDetails.toString())
/*
! Filter Out non-Song/Video results and incomplete results here itself
! From what we know about detail order, note that [1] - indicate result type
*/
if ( availableDetails.size == 5 && 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 //Has Been Giving Issues
/*
! 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],
duration = availableDetails[4],
videoId = videoId
)
youtubeTracks.add(ytTrack)
}
}
}
return youtubeTracks
}
@SuppressLint("DefaultLocale")
fun sortByBestMatch(ytTracks:List<YoutubeTrack>,
trackName:String,
trackArtists:List<String>,
trackDurationSec:Int,
):Map<String,Int>{
/*
* "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value
**/
val linksWithMatchValue = mutableMapOf<String,Int>()
for (result in ytTracks){
// LoweCasing Name to match Properly
// most song results on youtube go by $artist - $songName or artist1/artist2
var hasCommonWord = false
val resultName = result.name?.toLowerCase()?.replace("-"," ")?.replace("/"," ") ?: ""
val trackNameWords = trackName.toLowerCase().split(" ")
for (nameWord in trackNameWords){
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord,resultName) > 85) hasCommonWord = true
}
// Skip this Result if No Word is Common in Name
if (!hasCommonWord) {
//log("YT Api Removing", result.toString())
continue
}
// Find artist match
// Will Be Using Fuzzy Search Because YT Spelling might be mucked up
// match = (no of artist names in result) / (no. of artist names on spotify) * 100
var artistMatchNumber = 0
if(result.type == "Song"){
for (artist in trackArtists){
if(FuzzySearch.ratio(artist.toLowerCase(),result.artist?.toLowerCase()) > 85)
artistMatchNumber++
}
}else{//i.e. is a Video
for (artist in trackArtists) {
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85)
artistMatchNumber++
}
}
if(artistMatchNumber == 0) {
//log("YT Api Removing", result.toString())
continue
}
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
// Duration Match
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
! difference in song duration (delta) is usually of the magnitude of a few
! seconds, we need to amplify the delta if it is to have any meaningful impact
! wen we calculate the avg match value*/
val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60)
?.plus(result.duration?.split(":")?.get(1)?.toInt()?:0)
?.minus(trackDurationSec)?.absoluteValue ?: 0
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
val durationMatch = 100 - (nonMatchValue*100)
val avgMatch = (artistMatch + durationMatch)/2
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
}
//log("YT Api Result", "$trackName - $linksWithMatchValue")
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
}

View File

@ -0,0 +1,51 @@
/*
* 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 com.shabinder.spotiflyer.models.spotify.Source
import kotlinx.parcelize.Parcelize
import java.io.File
@Parcelize
data class TrackDetails(
var title:String,
var artists:List<String>,
var durationSec:Int,
var albumName:String?=null,
var year:String?=null,
var comment:String?=null,
var lyrics:String?=null,
var trackUrl:String?=null,
var albumArt: File,
var albumArtURL: String,
var source: Source,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
var progress: Int = 0,
var outputFile: String,
var videoID:String? = null
):Parcelable
enum class DownloadStatus{
Downloaded,
Downloading,
Queued,
NotDownloaded,
Converting,
Failed
}

View File

@ -0,0 +1,23 @@
/*
* 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 kotlinx.serialization.Serializable
@Serializable
data class Optional<T>(val value: T?)

View File

@ -0,0 +1,30 @@
/*
* 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.parcelize.Parcelize
@Parcelize
data class YoutubeTrack(
var name: String? = null,
var type: String? = null, // Song / Video
var artist: String? = null,
var duration:String? = null,
var videoId: String? = null
):Parcelable

View File

@ -0,0 +1,27 @@
/*
* 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.gaana
import com.squareup.moshi.Json
data class Artist (
val popularity : Int,
val seokey : String,
val name : String,
@Json(name = "artwork_175x175")var artworkLink :String?
)

View File

@ -0,0 +1,28 @@
/*
* 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.gaana
import com.squareup.moshi.Json
data class CustomArtworks (
@Json(name = "40x40") val size_40p : String,
@Json(name = "80x80") val size_80p : String,
@Json(name = "110x110")val size_110p : String,
@Json(name = "175x175")val size_175p : String,
@Json(name = "480x480")val size_480p : String,
)

View File

@ -0,0 +1,26 @@
/*
* 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.gaana
data class GaanaAlbum (
val tracks : List<GaanaTrack>,
val count : Int,
val custom_artworks : CustomArtworks,
val release_year : Int,
val favorite_count : Int,
)

View File

@ -0,0 +1,23 @@
/*
* 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.gaana
data class GaanaArtistDetails(
val artist : List<Artist>,
val count : Int,
)

View File

@ -0,0 +1,23 @@
/*
* 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.gaana
data class GaanaArtistTracks(
val count : Int,
val tracks : List<GaanaTrack>
)

View File

@ -0,0 +1,26 @@
/*
* 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.gaana
data class GaanaPlaylist (
val modified_on : String,
val count : Int,
val created_on : String,
val favorite_count : Int,
val tracks : List<GaanaTrack>,
)

View File

@ -0,0 +1,22 @@
/*
* 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.gaana
data class GaanaSong(
val tracks : List<GaanaTrack>
)

View File

@ -0,0 +1,41 @@
/*
* 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.gaana
import com.shabinder.spotiflyer.models.DownloadStatus
import com.squareup.moshi.Json
data class GaanaTrack (
val tags : List<Tags?>?,
val seokey : String,
val albumseokey : String?,
val track_title : String,
val album_title : String?,
val language : String?,
val duration: Int,
@Json(name = "artwork_large") val artworkLink : String,
val artist : List<Artist?>,
@Json(name = "gener") val genre : List<Genre?>?,
val lyrics_url : String?,
val youtube_id : String?,
val total_favourite_count : Int?,
val release_date : String?,
val play_ct : String?,
val secondary_language : String?,
var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded
)

View File

@ -0,0 +1,23 @@
/*
* 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.gaana
data class Genre (
val genre_id : Int,
val name : String
)

View File

@ -0,0 +1,23 @@
/*
* 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.gaana
data class Tags (
val tag_id : Int,
val tag_name : String
)

View File

@ -0,0 +1,42 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Album(
var album_type: String? = null,
var artists: List<Artist?>? = null,
var available_markets: List<String?>? = null,
var copyrights: List<Copyright?>? = null,
var external_ids: Map<String?, String?>? = null,
var external_urls: Map<String?, String?>? = null,
var genres: List<String?>? = null,
var href: String? = null,
var id: String? = null,
var images: List<Image?>? = null,
var label :String? = null,
var name: String? = null,
var popularity: Int? = null,
var release_date: String? = null,
var release_date_precision: String? = null,
var tracks: PagingObjectTrack? = null,
var type: String? = null,
var uri: String? = null):Parcelable

View File

@ -0,0 +1,30 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Artist(
var external_urls: Map<String?, String?>? = null,
var href: String? = null,
var id: String? = null,
var name: String? = null,
var type: String? = null,
var uri: String? = null):Parcelable

View File

@ -0,0 +1,26 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Copyright(
var text: String? = null,
var type: String? = null):Parcelable

View File

@ -0,0 +1,42 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Episodes(
var audio_preview_url:String?,
var description:String?,
var duration_ms:Int?,
var explicit:Boolean?,
var external_urls:Map<String,String>?,
var href:String?,
var id:String?,
var images:List<Image?>?,
var is_externally_hosted:Boolean?,
var is_playable:Boolean?,
var language:String?,
var languages:List<String?>?,
var name:String?,
var release_date:String?,
var release_date_precision:String?,
var type:String?,
var uri:String
): Parcelable

View File

@ -0,0 +1,26 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Followers(
var href: String? = null,
var total: Int? = null):Parcelable

View File

@ -0,0 +1,27 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Image(
var width: Int? = null,
var height: Int? = null,
var url: String? = null):Parcelable

View File

@ -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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class LinkedTrack(
var external_urls: Map<String?, String?>? = null,
var href: String? = null,
var id: String? = null,
var type: String? = null,
var uri: String? = null): Parcelable

View File

@ -0,0 +1,31 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class PagingObjectPlaylistTrack(
var href: String? = null,
var items: List<PlaylistTrack>? = null,
var limit: Int = 0,
var next: String? = null,
var offset: Int = 0,
var previous: String? = null,
var total: Int = 0): Parcelable

View File

@ -0,0 +1,31 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class PagingObjectTrack(
var href: String? = null,
var items: List<Track>? = null,
var limit: Int = 0,
var next: String? = null,
var offset: Int = 0,
var previous: String? = null,
var total: Int = 0):Parcelable

View File

@ -0,0 +1,39 @@
/*
* 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.spotify
import android.os.Parcelable
import com.squareup.moshi.Json
import kotlinx.parcelize.Parcelize
@Parcelize
data class Playlist(
@Json(name = "collaborative")var is_collaborative: Boolean? = null,
var description: String? = null,
var external_urls: Map<String?, String?>? = null,
var followers: Followers? = null,
var href: String? = null,
var id: String? = null,
var images: List<Image?>? = null,
var name: String? = null,
var owner: UserPublic? = null,
@Json(name = "public")var is_public: Boolean? = null,
var snapshot_id: String? = null,
var tracks: PagingObjectPlaylistTrack? = null,
var type: String? = null,
var uri: String? = null): Parcelable

View File

@ -0,0 +1,28 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class PlaylistTrack(
var added_at: String? = null,
var added_by: UserPublic? = null,
var track: Track? = null,
var is_local: Boolean? = null): Parcelable

View File

@ -0,0 +1,24 @@
/*
* 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.spotify
enum class Source {
Spotify,
YouTube,
Gaana,
}

View File

@ -0,0 +1,28 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Token(
var access_token:String,
var token_type:String,
var expires_in:Int
): Parcelable

View File

@ -0,0 +1,45 @@
/*
* 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.spotify
import android.os.Parcelable
import com.shabinder.spotiflyer.models.DownloadStatus
import kotlinx.parcelize.Parcelize
@Parcelize
data class Track(
var artists: List<Artist?>? = null,
var available_markets: List<String?>? = null,
var is_playable: Boolean? = null,
var linked_from: LinkedTrack? = null,
var disc_number: Int = 0,
var duration_ms: Long = 0,
var explicit: Boolean? = null,
var external_urls: Map<String?, String?>? = null,
var href: String? = null,
var name: String? = null,
var preview_url: String? = null,
var track_number: Int = 0,
var type: String? = null,
var uri: String? = null,
var album: Album? = null,
var external_ids: Map<String?, String?>? = null,
var popularity: Int? = null,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
):Parcelable

View File

@ -0,0 +1,35 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserPrivate(
val country:String,
var display_name: String,
val email:String,
var external_urls: Map<String?, String?>? = null,
var followers: Followers? = null,
var href: String? = null,
var id: String? = null,
var images: List<Image?>? = null,
var product:String,
var type: String? = null,
var uri: String? = null): Parcelable

View File

@ -0,0 +1,32 @@
/*
* 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.spotify
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserPublic(
var display_name: String? = null,
var external_urls: Map<String?, String?>? = null,
var followers: Followers? = null,
var href: String? = null,
var id: String? = null,
var images: List<Image?>? = null,
var type: String? = null,
var uri: String? = null): Parcelable

View File

@ -6,8 +6,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.navArgument import androidx.navigation.compose.navArgument
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.shabinder.spotiflyer.home.Home import com.shabinder.spotiflyer.ui.home.Home
import com.shabinder.spotiflyer.tracklist.TrackList import com.shabinder.spotiflyer.ui.platforms.gaana.Gaana
import com.shabinder.spotiflyer.ui.platforms.spotify.Spotify
import com.shabinder.spotiflyer.ui.platforms.youtube.Youtube
@Composable @Composable
fun ComposeNavigation() { fun ComposeNavigation() {
@ -22,13 +24,37 @@ fun ComposeNavigation() {
Home(navController = navController) Home(navController = navController)
} }
//Track list Screen //Spotify Screen
//Argument `link` = Link of Track/Album/Playlist //Argument `link` = Link of Track/Album/Playlist
composable( composable(
"track_list/{link}", "spotify/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType }) arguments = listOf(navArgument("link") { type = NavType.StringType })
) { ) {
TrackList( Spotify(
link = it.arguments?.getString("link") ?: "error",
navController = navController
)
}
//Gaana Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"gaana/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
Gaana(
link = it.arguments?.getString("link") ?: "error",
navController = navController
)
}
//Youtube Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"youtube/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
Youtube(
link = it.arguments?.getString("link") ?: "error", link = it.arguments?.getString("link") ?: "error",
navController = navController navController = navController
) )

View File

@ -0,0 +1,109 @@
/*
* 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.networking
import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.gaana.*
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.Url
const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990"
interface GaanaInterface {
/*
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular_playlist" , "playlist_home_featured" ,"playlist_detail" ,"user_playlist" ,"topCharts"]
**/
@GET(".")
suspend fun getGaanaPlaylist(
@Query("type") type: String = "playlist",
@Query("subtype") subtype: String = "playlist_detail",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
@Query("limit") limit: Int = 2000
): Optional<GaanaPlaylist>
/*
* Api Request: http://api.gaana.com/?type=album&subtype=album_detail&seokey=kabir-singh&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "new_release" ,"featured_album" ,"similar_album" ,"all_albums", "album" ,"album_detail" ,"album_detail_info"]
**/
@GET(".")
suspend fun getGaanaAlbum(
@Query("type") type: String = "album",
@Query("subtype") subtype: String = "album_detail",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
@Query("limit") limit: Int = 2000
): Optional<GaanaAlbum>
/*
* Api Request: http://api.gaana.com/?type=song&subtype=song_detail&seokey=pachtaoge&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "hot_songs" ,"recommendation" ,"song_detail"]
**/
@GET(".")
suspend fun getGaanaSong(
@Query("type") type: String = "song",
@Query("subtype") subtype: String = "song_detail",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
): Optional<GaanaSong>
/*
* Api Request: https://api.gaana.com/?type=artist&subtype=artist_details_info&seokey=neha-kakkar&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "artist_list" ,"artist_track_listing" ,"artist_album" ,"similar_artist","artist_details" ,"artist_details_info"]
**/
@GET(".")
suspend fun getGaanaArtistDetails(
@Query("type") type: String = "artist",
@Query("subtype") subtype: String = "artist_details_info",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
): Optional<GaanaArtistDetails>
/*
* Api Request: http://api.gaana.com/?type=artist&subtype=artist_track_listing&seokey=neha-kakkar&limit=50&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "artist_list" ,"artist_track_listing" ,"artist_album" ,"similar_artist","artist_details" ,"artist_details_info"]
**/
@GET(".")
suspend fun getGaanaArtistTracks(
@Query("type") type: String = "artist",
@Query("subtype") subtype: String = "artist_track_listing",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
@Query("limit") limit: Int = 50
): Optional<GaanaArtistTracks>
/*
* Dynamic Url Requests
* */
@GET
fun getResponse(@Url url:String): Call<ResponseBody>
}

View File

@ -0,0 +1,55 @@
/*
* 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.networking
import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.spotify.*
import retrofit2.http.*
interface SpotifyService {
@GET("playlists/{playlist_id}")
suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Optional<Playlist>
@GET("playlists/{playlist_id}/tracks")
suspend fun getPlaylistTracks(
@Path("playlist_id") playlistId: String?,
@Query("offset") offset: Int = 0,
@Query("limit") limit: Int = 100
): Optional<PagingObjectPlaylistTrack>
@GET("tracks/{id}")
suspend fun getTrack(@Path("id") trackId: String?): Optional<Track>
@GET("episodes/{id}")
suspend fun getEpisode(@Path("id") episodeId: String?): Optional<Track>
@GET("shows/{id}")
suspend fun getShow(@Path("id") showId: String?): Optional<Track>
@GET("albums/{id}")
suspend fun getAlbum(@Path("id") albumId: String?): Optional<Album>
}
interface SpotifyServiceTokenRequest{
@POST("api/token")
@FormUrlEncoded
suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"): Optional<Token>
}

View File

@ -0,0 +1,49 @@
/*
* 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.networking
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"
interface YoutubeMusicApi {
@Headers("Content-Type: application/json", "Referer: https://music.youtube.com/search")
@POST("search?alt=json&key=$apiKey")
fun getYoutubeMusicResponse(@Body text: String): 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
}

View File

@ -1,10 +0,0 @@
package com.shabinder.spotiflyer.tracklist
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
@Composable
fun TrackList(link: String,navController: NavController, modifier: Modifier = Modifier){
}

View File

@ -0,0 +1,33 @@
/*
* 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.ui.base
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.models.TrackDetails
abstract class TrackListViewModel:ViewModel() {
abstract var folderType:String
abstract var subFolder:String
open val trackList = MutableLiveData<MutableList<TrackDetails>>()
private val loading = "Loading!"
open var title = MutableLiveData<String>().apply { value = loading }
open var coverUrl = MutableLiveData<String>()
}

View File

@ -1,4 +1,4 @@
package com.shabinder.spotiflyer.home package com.shabinder.spotiflyer.ui.home
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*

View File

@ -1,7 +1,5 @@
package com.shabinder.spotiflyer.home package com.shabinder.spotiflyer.ui.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View File

@ -0,0 +1,8 @@
package com.shabinder.spotiflyer.ui.platforms.gaana
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
@Composable
fun Gaana(link: String, navController: NavController,) {
}

View File

@ -0,0 +1,204 @@
/*
* 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.ui.platforms.gaana
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.gaana.GaanaTrack
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.ui.base.TrackListViewModel
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.queryActiveTracks
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class GaanaViewModel @ViewModelInject constructor(
private val databaseDAO: DatabaseDAO,
private val gaanaInterface : GaanaInterface
) : TrackListViewModel(){
override var folderType:String = ""
override var subFolder:String = ""
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
fun gaanaSearch(type:String,link:String){
viewModelScope.launch {
when (type) {
"song" -> {
gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
folderType = "Tracks"
subFolder = ""
if (File(
finalOutputDir(
it.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded
}
trackList.value = listOf(it).toTrackDetailsList(folderType, subFolder)
title.value = it.track_title
coverUrl.value = it.artworkLink
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = title.value!!,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value!!,
totalFiles = 1,
)
)
}
}
}
"album" -> {
gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
folderType = "Albums"
subFolder = link
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
title.value = link
coverUrl.value = it.custom_artworks.size_480p
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Album",
name = title.value!!,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0,
)
)
}
}
}
"playlist" -> {
gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
folderType = "Playlists"
subFolder = link
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
title.value = link
//coverUrl.value = "TODO"
coverUrl.value = gaanaPlaceholderImageUrl
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Playlist",
name = title.value.toString(),
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(),
totalFiles = it.tracks.size,
)
)
}
}
}
"artist" -> {
folderType = "Artist"
subFolder = link
val artistDetails =
gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()
?.also {
title.value = it.name
coverUrl.value = it.artworkLink
}
gaanaInterface.getGaanaArtistTracks(seokey = link).value?.also {
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Artist",
name = artistDetails?.name ?: link,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0,
)
)
}
}
}
}
queryActiveTracks()
}
}
private fun List<GaanaTrack>.toTrackDetailsList(type:String , subFolder:String) = this.map {
TrackDetails(
title = it.track_title,
artists = it.artist.map { artist -> artist?.name.toString() },
durationSec = it.duration,
albumArt = File(
imageDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"),
albumName = it.album_title,
year = it.release_date,
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
trackUrl = it.lyrics_url,
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana,
albumArtURL = it.artworkLink,
outputFile = finalOutputDir(it.track_title,type, subFolder,".m4a")
)
}.toMutableList()
}

View File

@ -0,0 +1,8 @@
package com.shabinder.spotiflyer.ui.platforms.spotify
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
@Composable
fun Spotify(link: String, navController: NavController,) {
}

View File

@ -0,0 +1,209 @@
/*
* 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.ui.platforms.spotify
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Album
import com.shabinder.spotiflyer.models.spotify.Image
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.models.spotify.Track
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.ui.base.TrackListViewModel
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.log
import com.shabinder.spotiflyer.utils.queryActiveTracks
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class SpotifyViewModel @ViewModelInject constructor(
private val databaseDAO: DatabaseDAO,
private val gaanaInterface : GaanaInterface
) : TrackListViewModel(){
override var folderType:String = ""
override var subFolder:String = ""
var spotifyService : SpotifyService? = null
fun resolveLink(url:String):String {
val response = gaanaInterface.getResponse(url).execute().body()?.string().toString()
val regex = """https://open\.spotify\.com.+\w""".toRegex()
return regex.find(response)?.value.toString()
}
fun spotifySearch(type:String,link: String){
viewModelScope.launch {
when (type) {
"track" -> {
spotifyService?.getTrack(link)?.value?.also {
folderType = "Tracks"
subFolder = ""
if (File(
finalOutputDir(
it.name.toString(),
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded
}
trackList.value = listOf(it).toTrackDetailsList(folderType, subFolder)
title.value = it.name
coverUrl.value = it.album?.images?.elementAtOrNull(1)?.url
?: it.album?.images?.elementAtOrNull(0)?.url
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = title.value.toString(),
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(),
totalFiles = 1,
)
)
}
}
}
"album" -> {
val albumObject = spotifyService?.getAlbum(link)?.value
folderType = "Albums"
subFolder = albumObject?.name.toString()
albumObject?.tracks?.items?.forEach {
if (File(
finalOutputDir(
it.name.toString(),
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded
}
it.album = Album(
images = listOf(
Image(
url = albumObject.images?.elementAtOrNull(1)?.url
?: albumObject.images?.elementAtOrNull(0)?.url
)
)
)
}
trackList.value = albumObject?.tracks?.items?.toTrackDetailsList(folderType, subFolder)
title.value = albumObject?.name
coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url
?: albumObject?.images?.elementAtOrNull(0)?.url
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Album",
name = title.value.toString(),
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0,
)
)
}
}
"playlist" -> {
log("Spotify Service",spotifyService.toString())
val playlistObject = spotifyService?.getPlaylist(link)?.value
folderType = "Playlists"
subFolder = playlistObject?.name.toString()
val tempTrackList = mutableListOf<Track>()
log("Tracks Fetched", playlistObject?.tracks?.items?.size.toString())
playlistObject?.tracks?.items?.forEach {
it.track?.let { it1 ->
if (File(
finalOutputDir(
it1.name.toString(),
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it1.downloaded = DownloadStatus.Downloaded
}
tempTrackList.add(it1)
}
}
var moreTracksAvailable = !playlistObject?.tracks?.next.isNullOrBlank()
while (moreTracksAvailable) {
//Check For More Tracks If available
val moreTracks = spotifyService?.getPlaylistTracks(link, offset = tempTrackList.size)?.value
moreTracks?.items?.forEach {
it.track?.let { it1 -> tempTrackList.add(it1) }
}
moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
}
log("Total Tracks Fetched", tempTrackList.size.toString())
trackList.value = tempTrackList.toTrackDetailsList(folderType, subFolder)
title.value = playlistObject?.name
coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url
?: playlistObject?.images?.firstOrNull()?.url.toString()
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Playlist",
name = title.value.toString(),
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(),
totalFiles = tempTrackList.size,
)
)
}
}
"episode" -> {//TODO
}
"show" -> {//TODO
}
}
queryActiveTracks()
}
}
private fun List<Track>.toTrackDetailsList(type:String , subFolder:String) = this.map {
TrackDetails(
title = it.name.toString(),
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
durationSec = (it.duration_ms/1000).toInt(),
albumArt = File(
imageDir() + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
albumName = it.album?.name,
year = it.album?.release_date,
comment = "Genres:${it.album?.genres?.joinToString()}",
trackUrl = it.href,
downloaded = it.downloaded,
source = Source.Spotify,
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
outputFile = finalOutputDir(it.name.toString(),type, subFolder,".m4a")
)
}.toMutableList()
}

View File

@ -0,0 +1,9 @@
package com.shabinder.spotiflyer.ui.platforms.youtube
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
@Composable
fun Youtube(link: String, navController: NavController,) {
}

View File

@ -0,0 +1,163 @@
/*
* 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.ui.platforms.youtube
import android.annotation.SuppressLint
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.base.TrackListViewModel
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.imageDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class YoutubeViewModel @ViewModelInject constructor(
private val databaseDAO: DatabaseDAO,
private val ytDownloader: YoutubeDownloader
) : TrackListViewModel(){
/*
* YT Album Art Schema
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
* */
override var folderType = "YT_Downloads"
override var subFolder = ""
fun getYTPlaylist(searchId:String){
if(!isOnline())return
try{
viewModelScope.launch(Dispatchers.IO) {
log("YT Playlist",searchId)
val playlist = ytDownloader.getPlaylist(searchId)
val playlistDetails = playlist.details()
val name = playlistDetails.title()
subFolder = removeIllegalChars(name)
val videos = playlist.videos()
coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg")
title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
)
this@YoutubeViewModel.trackList.postValue(videos.map {
TrackDetails(
title = it.title(),
artists = listOf(it.author().toString()),
durationSec = it.lengthSeconds(),
albumArt = File(
imageDir() + it.videoId() + ".jpeg"
),
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
downloaded = if (File(
finalOutputDir(
itemName = it.title(),
type = folderType,
subFolder = subFolder
)).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
},
outputFile = finalOutputDir(it.title(),folderType, subFolder,".m4a"),
videoID = it.videoId()
)
}.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()}/hqdefault.jpg",
totalFiles = videos.size,
))
}
queryActiveTracks()
}
}catch (e:Exception){
showDialog("An Error Occurred While Processing!")
}
}
@SuppressLint("DefaultLocale")
fun getYTTrack(searchId:String) {
if(!isOnline())return
try{
viewModelScope.launch(Dispatchers.IO) {
log("YT Video",searchId)
val video = ytDownloader.getVideo(searchId)
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("YT View Model",detail.toString())
this@YoutubeViewModel.trackList.postValue(
listOf(
TrackDetails(
title = name,
artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds()?:0,
albumArt = File(imageDir(),"$searchId.jpeg"),
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
downloaded = if (File(
finalOutputDir(
itemName = name,
type = folderType,
subFolder = subFolder
)).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
},
outputFile = finalOutputDir(name,folderType, subFolder,".m4a"),
videoID = searchId
)
).toMutableList()
)
title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
)
withContext(Dispatchers.IO){
databaseDAO.insert(DownloadRecord(
type = "Track",
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
link = "https://www.youtube.com/watch?v=$searchId",
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1,
))
}
queryActiveTracks()
}
} catch (e:Exception){
showDialog("An Error Occurred While Processing!")
}
}
}

View File

@ -0,0 +1,14 @@
package com.shabinder.spotiflyer.ui.tracklist
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.viewModel
import androidx.navigation.NavController
/*
* UI for List of Tracks to be universally used.
* */
@Composable
fun TrackList(modifier: Modifier = Modifier){
}

View File

@ -6,6 +6,9 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import com.github.kiulian.downloader.model.YoutubeVideo
import com.github.kiulian.downloader.model.formats.Format
import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.spotiflyer.BuildConfig import com.shabinder.spotiflyer.BuildConfig
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
@ -26,7 +29,22 @@ fun MainActivity.requestStoragePermission() {
) )
} }
} }
fun YoutubeVideo.getData(): Format?{
return try {
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
findAudioWithQuality(AudioQuality.high)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
findAudioWithQuality(AudioQuality.low)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
log("YTDownloader", e.toString())
null
}
}
}
}
fun openPlatform(packageName:String, websiteAddress:String){ fun openPlatform(packageName:String, websiteAddress:String){
val manager: PackageManager = mainActivity.packageManager val manager: PackageManager = mainActivity.packageManager
try { try {

View File

@ -0,0 +1,63 @@
/*
* 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 okhttp3.Interceptor
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
const val NoInternetErrorCode = 222
class NetworkInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
log("Network Requesting",chain.request().url.toString())
return if (!isOnline()){
//No Internet Connection
showDialog()
//Lets Stop the Incoming Request and send Dummy Response
createEmptyResponse(chain,"No Internet Connection")
}else {
try{
val response = chain.proceed(chain.request())
val responseBody = response.body
val bodyString = responseBody?.string()
Response.Builder().run {
code(response.code) // code(200.300) = successful else = unsuccessful
body("{\"value\":${bodyString}}".toResponseBody(responseBody?.contentType())) // Whatever body
protocol(response.protocol)
message(response.message)
request(chain.request())
build()
}
}catch (e: java.net.SocketTimeoutException){
showDialog("Timeout!","Please Go Back and Try Again")
createEmptyResponse(chain,"Timeout!, Slow Internet Connection")
}
}
}
}
fun createEmptyResponse(chain: Interceptor.Chain, message:String = "Error") = Response.Builder().run {
code(NoInternetErrorCode) // code(200.300) = successful else = unsuccessful
body("{}".toResponseBody(null)) // Empty Object
protocol(Protocol.HTTP_2)
message(message)
request(chain.request())
build()
}

View File

@ -0,0 +1,125 @@
package com.shabinder.spotiflyer.utils
import android.content.Context
import android.os.Environment
import android.util.Base64
import androidx.lifecycle.ViewModelProvider
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.App
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecordDatabase
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.File
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object Provider {
//Default Directory to save Media in their Own Categorized Folders
@Suppress("DEPRECATION")// We Do Have Media Access (But Just Media in Media Directory,Not Anything Else)
val defaultDir = Environment.getExternalStorageDirectory().toString() + File.separator +
Environment.DIRECTORY_MUSIC + File.separator +
"SpotiFlyer"+ File.separator
//Default Cache Directory to save Album Art to use them for writing in Media Later
fun imageDir(ctx: Context = mainActivity): String = ctx
.externalCacheDir?.absolutePath + File.separator +
".Images" + File.separator
@Provides
@Singleton
fun databaseDAO(@ApplicationContext appContext: Context): DatabaseDAO {
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
}
@Provides
@Singleton
fun getYTDownloader(): YoutubeDownloader {
return YoutubeDownloader()
}
@Provides
@Singleton
fun getMoshi(): Moshi {
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
@Provides
@Singleton
fun getSpotifyTokenInterface(moshi: Moshi): SpotifyServiceTokenRequest {
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
val request: Request =
chain.request().newBuilder()
.addHeader(
"Authorization",
"Basic ${
Base64.encodeToString(
"${App.clientId}:${App.clientSecret}".toByteArray(),
Base64.NO_WRAP
)
}"
).build()
chain.proceed(request)
}).addInterceptor(NetworkInterceptor())
val retrofit = Retrofit.Builder()
.baseUrl("https://accounts.spotify.com/")
.client(httpClient2.build())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(SpotifyServiceTokenRequest::class.java)
}
@Provides
@Singleton
fun okHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(NetworkInterceptor())
.build()
}
@Provides
@Singleton
fun getGaanaInterface(moshi: Moshi, okHttpClient: OkHttpClient): GaanaInterface {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.gaana.com/")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(GaanaInterface::class.java)
}
@Provides
@Singleton
fun getYoutubeMusicApi(moshi: Moshi): YoutubeMusicApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://music.youtube.com/youtubei/v1/")
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(YoutubeMusicApi::class.java)
}
}

View File

@ -1,6 +1,160 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.worker.ForegroundService
import java.io.File
/**
* mainActivity Instance to use whereEver Needed , as Its God Activity.
* (i.e, almost Active Throughout App's Lifecycle )
*/
val mainActivity val mainActivity
get() = MainActivity.getInstance() get() = MainActivity.getInstance()
fun loadAllImages(context: Context? = mainActivity, images:List<String>? = null,source: Source) {
val serviceIntent = Intent(context, ForegroundService::class.java)
images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList<String>) }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
fun downloadTracks(
trackList: ArrayList<TrackDetails>,
context: Context? = mainActivity
) {
if(!trackList.isNullOrEmpty()){
val serviceIntent = Intent(context, ForegroundService::class.java)
serviceIntent.putParcelableArrayListExtra("object",trackList)
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
}
fun queryActiveTracks(context:Context? = mainActivity) {
val serviceIntent = Intent(context, ForegroundService::class.java).apply {
action = "query"
}
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
fun finalOutputDir(itemName:String ,type:String, subFolder:String,extension:String = ".mp3"): String{
return Provider.defaultDir + removeIllegalChars(type) + File.separator +
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + File.separator} +
removeIllegalChars(itemName) + extension
}
/**
* Util. Function To Check Connection Status
* */
@Suppress("DEPRECATION")
fun isOnline(): Boolean {
var result = false
val connectivityManager =
mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
}
} else {
val netInfo =
(mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
result = netInfo != null && netInfo.isConnected
}
}
return result
}
fun showDialog(title:String? = null, message: String? = null,response: String = "Ok"){
//TODO
Toast.makeText(mainActivity,title ?: "No Internet",Toast.LENGTH_SHORT).show()
}
/**
*Extension Function For Copying Files!
**/
fun File.copyTo(file: File) {
inputStream().use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
fun createDirectory(dir:String){
val yourAppDir = File(dir)
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
{ // create empty directory
if (yourAppDir.mkdirs())
{log("CreateDir","$dir created")}
else
{
Log.w("CreateDir","Unable to create Dir: $dir!")}
}
else
{log("CreateDir","$dir already exists")}
}
/**
* Removing Illegal Chars from File Name
* **/
fun removeIllegalChars(fileName: String): String {
val illegalCharArray = charArrayOf(
'/',
'\n',
'\r',
'\t',
'\u0000',
'\u000C',
'`',
'?',
'*',
'\\',
'<',
'>',
'|',
'\"',
'.',
'-',
'\''
)
var name = fileName
for (c in illegalCharArray) {
name = fileName.replace(c, '_')
}
name = name.replace("\\s".toRegex(), "_")
name = name.replace("\\)".toRegex(), "")
name = name.replace("\\(".toRegex(), "")
name = name.replace("\\[".toRegex(), "")
name = name.replace("]".toRegex(), "")
name = name.replace("\\.".toRegex(), "")
name = name.replace("\"".toRegex(), "")
name = name.replace("\'".toRegex(), "")
name = name.replace(":".toRegex(), "")
name = name.replace("\\|".toRegex(), "")
return name
}
fun createDirectories() {
createDirectory(Provider.defaultDir)
createDirectory(Provider.imageDir())
createDirectory(Provider.defaultDir + "Tracks/")
createDirectory(Provider.defaultDir + "Albums/")
createDirectory(Provider.defaultDir + "Playlists/")
createDirectory(Provider.defaultDir + "YT_Downloads/")
}

View File

@ -0,0 +1,694 @@
/*
* 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.worker
import android.annotation.SuppressLint
import android.app.*
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.*
import androidx.annotation.RequiresApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Cancel
import androidx.compose.material.icons.rounded.CloudDownload
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import com.arthenica.mobileffmpeg.Config
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
import com.arthenica.mobileffmpeg.FFmpeg
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.kiulian.downloader.YoutubeDownloader
import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.downloadHelper.getYTTracks
import com.shabinder.spotiflyer.downloadHelper.sortByBestMatch
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File
import java.io.IOException
import java.util.*
import javax.inject.Inject
@AndroidEntryPoint
class ForegroundService : Service(){
private val tag = "Foreground Service"
private val channelId = "ForegroundDownloaderService"
private val notificationId = 101
private var total = 0 //Total Downloads Requested
private var converted = 0//Total Files Converted
private var downloaded = 0//Total Files downloaded
private var failed = 0//Total Files failed
private val isFinished: Boolean
get() = converted + failed == total
private var isSingleDownload: Boolean = false
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private val requestMap = hashMapOf<Request, TrackDetails>()
private val allTracksStatus = hashMapOf<String,DownloadStatus>()
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
private var messageList = mutableListOf("", "", "", "","")
private val imageDir:String
get() = imageDir(this)
private lateinit var cancelIntent:PendingIntent
private lateinit var fetch:Fetch
private lateinit var downloadManager : DownloadManager
@Inject lateinit var ytDownloader: YoutubeDownloader
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId,"Downloader Service")
}
val intent = Intent(
this,
ForegroundService::class.java
).apply{action = "kill"}
cancelIntent = PendingIntent.getService (this, 0 , intent , PendingIntent.FLAG_CANCEL_CURRENT )
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
initialiseFetch()
}
@SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Send a notification that service is started
log(tag, "Service Started.")
startForeground(notificationId, getNotification())
intent?.let{
when (it.action) {
"kill" -> killService()
"query" -> {
val response = Intent().apply {
action = "query_result"
putExtra("tracks", allTracksStatus)
}
sendBroadcast(response)
}
}
val downloadObjects: ArrayList<TrackDetails>? = (it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
"object"
))
val imagesList: ArrayList<String>? = (it.getStringArrayListExtra("imagesList") ?: it.extras?.getStringArrayList(
"imagesList"
))
imagesList?.let{ imageList ->
serviceScope.launch {
downloadAllImages(imageList)
}
}
downloadObjects?.let { list ->
downloadObjects.size.let { size ->
total += size
isSingleDownload = (size == 1)
}
updateNotification()
downloadAllTracks(list)
}
}
//Wake locks and misc tasks from here :
return if (isServiceStarted){
//Service Already Started
START_STICKY
} else{
log(tag, "Starting the foreground service task")
isServiceStarted = true
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
acquire()
}
}
START_STICKY
}
}
/**
* Function To Download All Tracks Available in a List
**/
private fun downloadAllTracks(trackList: List<TrackDetails>) {
trackList.forEach {
serviceScope.launch {
if (it.downloaded == DownloadStatus.Downloaded) {//Download Already Present!!
} else {
allTracksStatus[it.title] = DownloadStatus.Queued
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
downloadTrack(it.videoID!!, it)
} else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
youtubeMusicApi.getYoutubeMusicResponse(jsonBody).enqueue(
object : Callback<String> {
override fun onResponse(
call: Call<String>,
response: Response<String>
) {
serviceScope.launch {
val videoId = sortByBestMatch(
getYTTracks(response.body().toString()),
trackName = it.title,
trackArtists = it.artists,
trackDurationSec = it.durationSec
).keys.firstOrNull()
log("Service VideoID", videoId ?: "Not Found")
if (videoId.isNullOrBlank()) {
sendTrackBroadcast(Status.FAILED.name, it)
failed++
updateNotification()
allTracksStatus[it.title] = DownloadStatus.Failed
} else {//Found Youtube Video ID
downloadTrack(videoId, it)
}
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
if (t.message.toString()
.contains("Failed to connect")
) showDialog("Failed, Check Your Internet Connection!")
log("YT API Req. Fail", t.message.toString())
}
}
)
}
}
}
}
}
fun downloadTrack(videoID:String,track: TrackDetails){
serviceScope.launch(Dispatchers.IO) {
try {
val audioData = ytDownloader.getVideo(videoID).getData()
audioData?.let {
val url: String = it.url()
log("DHelper Link Found", url)
val request= Request(url, track.outputFile).apply{
priority = Priority.NORMAL
networkType = NetworkType.ALL
}
fetch.enqueue(request,
{ request1 ->
requestMap[request1] = track
log(tag, "Enqueuing Download")
},
{ error ->
log(tag, "Enqueuing Error:${error.throwable.toString()}")
}
)
}
}catch (e: java.lang.Exception){
log("Service YT Error", e.message.toString())
}
}
}
/**
* Fetch Listener/ Responsible for Fetch Behaviour
**/
private var fetchListener: FetchListener = object : FetchListener {
override fun onQueued(
download: Download,
waitingOnNetwork: Boolean
) {
requestMap[download.request]?.let { sendTrackBroadcast(Status.QUEUED.name, it) }
}
override fun onRemoved(download: Download) {
// TODO("Not yet implemented")
}
override fun onResumed(download: Download) {
// TODO("Not yet implemented")
}
override fun onStarted(
download: Download,
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int
) {
serviceScope.launch {
val track = requestMap[download.request]
addToNotification("Downloading ${track?.title}")
log(tag, "${track?.title} Download Started")
track?.let{
allTracksStatus[it.title] = DownloadStatus.Downloading
sendTrackBroadcast(Status.DOWNLOADING.name,track)
}
}
}
override fun onWaitingNetwork(download: Download) {
// TODO("Not yet implemented")
}
override fun onAdded(download: Download) {
// TODO("Not yet implemented")
}
override fun onCancelled(download: Download) {
// TODO("Not yet implemented")
}
override fun onCompleted(download: Download) {
serviceScope.launch {
val track = requestMap[download.request]
removeFromNotification("Downloading ${track?.title}")
try{
track?.let {
convertToMp3(download.file, it)
allTracksStatus[it.title] = DownloadStatus.Converting
}
log(tag, "${track?.title} Download Completed")
}catch (
e: KotlinNullPointerException
){
log(tag, "${track?.title} Download Failed! Error:Fetch!!!!")
log(tag, "${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url, download.request.file, track!!)
downloaded++
requestMap.remove(download.request)
}
}
}
override fun onDeleted(download: Download) {
// TODO("Not yet implemented")
}
override fun onDownloadBlockUpdated(
download: Download,
downloadBlock: DownloadBlock,
totalBlocks: Int
) {
// TODO("Not yet implemented")
}
override fun onError(download: Download, error: Error, throwable: Throwable?) {
serviceScope.launch {
val track = requestMap[download.request]
downloaded++
log(tag, download.error.throwable.toString())
log(tag, "${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url, download.request.file, track!!)
requestMap.remove(download.request)
removeFromNotification("Downloading ${track.title}")
}
updateNotification()
}
override fun onPaused(download: Download) {
// TODO("Not yet implemented")
}
override fun onProgress(
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long
) {
serviceScope.launch {
val track = requestMap[download.request]
log(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec")
val intent = Intent().apply {
action = "Progress"
putExtra("progress", download.progress)
putExtra("track", requestMap[download.request])
}
sendBroadcast(intent)
}
}
}
/**
* If fetch Fails , Android Download Manager To RESCUE!!
**/
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
serviceScope.launch {
val uri = Uri.parse(url)
val request = DownloadManager.Request(uri).apply {
setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE
)
setAllowedOverRoaming(false)
setTitle(track.title)
setDescription("Spotify Downloader Working Up here...")
setDestinationUri(File(outputDir).toUri())
setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
}
//Start Download
val downloadID = downloadManager.enqueue(request)
log("DownloadManager", "Download Request Sent")
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//Fetching the download id received with the broadcast
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
//Checking if the received broadcast is for our enqueued download by matching download id
if (downloadID == id) {
allTracksStatus[track.title] = DownloadStatus.Converting
convertToMp3(outputDir, track)
converted++
//Unregister this broadcast Receiver
this@ForegroundService.unregisterReceiver(this)
}
}
}
registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
}
/**
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
**/
fun convertToMp3(filePath: String, track: TrackDetails){
serviceScope.launch {
sendTrackBroadcast("Converting",track)
val m4aFile = File(filePath)
addToNotification("Processing ${track.title}")
FFmpeg.executeAsync(
"-i $filePath -y -b:a 160k -acodec libmp3lame -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
) { _, returnCode ->
when (returnCode) {
RETURN_CODE_SUCCESS -> {
log(Config.TAG, "Async command execution completed successfully.")
removeFromNotification("Processing ${track.title}")
m4aFile.delete()
writeMp3Tags(filePath.substringBeforeLast('.') + ".mp3", track)
//FFMPEG task Completed
}
RETURN_CODE_CANCEL -> {
log(Config.TAG, "Async command execution cancelled by user.")
}
else -> {
log(
Config.TAG, String.format(
"Async command execution failed with rc=%d.",
returnCode
)
)
}
}
}
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private fun writeMp3Tags(filePath: String, track: TrackDetails){
serviceScope.launch {
var mp3File = Mp3File(filePath)
mp3File = removeAllTags(mp3File)
mp3File = setId3v1Tags(mp3File, track)
mp3File = setId3v2Tags(mp3File, track,this@ForegroundService)
log("Mp3Tags", "saving file")
mp3File.save(filePath.substringBeforeLast('.') + ".new.mp3")
val file = File(filePath)
file.delete()
val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
newFile.renameTo(file)
converted++
updateNotification()
addToLibrary(file.absolutePath)
allTracksStatus.remove(track.title)
//Notify Download Completed
sendTrackBroadcast("track_download_completed",track)
//All tasks completed (REST IN PEACE)
if(isFinished && !isSingleDownload){
delay(5000)
onDestroy()
}
}
}
/**
* This is the method that can be called to update the Notification
*/
private fun updateNotification() {
val mNotificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.notify(notificationId, getNotification())
}
private fun releaseWakeLock() {
log(tag, "Releasing Wake Lock")
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
} catch (e: Exception) {
log(tag, "Service stopped without being started: ${e.message}")
}
isServiceStarted = false
}
@Suppress("SameParameterValue")
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String){
val channel = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(channel)
}
/**
* Cleaning All Residual Files except Mp3 Files
**/
private fun cleanFiles(dir: File) {
log(tag, "Starting Cleaning in ${dir.path} ")
val fList = dir.listFiles()
fList?.let {
for (file in fList) {
if (file.isDirectory) {
cleanFiles(file)
} else if(file.isFile) {
if(file.path.toString().substringAfterLast(".") != "mp3"){
log(tag, "Cleaning ${file.path}")
file.delete()
}
}
}
}
}
/*
* Add File to Android's Media Library.
* */
private fun addToLibrary(path:String) {
log(tag,"Scanning File")
MediaScannerConnection.scanFile(this,
listOf(path).toTypedArray(), null,null)
}
/**
* Function to fetch all Images for use in mp3 tags.
**/
fun downloadAllImages(urlList: ArrayList<String>, func: ((resource:File) -> Unit)? = null) {
/*
* Last Element of this List defines Its Source
* */
val source = urlList.last()
for (url in urlList.subList(0, urlList.size - 2)) {
val imgUri = url.toUri().buildUpon().scheme("https").build()
Glide
.with(this@ForegroundService)
.asFile()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.load(imgUri)
.listener(object : RequestListener<File> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File>?,
isFirstResource: Boolean
): Boolean {
log("Glide", "LoadFailed")
return false
}
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
try {
serviceScope.launch {
val file = when (source) {
Source.Spotify.name -> {
File(imageDir, url.substringAfterLast('/') + ".jpeg")
}
Source.YouTube.name -> {
File(
imageDir,
url.substringBeforeLast('/', url)
.substringAfterLast(
'/',
url
) + ".jpeg"
)
}
Source.Gaana.name -> {
File(
imageDir,
(url.substringBeforeLast('/').substringAfterLast(
'/'
)) + ".jpeg"
)
}
else -> File(
imageDir,
url.substringAfterLast('/') + ".jpeg"
)
}
resource?.copyTo(file)
func?.let { it(file) }
}
} catch (e: IOException) {
e.printStackTrace()
}
return false
}
}).submit()
}
}
private fun killService() {
serviceScope.launch{
log(tag,"Killing Self")
messageList = mutableListOf("Cleaning And Exiting","","","","")
fetch.cancelAll()
fetch.removeAll()
updateNotification()
cleanFiles(File(defaultDir))
cleanFiles(File(imageDir))
messageList = mutableListOf("","","","","")
releaseWakeLock()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true)
} else {
stopSelf()//System will automatically close it
}
}
}
override fun onDestroy() {
super.onDestroy()
if(isFinished){
killService()
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if(isFinished){
killService()
}
}
private fun initialiseFetch() {
val fetchConfiguration =
FetchConfiguration.Builder(this).run {
setNamespace(channelId)
setDownloadConcurrentLimit(4)
build()
}
fetch = Fetch.run {
setDefaultInstanceConfiguration(fetchConfiguration)
getDefaultInstance()
}.apply {
addListener(fetchListener)
removeAll() //Starting fresh
}
}
private fun getNotification():Notification = NotificationCompat.Builder(this, channelId).run {
setSmallIcon(R.drawable.ic_download_arrow)
setContentTitle("Total: $total Completed:$converted Failed:$failed")
setNotificationSilent()
setStyle(
NotificationCompat.InboxStyle().run {
addLine(messageList[messageList.size - 1])
addLine(messageList[messageList.size - 2])
addLine(messageList[messageList.size - 3])
addLine(messageList[messageList.size - 4])
addLine(messageList[messageList.size - 5])
}
)
addAction(R.drawable.ic_round_cancel_24,"Exit",cancelIntent)
build()
}
private fun addToNotification(message:String){
messageList.add(message)
updateNotification()
}
private fun removeFromNotification(message: String){
messageList.remove(message)
updateNotification()
}
fun sendTrackBroadcast(action:String,track:TrackDetails){
val intent = Intent().apply{
setAction(action)
putExtra("track", track)
}
this@ForegroundService.sendBroadcast(intent)
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.worker
import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.log
import java.io.FileInputStream
/**
*Modifying Mp3 com.shabinder.spotiflyer.models.gaana.Tags with MetaData!
**/
fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File {
val id3v1Tag = ID3v1Tag().apply {
artist = track.artists.joinToString(",")
title = track.title
album = track.albumName
year = track.year
comment = "Genres:${track.comment}"
}
mp3File.id3v1Tag = id3v1Tag
return mp3File
}
fun setId3v2Tags(mp3file: Mp3File, track: TrackDetails,service: ForegroundService): Mp3File {
val id3v2Tag = ID3v24Tag().apply {
artist = track.artists.joinToString(",")
title = track.title
album = track.albumName
year = track.year
comment = "Genres:${track.comment}"
lyrics = "Gonna Implement Soon"
url = track.trackUrl
}
try{
val bytesArray = ByteArray(track.albumArt.length().toInt())
val fis = FileInputStream(track.albumArt)
fis.read(bytesArray) //read file into bytes[]
fis.close()
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
}catch (e: java.io.FileNotFoundException){
try {
//Image Still Not Downloaded!
//Lets Download Now and Write it into Album Art
service.downloadAllImages(arrayListOf(track.albumArtURL, track.source.name)){
val bytesArray = ByteArray(it.length().toInt())
val fis = FileInputStream(it)
fis.read(bytesArray) //read file into bytes[]
fis.close()
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
}
}catch (e: Exception){log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")}
}
mp3file.id3v2Tag = id3v2Tag
return mp3file
}
fun removeAllTags(mp3file: Mp3File): Mp3File {
if (mp3file.hasId3v1Tag()) {
mp3file.removeId3v1Tag()
}
if (mp3file.hasId3v2Tag()) {
mp3file.removeId3v2Tag()
}
if (mp3file.hasCustomTag()) {
mp3file.removeCustomTag()
}
return mp3file
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM16.3,16.3c-0.39,0.39 -1.02,0.39 -1.41,0L12,13.41 9.11,16.3c-0.39,0.39 -1.02,0.39 -1.41,0 -0.39,-0.39 -0.39,-1.02 0,-1.41L10.59,12 7.7,9.11c-0.39,-0.39 -0.39,-1.02 0,-1.41 0.39,-0.39 1.02,-0.39 1.41,0L12,10.59l2.89,-2.89c0.39,-0.39 1.02,-0.39 1.41,0 0.39,0.39 0.39,1.02 0,1.41L13.41,12l2.89,2.89c0.38,0.38 0.38,1.02 0,1.41z"/>
</vector>

View File

@ -7,6 +7,8 @@ buildscript {
okhttp_version = "4.9.0" okhttp_version = "4.9.0"
coroutines_version = "1.4.2" coroutines_version = "1.4.2"
coil_version = "0.4.1" coil_version = "0.4.1"
kotlin_version = "1.4.21"
hilt_version = '2.30.1-alpha'
} }
repositories { repositories {
@ -17,7 +19,10 @@ buildscript {
dependencies { dependencies {
classpath "com.android.tools.build:gradle:7.0.0-alpha03" classpath "com.android.tools.build:gradle:7.0.0-alpha03"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21"
//Hilt
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
//Kotlinx-Serialization
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }