diff --git a/app/build.gradle b/app/build.gradle index 11df113f..2f73bc55 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,10 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlin-parcelize' id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' + id 'kotlinx-serialization' } kapt { @@ -98,6 +101,15 @@ dependencies { implementation "androidx.room:room-runtime:$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 implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" @@ -106,11 +118,15 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' //Json + implementation 'com.beust:klaxon:5.4' implementation 'com.squareup.moshi:moshi: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-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 implementation "dev.chrisbanes.accompanist:accompanist-coil:$coil_version" diff --git a/app/libs/mobile-ffmpeg.aar b/app/libs/mobile-ffmpeg.aar new file mode 100644 index 00000000..ba7575f6 Binary files /dev/null and b/app/libs/mobile-ffmpeg.aar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 20a151a7..a1ffe0ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ . + */ + +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 . + */ + +@HiltAndroidApp +class App:Application(){ + companion object{ + const val clientId:String = "694d8bf4f6ec420fa66ea7fb4c68f89d" + const val clientSecret:String = "02ca2d4021a7452dae2328b47a6e8fe8" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 002147df..5bd2575d 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -16,18 +16,28 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.setContent import androidx.compose.ui.res.vectorResource 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.networking.SpotifyService import com.shabinder.spotiflyer.ui.ComposeLearnTheme import com.shabinder.spotiflyer.ui.appNameStyle import com.shabinder.spotiflyer.utils.requestStoragePermission +import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.accompanist.insets.ProvideWindowInsets import dev.chrisbanes.accompanist.insets.statusBarsHeight +import javax.inject.Inject +/* +* This is App's God Activity +* */ +@AndroidEntryPoint class MainActivity : AppCompatActivity() { + + private var spotifyService : SpotifyService? = null + override fun onCreate(savedInstanceState: Bundle?) { 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 WindowCompat.setDecorFitsSystemWindows(window, false) @@ -56,7 +66,9 @@ class MainActivity : AppCompatActivity() { companion object{ private lateinit var instance: MainActivity + private lateinit var sharedViewModel: SharedViewModel fun getInstance():MainActivity = this.instance + fun getSharedViewModel():SharedViewModel = this.sharedViewModel } init { @@ -100,6 +112,20 @@ fun AppBar( @Composable fun DefaultPreview() { 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() + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt new file mode 100644 index 00000000..9c5711e7 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt @@ -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 . + */ + +package com.shabinder.spotiflyer + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.shabinder.spotiflyer.networking.SpotifyService + +class SharedViewModel : ViewModel() { + var intentString = MutableLiveData() + var spotifyService = MutableLiveData() +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/database/DatabaseDAO.kt b/app/src/main/java/com/shabinder/spotiflyer/database/DatabaseDAO.kt new file mode 100644 index 00000000..f323cf01 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/database/DatabaseDAO.kt @@ -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 . + */ + +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 + + @Query("DELETE FROM download_record_table") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/database/DownloadRecord.kt b/app/src/main/java/com/shabinder/spotiflyer/database/DownloadRecord.kt new file mode 100644 index 00000000..0a734771 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/database/DownloadRecord.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/database/DownloadRecordDatabase.kt b/app/src/main/java/com/shabinder/spotiflyer/database/DownloadRecordDatabase.kt new file mode 100644 index 00000000..98d84a09 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/database/DownloadRecordDatabase.kt @@ -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 . + */ + +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 + } + + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YoutubeProvider.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YoutubeProvider.kt new file mode 100644 index 00000000..444d11df --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YoutubeProvider.kt @@ -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 . + */ + +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{ + val youtubeTracks = mutableListOf() + + val stringBuilder: StringBuilder = StringBuilder(response) + val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject + val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array("contents") + val resultBlocks = mutableListOf>() + 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("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("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() + + /* + 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("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, + trackName:String, + trackArtists:List, + trackDurationSec:Int, + ):Map{ + /* + * "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value + **/ + val linksWithMatchValue = mutableMapOf() + + 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() +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt new file mode 100644 index 00000000..14001ee9 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt @@ -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 . + */ + +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, + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Optional.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Optional.kt new file mode 100644 index 00000000..2257df5d --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Optional.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models + +import kotlinx.serialization.Serializable + +@Serializable +data class Optional(val value: T?) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt new file mode 100644 index 00000000..d82ab05c --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Artist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Artist.kt new file mode 100644 index 00000000..49c27d04 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Artist.kt @@ -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 . + */ + +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? +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/CustomArtworks.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/CustomArtworks.kt new file mode 100644 index 00000000..f73b5510 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/CustomArtworks.kt @@ -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 . + */ + +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, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaAlbum.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaAlbum.kt new file mode 100644 index 00000000..354e9e4d --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaAlbum.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaAlbum ( + val tracks : List, + val count : Int, + val custom_artworks : CustomArtworks, + val release_year : Int, + val favorite_count : Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistDetails.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistDetails.kt new file mode 100644 index 00000000..17d57ac3 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistDetails.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaArtistDetails( + val artist : List, + val count : Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistTracks.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistTracks.kt new file mode 100644 index 00000000..2f31fb01 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistTracks.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaArtistTracks( + val count : Int, + val tracks : List +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaPlaylist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaPlaylist.kt new file mode 100644 index 00000000..b64ca02e --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaPlaylist.kt @@ -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 . + */ + +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, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaSong.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaSong.kt new file mode 100644 index 00000000..8acbff96 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaSong.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaSong( + val tracks : List +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaTrack.kt new file mode 100644 index 00000000..f4a0b94d --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaTrack.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +import com.shabinder.spotiflyer.models.DownloadStatus +import com.squareup.moshi.Json + +data class GaanaTrack ( + val tags : List?, + 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, + @Json(name = "gener") val genre : List?, + 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 +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Genre.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Genre.kt new file mode 100644 index 00000000..0f4fcd21 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Genre.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class Genre ( + val genre_id : Int, + val name : String +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Tags.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Tags.kt new file mode 100644 index 00000000..c348a321 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Tags.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class Tags ( + val tag_id : Int, + val tag_name : String +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Album.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Album.kt new file mode 100644 index 00000000..b8eec7b9 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Album.kt @@ -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 . + */ + +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? = null, + var available_markets: List? = null, + var copyrights: List? = null, + var external_ids: Map? = null, + var external_urls: Map? = null, + var genres: List? = null, + var href: String? = null, + var id: String? = null, + var images: List? = 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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Artist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Artist.kt new file mode 100644 index 00000000..6b7d2050 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Artist.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.spotify + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Artist( + var external_urls: Map? = null, + var href: String? = null, + var id: String? = null, + var name: String? = null, + var type: String? = null, + var uri: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Copyright.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Copyright.kt new file mode 100644 index 00000000..ace8f1cc --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Copyright.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Episodes.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Episodes.kt new file mode 100644 index 00000000..cb502c13 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Episodes.kt @@ -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 . + */ + +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?, + var href:String?, + var id:String?, + var images:List?, + var is_externally_hosted:Boolean?, + var is_playable:Boolean?, + var language:String?, + var languages:List?, + var name:String?, + var release_date:String?, + var release_date_precision:String?, + var type:String?, + var uri:String +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Followers.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Followers.kt new file mode 100644 index 00000000..8a3ad1a0 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Followers.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Image.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Image.kt new file mode 100644 index 00000000..f2b1e355 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Image.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/LinkedTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/LinkedTrack.kt new file mode 100644 index 00000000..ce1745d3 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/LinkedTrack.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.spotify + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LinkedTrack( + var external_urls: Map? = null, + var href: String? = null, + var id: String? = null, + var type: String? = null, + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectPlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectPlaylistTrack.kt new file mode 100644 index 00000000..55c7b000 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectPlaylistTrack.kt @@ -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 . + */ + +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? = null, + var limit: Int = 0, + var next: String? = null, + var offset: Int = 0, + var previous: String? = null, + var total: Int = 0): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectTrack.kt new file mode 100644 index 00000000..617c9ead --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectTrack.kt @@ -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 . + */ + +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? = null, + var limit: Int = 0, + var next: String? = null, + var offset: Int = 0, + var previous: String? = null, + var total: Int = 0):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Playlist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Playlist.kt new file mode 100644 index 00000000..e60154e5 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Playlist.kt @@ -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 . + */ + +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? = null, + var followers: Followers? = null, + var href: String? = null, + var id: String? = null, + var images: List? = 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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PlaylistTrack.kt new file mode 100644 index 00000000..56ebc380 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PlaylistTrack.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Source.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Source.kt new file mode 100644 index 00000000..4c925217 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Source.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.spotify + +enum class Source { + Spotify, + YouTube, + Gaana, +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Token.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Token.kt new file mode 100644 index 00000000..0d8bdaf8 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Token.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Track.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Track.kt new file mode 100644 index 00000000..33700996 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Track.kt @@ -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 . + */ + +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? = null, + var available_markets: List? = 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? = 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? = null, + var popularity: Int? = null, + var downloaded: DownloadStatus = DownloadStatus.NotDownloaded +):Parcelable + diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPrivate.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPrivate.kt new file mode 100644 index 00000000..66b00f13 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPrivate.kt @@ -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 . + */ + +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? = null, + var followers: Followers? = null, + var href: String? = null, + var id: String? = null, + var images: List? = null, + var product:String, + var type: String? = null, + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPublic.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPublic.kt new file mode 100644 index 00000000..7d504c8b --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPublic.kt @@ -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 . + */ + +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? = null, + var followers: Followers? = null, + var href: String? = null, + var id: String? = null, + var images: List? = null, + var type: String? = null, + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/navigation/ComposeNavigation.kt b/app/src/main/java/com/shabinder/spotiflyer/navigation/ComposeNavigation.kt index ab50cf73..281b4bd0 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/navigation/ComposeNavigation.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/navigation/ComposeNavigation.kt @@ -6,8 +6,10 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.navArgument import androidx.navigation.compose.rememberNavController -import com.shabinder.spotiflyer.home.Home -import com.shabinder.spotiflyer.tracklist.TrackList +import com.shabinder.spotiflyer.ui.home.Home +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 fun ComposeNavigation() { @@ -22,13 +24,37 @@ fun ComposeNavigation() { Home(navController = navController) } - //Track list Screen + //Spotify Screen //Argument `link` = Link of Track/Album/Playlist composable( - "track_list/{link}", + "spotify/{link}", 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", navController = navController ) diff --git a/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt b/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt new file mode 100644 index 00000000..93d5c714 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt @@ -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 . + */ + +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 + + /* + * 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 + + /* + * 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 + + /* + * 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 + /* + * 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 + + /* + * Dynamic Url Requests + * */ + @GET + fun getResponse(@Url url:String): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/networking/SpotifyInterface.kt b/app/src/main/java/com/shabinder/spotiflyer/networking/SpotifyInterface.kt new file mode 100644 index 00000000..fcf469b5 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/networking/SpotifyInterface.kt @@ -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 . + */ + +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 + + @GET("playlists/{playlist_id}/tracks") + suspend fun getPlaylistTracks( + @Path("playlist_id") playlistId: String?, + @Query("offset") offset: Int = 0, + @Query("limit") limit: Int = 100 + ): Optional + + @GET("tracks/{id}") + suspend fun getTrack(@Path("id") trackId: String?): Optional + + @GET("episodes/{id}") + suspend fun getEpisode(@Path("id") episodeId: String?): Optional + + @GET("shows/{id}") + suspend fun getShow(@Path("id") showId: String?): Optional + + @GET("albums/{id}") + suspend fun getAlbum(@Path("id") albumId: String?): Optional +} + +interface SpotifyServiceTokenRequest{ + + @POST("api/token") + @FormUrlEncoded + suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"): Optional + +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/networking/YoutubeMusicApi.kt b/app/src/main/java/com/shabinder/spotiflyer/networking/YoutubeMusicApi.kt new file mode 100644 index 00000000..e321d19a --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/networking/YoutubeMusicApi.kt @@ -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 . + */ + +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 +} + +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 +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/tracklist/TrackList.kt b/app/src/main/java/com/shabinder/spotiflyer/tracklist/TrackList.kt deleted file mode 100644 index ec09f838..00000000 --- a/app/src/main/java/com/shabinder/spotiflyer/tracklist/TrackList.kt +++ /dev/null @@ -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){ - -} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/base/TrackListViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/base/TrackListViewModel.kt new file mode 100644 index 00000000..533b7bfe --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/base/TrackListViewModel.kt @@ -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 . + */ + +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>() + + private val loading = "Loading!" + open var title = MutableLiveData().apply { value = loading } + open var coverUrl = MutableLiveData() + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/home/Home.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/home/Home.kt similarity index 99% rename from app/src/main/java/com/shabinder/spotiflyer/home/Home.kt rename to app/src/main/java/com/shabinder/spotiflyer/ui/home/Home.kt index 888920f7..ef6ff397 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/home/Home.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/home/Home.kt @@ -1,4 +1,4 @@ -package com.shabinder.spotiflyer.home +package com.shabinder.spotiflyer.ui.home import androidx.compose.foundation.* import androidx.compose.foundation.layout.* diff --git a/app/src/main/java/com/shabinder/spotiflyer/home/HomeViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/home/HomeViewModel.kt similarity index 87% rename from app/src/main/java/com/shabinder/spotiflyer/home/HomeViewModel.kt rename to app/src/main/java/com/shabinder/spotiflyer/ui/home/HomeViewModel.kt index 4c8d855f..ea3a12da 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/home/HomeViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/home/HomeViewModel.kt @@ -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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/gaana/Gaana.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/gaana/Gaana.kt new file mode 100644 index 00000000..ed1f3a6e --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/gaana/Gaana.kt @@ -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,) { +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/gaana/GaanaViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/gaana/GaanaViewModel.kt new file mode 100644 index 00000000..bf8aeee8 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/gaana/GaanaViewModel.kt @@ -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 . + */ + +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.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() +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/spotify/Spotify.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/spotify/Spotify.kt new file mode 100644 index 00000000..74205f9a --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/spotify/Spotify.kt @@ -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,) { +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/spotify/SpotifyViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/spotify/SpotifyViewModel.kt new file mode 100644 index 00000000..0cdeb694 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/spotify/SpotifyViewModel.kt @@ -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 . + */ + +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() + 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.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() +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/youtube/Youtube.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/youtube/Youtube.kt new file mode 100644 index 00000000..0ddaba00 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/youtube/Youtube.kt @@ -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,) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/youtube/YoutubeViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/youtube/YoutubeViewModel.kt new file mode 100644 index 00000000..92c41952 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/platforms/youtube/YoutubeViewModel.kt @@ -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 . + */ + +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!") + } + } +} + diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/tracklist/TrackList.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/tracklist/TrackList.kt new file mode 100644 index 00000000..392ca605 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/tracklist/TrackList.kt @@ -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){ + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt index 9360edba..d7e2bb33 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt @@ -6,6 +6,9 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build 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.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){ val manager: PackageManager = mainActivity.packageManager try { diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/NetworkInterceptor.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/NetworkInterceptor.kt new file mode 100644 index 00000000..03c271a5 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/NetworkInterceptor.kt @@ -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 . + */ + +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() +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt new file mode 100644 index 00000000..76ee63ec --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt index 9df7fb3e..8f206bf1 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt @@ -1,6 +1,160 @@ 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.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 - get() = MainActivity.getInstance() \ No newline at end of file + get() = MainActivity.getInstance() + +fun loadAllImages(context: Context? = mainActivity, images:List? = null,source: Source) { + val serviceIntent = Intent(context, ForegroundService::class.java) + images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList) } + context?.let { ContextCompat.startForegroundService(it, serviceIntent) } +} + +fun downloadTracks( + trackList: ArrayList, + 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/") +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt new file mode 100644 index 00000000..06d65101 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt @@ -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 . + */ + +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() + private val allTracksStatus = hashMapOf() + 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? = (it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList( + "object" + )) + val imagesList: ArrayList? = (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) { + 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 { + override fun onResponse( + call: Call, + response: Response + ) { + 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, 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, + 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, 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 { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + log("Glide", "LoadFailed") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/worker/ID3Tagging.kt b/app/src/main/java/com/shabinder/spotiflyer/worker/ID3Tagging.kt new file mode 100644 index 00000000..040dc13f --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/worker/ID3Tagging.kt @@ -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 . + */ + +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 +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download_arrow.xml b/app/src/main/res/drawable/ic_download_arrow.xml new file mode 100644 index 00000000..baca592a --- /dev/null +++ b/app/src/main/res/drawable/ic_download_arrow.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_cancel_24.xml b/app/src/main/res/drawable/ic_round_cancel_24.xml new file mode 100644 index 00000000..ce93e711 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_cancel_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/build.gradle b/build.gradle index ae12ba7e..13bf7ba7 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,8 @@ buildscript { okhttp_version = "4.9.0" coroutines_version = "1.4.2" coil_version = "0.4.1" + kotlin_version = "1.4.21" + hilt_version = '2.30.1-alpha' } repositories { @@ -17,7 +19,10 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:7.0.0-alpha03" 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 // in the individual module build.gradle files }