From 4dca0c4c3c8a78c4686c8036f274c4ed06e686ae Mon Sep 17 00:00:00 2001 From: kostas214pro Date: Wed, 22 Mar 2023 23:58:45 +0200 Subject: [PATCH] first commit The repository is moved to this one due to issues i was facing --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/compiler.xml | 6 + .idea/deploymentTargetDropDown.xml | 17 + .idea/gradle.xml | 19 + .idea/misc.xml | 10 + app/.gitignore | 1 + app/build.gradle | 83 + app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 39 + app/src/main/ic_launcher-playstore.png | Bin 0 -> 23537 bytes .../example/spotifydownloader/MainActivity.kt | 325 ++++ app/src/main/python/SpotifyApiCredentials.py | 2 + app/src/main/python/main.py | 121 ++ app/src/main/res/drawable/adaptiveicon.xml | 1730 +++++++++++++++++ app/src/main/res/drawable/floppydisk.xml | 21 + .../res/drawable/ic_baseline_download_24.xml | 5 + .../main/res/drawable/ic_baseline_east_24.xml | 5 + .../drawable/ic_baseline_insert_link_24.xml | 5 + .../main/res/drawable/ic_baseline_save_24.xml | 5 + .../res/drawable/ic_baseline_share_24.xml | 5 + .../res/drawable/ic_baseline_start_24.xml | 5 + .../res/drawable/ic_baseline_storage_24.xml | 5 + .../res/drawable/ic_launcher_background.xml | 170 ++ .../res/drawable/ic_launcher_foreground.xml | 51 + app/src/main/res/layout/activity_main.xml | 216 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2194 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 2127 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4266 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1494 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1344 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2686 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3028 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2956 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6091 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4723 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 4785 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 9512 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6475 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 6756 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 13742 bytes app/src/main/res/values-night/themes.xml | 16 + app/src/main/res/values/colors.xml | 10 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 14 + app/src/main/res/values/themes.xml | 17 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../spotifydownloader/ExampleUnitTest.kt | 17 + build.gradle | 7 + gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++ gradlew.bat | 89 + settings.gradle | 18 + 59 files changed, 3359 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/spotifydownloader/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/example/spotifydownloader/MainActivity.kt create mode 100644 app/src/main/python/SpotifyApiCredentials.py create mode 100644 app/src/main/python/main.py create mode 100644 app/src/main/res/drawable/adaptiveicon.xml create mode 100644 app/src/main/res/drawable/floppydisk.xml create mode 100644 app/src/main/res/drawable/ic_baseline_download_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_east_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_insert_link_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_save_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_share_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_start_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_storage_24.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/example/spotifydownloader/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..4d4fa2c --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..a2d7c21 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..54d5acd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..2070dd9 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'com.chaquo.python' +} + +android { + namespace 'com.example.spotifydownloader' + compileSdk 33 + + defaultConfig { + ndk { + abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" + } + python { + version "3.8" + + pip{ + install "spotipy" + install "youtube-search-python" + install "music-tag" + install "Pillow" + install "mutagen" + + + + } + } + applicationId "com.example.spotifydownloader" + minSdk 24 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + + viewBinding = true + + } + splits { + abi { + enable true + reset() + include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' + universalApk true + } + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + + + + //YoutubeDl + implementation 'com.github.yausername.youtubedl-android:library:-SNAPSHOT' + implementation 'com.github.yausername.youtubedl-android:ffmpeg:-SNAPSHOT' // Optional + implementation 'com.github.yausername.youtubedl-android:aria2c:-SNAPSHOT' // Optional +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/spotifydownloader/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/spotifydownloader/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b400c57 --- /dev/null +++ b/app/src/androidTest/java/com/example/spotifydownloader/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.spotifydownloader + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.spotifydownloader", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7384fbc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..a2d52d00354ed234bf314a5d6ff16e8845922c75 GIT binary patch literal 23537 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 qOc2E2F@O%zC() + var code: Int? + fun RadioButtonSelection(): Int { + var toBeReturned = 0 + if (binding.selection1.isChecked) { + toBeReturned = 1 + } else if (binding.selection2.isChecked) { + toBeReturned = 2 + } else if (binding.selection3.isChecked) { + toBeReturned = 3 + } else if (binding.selection4.isChecked) { + toBeReturned = 4 + } + return toBeReturned + + } + + binding.perms.setOnClickListener { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + 100 + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.data = Uri.parse("package:${applicationContext.packageName}") + startActivity(intent) + } + + + } + + binding.download2.setOnClickListener { + binding.progressBar.progress = 0 + + + val deviceInternetAccess = isDeviceOnline(this.applicationContext) + + + val saveDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) + + + val task1 = Thread { + val data = module.callAttr( + "songSearchSpotify", + binding.PlaylistLinkEditText.text.toString() + ).asList().toList() + songNames = data[0].asList() + SuccessCode = data[1].toInt() + binding.progressBar.max = songNames.size + println("Got Data") + + } + val task2 = Thread { + code = null + SuccessCode = null + runOnUiThread { + binding.perms.isEnabled = false + binding.PlaylistLinkTextBox.isEnabled = false + binding.download2.isEnabled = false + binding.selection1.isEnabled = false + binding.selection2.isEnabled = false + binding.selection3.isEnabled = false + binding.selection4.isEnabled = false + + } + + while (true) { + when (code) { + 1 -> { + runOnUiThread { + Toast.makeText( + this, + "Connection Error try again later", + Toast.LENGTH_SHORT + ).show() + binding.perms.isEnabled = true + binding.PlaylistLinkTextBox.isEnabled = true + binding.download2.isEnabled = true + binding.selection1.isEnabled = true + binding.selection2.isEnabled = true + binding.selection3.isEnabled = true + binding.selection4.isEnabled = true + + + } + break + } + 2 -> { + runOnUiThread { + Toast.makeText( + this, + "Connection Error try again later", + Toast.LENGTH_SHORT + ).show() + binding.perms.isEnabled = true + binding.PlaylistLinkTextBox.isEnabled = true + binding.download2.isEnabled = true + binding.selection1.isEnabled = true + binding.selection2.isEnabled = true + binding.selection3.isEnabled = true + binding.selection4.isEnabled = true + + + } + break + } + + 3 -> { + runOnUiThread { + Toast.makeText( + this, + "Storage permission not given", + Toast.LENGTH_SHORT + ).show() + binding.perms.isEnabled = true + binding.PlaylistLinkTextBox.isEnabled = true + binding.download2.isEnabled = true + binding.selection1.isEnabled = true + binding.selection2.isEnabled = true + binding.selection3.isEnabled = true + binding.selection4.isEnabled = true + + } + break + } + } + when (SuccessCode) { + 1 -> { + runOnUiThread { + Toast.makeText( + this, + "Connection Error try again later", + Toast.LENGTH_SHORT + ).show() + binding.perms.isEnabled = true + binding.PlaylistLinkTextBox.isEnabled = true + binding.download2.isEnabled = true + binding.selection1.isEnabled = true + binding.selection2.isEnabled = true + binding.selection3.isEnabled = true + binding.selection4.isEnabled = true + + } + break + } + 2 -> { + runOnUiThread { + Toast.makeText(this, "Invalid link", Toast.LENGTH_SHORT).show() + binding.perms.isEnabled = true + binding.PlaylistLinkTextBox.isEnabled = true + binding.download2.isEnabled = true + binding.selection1.isEnabled = true + binding.selection2.isEnabled = true + binding.selection3.isEnabled = true + binding.selection4.isEnabled = true + + } + break + } + } + if (!task1.isAlive) { + if (binding.progressBar.progress == binding.progressBar.max) { + println(binding.progressBar.progress) + println(binding.progressBar.max) + runOnUiThread { + Toast.makeText(this, "Finished", Toast.LENGTH_SHORT).show() + binding.perms.isEnabled = true + binding.PlaylistLinkTextBox.isEnabled = true + binding.download2.isEnabled = true + binding.selection1.isEnabled = true + binding.selection2.isEnabled = true + binding.selection3.isEnabled = true + binding.selection4.isEnabled = true + } + break + } + } + } + } + task1.start() + task2.start() + + GlobalScope.launch { + + println(SuccessCode) + var run = true + + while (run) { + val isRunning = task1.isAlive + if (!isRunning) { + run = false + + val executor = Executors.newFixedThreadPool(RadioButtonSelection()) + for (i in songNames) { + val worker = Runnable { + + + var title = module.callAttr("getDownloadPath", i).asList().toList() + println(title) + + var filename = title[0].toString() + var ytLInk = title[1].toString() + code = title[2].toInt() + + val youtubeDLDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), + "SpotifyDownloaderTest" + ) + val request = YoutubeDLRequest(ytLInk) + + + var fileLocation = "$youtubeDLDir/${filename}" + + request.addOption("--output", fileLocation) + request.addOption("--audio-format", "aac") + request.addOption("-x") + request.addOption("--audio-format", "mp3") + request.addOption("-R", "2") + request.addOption("--socket-timeout", "40") + + if (isDeviceOnline(applicationContext)) { + try { + YoutubeDL.getInstance().execute(request, + DownloadProgressCallback { fl: Float, l: Long, s: String? -> }) + code = module.callAttr("DownloadSongs", i, fileLocation) + .toInt() + binding.progressBar.incrementProgressBy(1) + + + } catch (e: YoutubeDLException) { + code = 3 + } + } else { + code = 1 + } + + if (code == 1 || code == 2 || code == 3) { + executor.shutdownNow() + println("1") + } + } + if (SuccessCode == 0 && deviceInternetAccess) { + executor.execute(worker) + } + } + } + } + } + } + } +} +fun isDeviceOnline(context: Context): Boolean { + val connManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkCapabilities = connManager.getNetworkCapabilities(connManager.activeNetwork) + if (networkCapabilities == null) { + println("Device Offline") + return false + } else { + return true + } + } else { + // below Marshmallow + val activeNetwork = connManager.activeNetworkInfo + if (activeNetwork?.isConnectedOrConnecting == true && activeNetwork.isAvailable) { + println("Device Online") + return true + } else { + println("Device Offline") + return false + } + } +} diff --git a/app/src/main/python/SpotifyApiCredentials.py b/app/src/main/python/SpotifyApiCredentials.py new file mode 100644 index 0000000..1c5f314 --- /dev/null +++ b/app/src/main/python/SpotifyApiCredentials.py @@ -0,0 +1,2 @@ +SPOTIPY_CLIENT_ID = "839761bd027c48d8bdfc23f0efcd4a11" +SPOTIPY_CLIENT_SECRET = "7a06c728f0ad41298754f159242fb914" \ No newline at end of file diff --git a/app/src/main/python/main.py b/app/src/main/python/main.py new file mode 100644 index 0000000..2b56fd4 --- /dev/null +++ b/app/src/main/python/main.py @@ -0,0 +1,121 @@ +import httpx +import music_tag +import requests +import spotipy +import string +import time +from httpx import ReadError +from requests.exceptions import ChunkedEncodingError +from spotipy.oauth2 import SpotifyClientCredentials +from youtubesearchpython import VideosSearch +import mutagen + +import SpotifyApiCredentials + +sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials( + client_id=SpotifyApiCredentials.SPOTIPY_CLIENT_ID, + client_secret=SpotifyApiCredentials.SPOTIPY_CLIENT_SECRET, +)) + +Failed = [] + + +def songSearchSpotify(playlistLink): + # Initialize Spotipy + # Store the songs of the playlist in a list + success = 0 + done = True + offset = 0 + songs = [] + try: + tracks = sp.playlist_items(playlist_id=playlistLink, offset=offset) + except requests.exceptions.ConnectionError: + success = 1 + print("Unable to connect to the internet") + except spotipy.exceptions.SpotifyException: + success = 2 + print("Invalid Link") + + if success == 0: + for key in tracks['items']: + songs.append(f"{key['track']['name']} {key['track']['artists'][0]['name']}") + while done: + if len(songs) == offset + 100: + try: + tracks = sp.playlist_items(playlist_id=playlistLink, offset=offset) + except requests.exceptions.ConnectionError: + success = 1 + print("unable to connect to the internet") + for key in tracks['items']: + songs.append(f"{key['track']['name']} {key['track']['artists'][0]['name']}") + offset += 100 + if len(songs) < offset + 100: + done = False + + return songs, success + + else: + print("unable to get songs") + return songs, success + + + + + +def getDownloadPath(songs): + + + try: + songSearch = VideosSearch(songs, limit=1).result() + songId = songSearch['result'][0]['id'] + songUrl= "https://youtu.be/"+songId + songTitle = songSearch['result'][0]['title'] + translation_table = str.maketrans('', '', string.punctuation) + safeString = songTitle.translate(translation_table) + fileName = safeString.replace(" ", "") + ".mp3" + except(httpx.ConnectError, ChunkedEncodingError, ReadError): + return fileName,songUrl,1 + return fileName,songUrl,0 + + +def DownloadSongs(songs,fileLocation): + + try: + songSearch = VideosSearch(songs, limit=1).result() + tracks = sp.search(songs) + ArtWorkURL = tracks['tracks']['items'][0]['album']['images'][0]['url'] + + songId = songSearch['result'][0]['id'] + songTitle = songSearch['result'][0]['title'] + + response = requests.get(ArtWorkURL) + + albumName = tracks['tracks']['items'][0]['album']['name'] + albumArtistName = tracks['tracks']['items'][0]['album']['artists'][0]['name'] + albumTrackCount = tracks['tracks']['items'][0]['album']['total_tracks'] + albumTrackNumber = tracks['tracks']['items'][0]['track_number'] + releaseDate = tracks['tracks']['items'][0]['album']['release_date'][0:4] + artistName = tracks['tracks']['items'][0]['artists'][0]['name'] + trackName = tracks['tracks']['items'][0]['name'] + + f = music_tag.load_file(fileLocation) + f['album'] = albumName + f['albumartist'] = albumArtistName + f['artist'] = artistName + f['artwork'] = response.content + f['totaltracks'] = albumTrackCount + f['tracknumber'] = albumTrackNumber + f['tracktitle'] = trackName + f['year'] = releaseDate + f.save() + print("finished") + return 0 + except (httpx.ConnectError, ChunkedEncodingError, ReadError): + print("ConnectionError") + return 1 + except (requests.exceptions.ConnectionError, mutagen.aac.AACError): + print("Internal server is down ") + return 2 + except (PermissionError): + print("No permissions") + return 3 diff --git a/app/src/main/res/drawable/adaptiveicon.xml b/app/src/main/res/drawable/adaptiveicon.xml new file mode 100644 index 0000000..4d3867c --- /dev/null +++ b/app/src/main/res/drawable/adaptiveicon.xml @@ -0,0 +1,1730 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/floppydisk.xml b/app/src/main/res/drawable/floppydisk.xml new file mode 100644 index 0000000..f72ddc4 --- /dev/null +++ b/app/src/main/res/drawable/floppydisk.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_download_24.xml b/app/src/main/res/drawable/ic_baseline_download_24.xml new file mode 100644 index 0000000..1fc8106 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_download_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_east_24.xml b/app/src/main/res/drawable/ic_baseline_east_24.xml new file mode 100644 index 0000000..426df1c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_east_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_insert_link_24.xml b/app/src/main/res/drawable/ic_baseline_insert_link_24.xml new file mode 100644 index 0000000..ff57e07 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_insert_link_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_save_24.xml b/app/src/main/res/drawable/ic_baseline_save_24.xml new file mode 100644 index 0000000..82070aa --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_save_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_share_24.xml b/app/src/main/res/drawable/ic_baseline_share_24.xml new file mode 100644 index 0000000..87cea78 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_share_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_start_24.xml b/app/src/main/res/drawable/ic_baseline_start_24.xml new file mode 100644 index 0000000..fc83aca --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_start_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_storage_24.xml b/app/src/main/res/drawable/ic_baseline_storage_24.xml new file mode 100644 index 0000000..c8faeb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_storage_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2f37eca --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b6c50f0 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,216 @@ + + + + + + + + +