diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5cccc364..aac7de73 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,8 @@ + + (R.id.content) - content.visibility = View.GONE + content.text = item?.content card.setOnClickListener { onItemClickListener.onItemClick(item!!) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/HistoryAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/HistoryAdapter.kt index 172c8203..7c2d3f0e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/HistoryAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/HistoryAdapter.kt @@ -21,6 +21,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.HistoryItem +import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView @@ -172,6 +173,16 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit checkedItems.clear() } + fun invertSelected(items: List?){ + val invertedList = mutableListOf() + items?.forEach { + if (!checkedItems.contains(it!!.id)) invertedList.add(it.id) + } + checkedItems.clear() + checkedItems.addAll(invertedList) + notifyDataSetChanged() + } + companion object { private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/HomeAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/HomeAdapter.kt index 11205185..2061e678 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/HomeAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/HomeAdapter.kt @@ -173,6 +173,16 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity) notifyDataSetChanged() } + fun invertSelected(items: List?){ + val invertedList = mutableListOf() + items?.forEach { + if (!checkedVideos.contains(it!!.url)) invertedList.add(it.url) + } + checkedVideos.clear() + checkedVideos.addAll(invertedList) + notifyDataSetChanged() + } + fun clearCheckedItems(){ checkedVideos.clear() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt index 9bce5544..c753f382 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt @@ -74,7 +74,7 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ } override fun areContentsTheSame(oldItem: CommandTemplate, newItem: CommandTemplate): Boolean { - return oldItem.content == newItem.content + return oldItem.title == newItem.title && oldItem.content == newItem.content } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/CookieDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/CookieDao.kt index 855e4902..46091c2b 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/CookieDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/CookieDao.kt @@ -13,11 +13,11 @@ interface CookieDao { @Query("SELECT * FROM cookies ORDER BY id DESC") fun getAllCookiesLiveData() : LiveData> - @Query("SELECT * FROM cookies WHERE url=:url") - fun checkIfExistsWithSameURL(url: String) : CookieItem + @Query("SELECT EXISTS(SELECT * FROM cookies WHERE url=:url LIMIT 1)") + fun checkIfExistsWithSameURL(url: String) : Boolean @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(item: CookieItem) + suspend fun insert(item: CookieItem) : Long @Query("DELETE FROM cookies") suspend fun deleteAll() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/CookieRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/CookieRepository.kt index bff6c098..1bc0b44a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/CookieRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/CookieRepository.kt @@ -12,12 +12,11 @@ class CookieRepository(private val cookieDao: CookieDao) { } - suspend fun insert(item: CookieItem){ - try{ - cookieDao.checkIfExistsWithSameURL(item.url) - }catch(e: Exception){ - cookieDao.insert(item) + suspend fun insert(item: CookieItem) : Long{ + if (! cookieDao.checkIfExistsWithSameURL(item.url)){ + return cookieDao.insert(item) } + return -1 } suspend fun delete(item: CookieItem){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt index 41aa1a1b..a80df524 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt @@ -6,26 +6,27 @@ import android.content.ClipboardManager import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase.OPEN_READONLY +import android.util.Log import android.webkit.CookieManager import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import com.deniscerri.ytdlnis.database.DBManager -import com.deniscerri.ytdlnis.database.models.CommandTemplateExport import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.ui.more.WebViewActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.io.File class CookieViewModel(private val application: Application) : AndroidViewModel(application) { private val repository: CookieRepository val items: LiveData> private val jsonFormat = Json { prettyPrint = true } + private val cookieHeader = "# Netscape HTTP Cookie File\n" + + "# WebView Generated by the YTDLnis app\n" init { val dao = DBManager.getInstance(application).cookieDao @@ -37,12 +38,13 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a return repository.getAll() } - fun insert(item: CookieItem) = viewModelScope.launch(Dispatchers.IO) { - repository.insert(item) + suspend fun insert(item: CookieItem) : Long { + return repository.insert(item) } fun delete(item: CookieItem) = viewModelScope.launch(Dispatchers.IO) { repository.delete(item) + updateCookiesFile() } fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { @@ -106,20 +108,53 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a close() } close() - val header = "# Netscape HTTP Cookie File\n" + - "# WebView Generated by the YTDLnis app\n" - cookieList.fold(StringBuilder(header)) { acc, cookie -> + cookieList.fold(StringBuilder("")) { acc, cookie -> acc.append(cookie.toNetscapeFormat()).append("\n") }.toString() } } + fun updateCookiesFile() = viewModelScope.launch(Dispatchers.IO) { + val cookies = repository.getAll() + val cookieTXT = StringBuilder(cookieHeader) + val cookieFile = File(application.cacheDir, "cookies.txt") + if (cookies.isEmpty()) cookieFile.apply { writeText("") } + cookies.forEach { + it.content.lines().forEach {line -> + if (! cookieTXT.contains(line)) cookieTXT.append(it.content) + } + } + cookieFile.apply { writeText(cookieTXT.toString()) } + } + + suspend fun importFromClipboard() { + try{ + val clipboard: ClipboardManager = + application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + var clip = clipboard.primaryClip!!.getItemAt(0).text + Log.e("Aaa", clip.toString()) + if (clip.startsWith(cookieHeader)){ + clip = clip.removePrefix(cookieHeader) + val cookie = CookieItem( + 0, + "Cookie Import [${System.currentTimeMillis()}]", + clip.toString() + ) + insert(cookie) + updateCookiesFile() + } + }catch (e: Exception){ + e.printStackTrace() + } + } + fun exportToClipboard() = viewModelScope.launch { try{ val clipboard: ClipboardManager = application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - - getCookiesFromDB().getOrNull()?.let { + val cookieFile = File(application.cacheDir, "cookies.txt") + if (! cookieFile.exists()) updateCookiesFile() + cookieFile.readText().let { clipboard.setText(it) } }catch (e: Exception){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index 55b1b83c..9aebda3c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -2,7 +2,10 @@ package com.deniscerri.ytdlnis.database.viewmodel import android.app.Activity import android.app.Application +import android.content.Context import android.content.SharedPreferences +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.text.format.DateFormat import android.util.Log import android.widget.Toast @@ -10,10 +13,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager +import androidx.work.* import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager @@ -287,6 +287,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application suspend fun queueDownloads(items: List) { val context = getApplication().applicationContext + val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) items.forEach { it.status = DownloadRepository.Status.Queued.toString() } val ids = repository.insertAll(items) for (i in ids.indices){ @@ -301,6 +302,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val workRequest = OneTimeWorkRequestBuilder() .setInputData(Data.Builder().putLong("id", it.id).build()) .addTag("download") + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(if (allowMeteredNetworks) NetworkType.CONNECTED else NetworkType.UNMETERED) + .build() + ) .setInitialDelay(delay, TimeUnit.MILLISECONDS) .build() @@ -318,6 +324,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application repository.delete(it) } } + val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered + if (!allowMeteredNetworks && isCurrentNetworkMetered){ + Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt index 7c98c073..3e14190c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt @@ -78,10 +78,7 @@ class ShareActivity : AppCompatActivity() { cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java] sharedPreferences = getSharedPreferences("root_preferences", Activity.MODE_PRIVATE) - cookieViewModel.getCookiesFromDB().getOrNull()?.let{ - File(cacheDir, "cookies.txt").apply { writeText(it) } - } - + cookieViewModel.updateCookiesFile() val intent = intent handleIntents(intent) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt index 034a22e1..43d3dc6e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -52,6 +52,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.* +import kotlin.collections.ArrayList class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickListener { @@ -595,6 +596,17 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickLi mode?.title = getString(R.string.all_items_selected) true } + R.id.invert_selected -> { + homeAdapter?.invertSelected(resultsList) + val invertedList = arrayListOf() + resultsList?.forEach { + if (!selectedObjects?.contains(it)!!) invertedList.add(it!!) + } + selectedObjects?.clear() + selectedObjects?.addAll(invertedList) + actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" + true + } else -> false } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt index 9d1962a1..b14e9e25 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt @@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloads import android.app.Activity import android.content.Context import android.content.DialogInterface +import android.content.Intent import android.content.res.Configuration import android.os.Bundle import android.os.Handler @@ -32,6 +33,7 @@ import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.adapter.HistoryAdapter import com.deniscerri.ytdlnis.database.models.HistoryItem +import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.repository.HistoryRepository import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel @@ -213,6 +215,10 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ deleteDialog.show() } } + R.id.download_queue -> { + val intent = Intent(context, DownloadQueueActivity::class.java) + startActivity(intent) + } R.id.remove_deleted_history -> { if(allhistoryList!!.isEmpty()){ Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() @@ -565,6 +571,17 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ actionMode?.finish() true } + R.id.invert_selected -> { + historyAdapter?.invertSelected(historyList) + val invertedList = arrayListOf() + historyList?.forEach { + if (!selectedObjects?.contains(it)!!) invertedList.add(it!!) + } + selectedObjects?.clear() + selectedObjects?.addAll(invertedList) + actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" + true + } else -> false } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt index c2c74203..fda0d30e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt @@ -4,13 +4,16 @@ import android.content.Context import android.content.DialogInterface import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.MenuItem import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R @@ -22,6 +25,8 @@ import com.deniscerri.ytdlnis.util.UiUtil import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.chip.Chip import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.io.File import java.net.CookieManager @@ -70,13 +75,18 @@ class CookiesActivity : AppCompatActivity(), CookieAdapter.OnItemClickListener { deleteDialog.setMessage(getString(R.string.confirm_delete_cookies_desc)) deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> - android.webkit.CookieManager.getInstance().removeAllCookies(null) + cookiesViewModel.deleteAll() kotlin.runCatching { File(cacheDir, "cookies.txt").apply { writeText("") } } } deleteDialog.show() } + R.id.import_clipboard -> { + lifecycleScope.launch(Dispatchers.IO){ + cookiesViewModel.importFromClipboard() + } + } R.id.export_clipboard -> { cookiesViewModel.exportToClipboard() } @@ -97,7 +107,9 @@ class CookiesActivity : AppCompatActivity(), CookieAdapter.OnItemClickListener { builder.setTitle(getString(R.string.cookies)) val inputLayout = layoutInflater.inflate(R.layout.textinput, null) val editText = inputLayout.findViewById(R.id.url_edittext) - editText.setText(url) + val text = if (url.isNullOrBlank()) "https://" else url + editText.setText(text) + editText.setSelection(editText.text.length) builder.setView(inputLayout) builder.setPositiveButton( getString(R.string.get_cookies) @@ -112,9 +124,8 @@ class CookiesActivity : AppCompatActivity(), CookieAdapter.OnItemClickListener { getString(R.string.cancel) ) { dialog: DialogInterface?, which: Int -> } val dialog = builder.create() - editText.setOnKeyListener { view, i, keyEvent -> + editText.doOnTextChanged { text, start, before, count -> dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty() - false } dialog.show() val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt index be582bcc..8736028e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdlnis.ui.more +import android.annotation.SuppressLint import android.os.Bundle import android.webkit.CookieManager import android.webkit.WebView @@ -7,12 +8,16 @@ import android.webkit.WebViewClient import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.button.MaterialButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File @@ -25,6 +30,7 @@ class WebViewActivity : AppCompatActivity() { private lateinit var url: String private lateinit var cookies: String + @SuppressLint("SetJavaScriptEnabled") public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.webview_activity) @@ -35,6 +41,7 @@ class WebViewActivity : AppCompatActivity() { toolbar = appbar.findViewById(R.id.webviewToolbar) generateBtn = toolbar.findViewById(R.id.generate) cookieManager = CookieManager.getInstance() + cookieManager.removeAllCookies(null) webView = findViewById(R.id.webview) webView.settings.javaScriptEnabled = true @@ -62,20 +69,24 @@ class WebViewActivity : AppCompatActivity() { generateBtn.setOnClickListener { cookiesViewModel.getCookiesFromDB().getOrNull()?.let { - File(cacheDir , "/cookies.txt").apply { writeText(it) } + kotlin.runCatching { + lifecycleScope.launch { + withContext(Dispatchers.IO){ + cookiesViewModel.insert( + CookieItem( + 0, + url, + it + ) + ) + } + cookiesViewModel.updateCookiesFile() + } + }.onFailure { + Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show() + } } - kotlin.runCatching { - cookiesViewModel.insert( - CookieItem( - 0, - url, - "" - ) - ) - }.onFailure { - Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show() - } - cookieManager.flush() + cookieManager.removeAllCookies(null) onBackPressedDispatcher.onBackPressed() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsFragment.kt index b0b19753..c578baa3 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsFragment.kt @@ -43,6 +43,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private var incognito: SwitchPreferenceCompat? = null private var preferredDownloadType : ListPreference? = null private var downloadCard: SwitchPreferenceCompat? = null + private var meteredNetwork: SwitchPreferenceCompat? = null private var apiKey: EditTextPreference? = null private var concurrentFragments: SeekBarPreference? = null private var concurrentDownloads: SeekBarPreference? = null @@ -106,6 +107,7 @@ class SettingsFragment : PreferenceFragmentCompat() { incognito = findPreference("incognito") preferredDownloadType = findPreference("preferred_download_type") downloadCard = findPreference("download_card") + meteredNetwork = findPreference("metered_networks") apiKey = findPreference("api_key") concurrentFragments = findPreference("concurrent_fragments") concurrentDownloads = findPreference("concurrent_downloads") @@ -168,6 +170,7 @@ class SettingsFragment : PreferenceFragmentCompat() { editor.putBoolean("incognito", incognito!!.isChecked) editor.putString("preferred_download_type", preferredDownloadType!!.value) editor.putBoolean("download_card", downloadCard!!.isChecked) + editor.putBoolean("metered_networks", meteredNetwork!!.isChecked) editor.putString("api_key", apiKey!!.text) editor.putInt("concurrent_fragments", concurrentFragments!!.value) editor.putInt("concurrent_downloads", concurrentDownloads!!.value) @@ -282,8 +285,6 @@ class SettingsFragment : PreferenceFragmentCompat() { editor.apply() true } - - downloadCard!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> val enable = newValue as Boolean @@ -291,6 +292,13 @@ class SettingsFragment : PreferenceFragmentCompat() { editor.apply() true } + meteredNetwork!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + val enable = newValue as Boolean + editor.putBoolean("metered_networks", enable) + editor.apply() + true + } apiKey!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> editor.putString("api_key", newValue.toString()) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt index ecda6225..9011600c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -3,7 +3,9 @@ package com.deniscerri.ytdlnis.util import android.app.Activity import android.content.Context import android.os.Environment +import android.os.Looper import android.util.Log +import android.widget.Toast import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.ResultItem @@ -138,63 +140,70 @@ class InfoUtil(private val context: Context) { @Throws(JSONException::class) fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple { - init() - items = ArrayList() - if (key!!.isEmpty()) { - return if (useInvidous) getPlaylistFromInvidous(id) else PlaylistTuple( + try{ + init() + items = ArrayList() + if (key!!.isEmpty()) { + return if (useInvidous) getPlaylistFromInvidous(id) else PlaylistTuple( + "", + getFromYTDL("https://www.youtube.com/playlist?list=$id") + ) + } + val url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&pageToken=$nextPageToken&maxResults=50®ionCode=$countryCODE&playlistId=$id&key=$key" + //short data + val res = genericRequest(url) + if (!res.has("items")) return PlaylistTuple( + "", + getFromYTDL("https://www.youtube.com/playlist?list=$id") + ) + val dataArray = res.getJSONArray("items") + + //extra data + var url2 = "https://www.googleapis.com/youtube/v3/videos?id=" + //getting all ids, for the extra data request + for (i in 0 until dataArray.length()) { + val element = dataArray.getJSONObject(i) + val snippet = element.getJSONObject("snippet") + val videoID = snippet.getJSONObject("resourceId").getString("videoId") + url2 = "$url2$videoID," + snippet.put("videoID", videoID) + } + url2 = url2.substring( + 0, + url2.length - 1 + ) + "&part=contentDetails®ionCode=" + countryCODE + "&key=" + key + val extra = genericRequest(url2) + val extraArray = extra.getJSONArray("items") + var j = 0 + var i = 0 + while (i < extraArray.length()) { + val element = dataArray.getJSONObject(i) + val snippet = element.getJSONObject("snippet") + var duration = + extra.getJSONArray("items").getJSONObject(j).getJSONObject("contentDetails") + .getString("duration") + duration = formatDuration(duration) + snippet.put("duration", duration) + fixThumbnail(snippet) + val v = createVideofromJSON(snippet) + if (v == null || v.thumb.isEmpty()) { + i++ + continue + } else { + j++ + } + v.playlistTitle = "YTDLnis" + items.add(v) + i++ + } + val next = res.optString("nextPageToken") + return PlaylistTuple(next, items) + }catch (e: Exception){ + return PlaylistTuple( "", getFromYTDL("https://www.youtube.com/playlist?list=$id") ) } - val url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&pageToken=$nextPageToken&maxResults=50®ionCode=$countryCODE&playlistId=$id&key=$key" - //short data - val res = genericRequest(url) - if (!res.has("items")) return PlaylistTuple( - "", - getFromYTDL("https://www.youtube.com/playlist?list=$id") - ) - val dataArray = res.getJSONArray("items") - - //extra data - var url2 = "https://www.googleapis.com/youtube/v3/videos?id=" - //getting all ids, for the extra data request - for (i in 0 until dataArray.length()) { - val element = dataArray.getJSONObject(i) - val snippet = element.getJSONObject("snippet") - val videoID = snippet.getJSONObject("resourceId").getString("videoId") - url2 = "$url2$videoID," - snippet.put("videoID", videoID) - } - url2 = url2.substring( - 0, - url2.length - 1 - ) + "&part=contentDetails®ionCode=" + countryCODE + "&key=" + key - val extra = genericRequest(url2) - val extraArray = extra.getJSONArray("items") - var j = 0 - var i = 0 - while (i < extraArray.length()) { - val element = dataArray.getJSONObject(i) - val snippet = element.getJSONObject("snippet") - var duration = - extra.getJSONArray("items").getJSONObject(j).getJSONObject("contentDetails") - .getString("duration") - duration = formatDuration(duration) - snippet.put("duration", duration) - fixThumbnail(snippet) - val v = createVideofromJSON(snippet) - if (v == null || v.thumb.isEmpty()) { - i++ - continue - } else { - j++ - } - v.playlistTitle = "YTDLnis" - items.add(v) - i++ - } - val next = res.optString("nextPageToken") - return PlaylistTuple(next, items) } private fun getPlaylistFromInvidous(id: String): PlaylistTuple { @@ -221,30 +230,34 @@ class InfoUtil(private val context: Context) { @Throws(JSONException::class) fun getVideo(id: String): ResultItem? { - init() - if (key!!.isEmpty()) { - return if (useInvidous) { - val res = genericRequest(invidousURL + "videos/" + id) - if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideofromInvidiousJSON( - res - ) - } else { - getFromYTDL("https://www.youtube.com/watch?v=$id")[0] + try { + init() + if (key!!.isEmpty()) { + return if (useInvidous) { + val res = genericRequest(invidousURL + "videos/" + id) + if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideofromInvidiousJSON( + res + ) + } else { + getFromYTDL("https://www.youtube.com/watch?v=$id")[0] + } } - } - //short data - var res = - genericRequest("https://youtube.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=$id&key=$key") - if (!res.has("items")) return getFromYTDL("https://www.youtube.com/watch?v=$id")[0] - var duration = res.getJSONArray("items").getJSONObject(0).getJSONObject("contentDetails") - .getString("duration") - duration = formatDuration(duration) - res = res.getJSONArray("items").getJSONObject(0).getJSONObject("snippet") - res.put("videoID", id) - res.put("duration", duration) - fixThumbnail(res) - return createVideofromJSON(res) + //short data + var res = + genericRequest("https://youtube.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=$id&key=$key") + if (!res.has("items")) return getFromYTDL("https://www.youtube.com/watch?v=$id")[0] + var duration = res.getJSONArray("items").getJSONObject(0).getJSONObject("contentDetails") + .getString("duration") + duration = formatDuration(duration) + res = res.getJSONArray("items").getJSONObject(0).getJSONObject("snippet") + res.put("videoID", id) + res.put("duration", duration) + fixThumbnail(res) + return createVideofromJSON(res) + }catch (e: Exception){ + return getFromYTDL("https://www.youtube.com/watch?v=$id")[0] + } } private fun createVideofromJSON(obj: JSONObject): ResultItem? { @@ -510,7 +523,8 @@ class InfoUtil(private val context: Context) { arrayOf(youtubeDLResponse.out) } for (result in results) { - val jsonObject = JSONObject(result!!) + if (result.isNullOrBlank()) continue + val jsonObject = JSONObject(result) val title = if (jsonObject.has("title")) { if (jsonObject.getString("title") == "[Private video]") continue jsonObject.getString("title") @@ -531,7 +545,9 @@ class InfoUtil(private val context: Context) { thumb = jsonObject.getString("thumbnail") } else if (jsonObject.has("thumbnails")) { val thumbs = jsonObject.getJSONArray("thumbnails") - thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url") + if (thumbs.length() > 0){ + thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url") + } } val website = if (jsonObject.has("ie_key")) jsonObject.getString("ie_key") else jsonObject.getString("extractor") var playlistTitle: String? = "" @@ -560,6 +576,9 @@ class InfoUtil(private val context: Context) { ) } } catch (e: Exception) { + Looper.prepare().run { + Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() + } e.printStackTrace() } return items diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt index 51fcec0e..8a4c77a3 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -145,25 +145,27 @@ class DownloadWorker( if (audioQualityId.isNotBlank()){ request.addOption("-f", audioQualityId) - } - val ext = downloadItem.format.container - if(ext != context.getString(R.string.defaultValue) && ext != "webm"){ - val codec = when(downloadItem.format.container){ - "m4a" -> "aac" - "aac" -> "aac" - "mp3" -> "libmp3lame" - "flac" -> "flac" - "opus" -> "libopus" - else -> "" - } - Log.e("aa", codec) - if (codec.isEmpty()){ - request.addOption("-x") - }else{ - request.addOption("--remux-video", ext) - request.addOption("--ppa", "VideoRemuxer:-vn -c:a $codec") + val ext = downloadItem.format.container + if(ext != context.getString(R.string.defaultValue) && ext != "webm"){ + val codec = when(downloadItem.format.container){ + "aac" -> "aac" + "mp3" -> "libmp3lame" + "flac" -> "flac" + "opus" -> "libopus" + else -> "" + } + Log.e("aa", codec) + if (codec.isEmpty()){ + request.addOption("-x") + }else{ + request.addOption("--remux-video", ext) + request.addOption("--ppa", "VideoRemuxer:-vn -c:a $codec") + } } + + }else{ + request.addOption("-x") } request.addOption("--embed-metadata") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt index 26c5480f..fec492e3 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt @@ -103,6 +103,15 @@ class TerminalDownloadWorker( }, 1000) } + val chunks = it.out.chunked(5000) + val dataBuilder = Data.Builder(); + dataBuilder.putString("output", chunks[0]) + + + return Result.success( + dataBuilder.build() + ) + }.onFailure { if (it is YoutubeDL.CanceledException) { return Result.failure() diff --git a/app/src/main/res/drawable/baseline_downloading_24.xml b/app/src/main/res/drawable/baseline_downloading_24.xml new file mode 100644 index 00000000..a4bfc0a6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_downloading_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/metered_networks.xml b/app/src/main/res/drawable/metered_networks.xml new file mode 100644 index 00000000..2ae0b9f1 --- /dev/null +++ b/app/src/main/res/drawable/metered_networks.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/active_download_card.xml b/app/src/main/res/layout/active_download_card.xml index 287d371f..bcd1ffe5 100644 --- a/app/src/main/res/layout/active_download_card.xml +++ b/app/src/main/res/layout/active_download_card.xml @@ -24,7 +24,7 @@ android:checkable="true" app:strokeWidth="0dp" app:cardPreventCornerOverlap="true" - android:layout_margin="20dp"> + android:layout_margin="10dp"> @@ -37,6 +36,7 @@ android:id="@+id/chips_command" android:layout_width="wrap_content" android:scrollbars="none" + android:layout_margin="10dp" android:layout_height="wrap_content"> - - + android:layout_height="match_parent" + android:layout_margin="10dp"> - + android:focusableInTouchMode="true" + android:orientation="vertical"> - + - + - + + + + + + + + diff --git a/app/src/main/res/layout/fragment_download_audio.xml b/app/src/main/res/layout/fragment_download_audio.xml index 90434456..31e851d3 100644 --- a/app/src/main/res/layout/fragment_download_audio.xml +++ b/app/src/main/res/layout/fragment_download_audio.xml @@ -19,7 +19,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" - android:padding="10dp" + android:paddingHorizontal="10dp" + android:paddingBottom="10dp" android:hint="@string/title" style="@style/Widget.Material3.TextInputLayout.FilledBox"> @@ -34,7 +35,7 @@ diff --git a/app/src/main/res/layout/fragment_download_video.xml b/app/src/main/res/layout/fragment_download_video.xml index 2fa91180..109c213e 100644 --- a/app/src/main/res/layout/fragment_download_video.xml +++ b/app/src/main/res/layout/fragment_download_video.xml @@ -19,7 +19,8 @@ android:layout_height="wrap_content" android:layout_marginTop="10dp" android:hint="@string/title" - android:padding="10dp"> + android:paddingBottom="10dp" + android:paddingHorizontal="10dp"> + android:paddingHorizontal="10dp"> + android:paddingHorizontal="10dp"> - + android:layout_height="match_parent"> - - - + android:gravity="top" + android:orientation="vertical" + tools:ignore="MissingConstraints"> - + + + + + - + - + - + - + - + - + - + + + diff --git a/app/src/main/res/layout/history_sort_sheet.xml b/app/src/main/res/layout/history_sort_sheet.xml index 1d6b8d5d..d3df3298 100644 --- a/app/src/main/res/layout/history_sort_sheet.xml +++ b/app/src/main/res/layout/history_sort_sheet.xml @@ -10,15 +10,13 @@ - diff --git a/app/src/main/res/layout/template_shortcuts_list.xml b/app/src/main/res/layout/template_shortcuts_list.xml index 921d2052..5b0ac847 100644 --- a/app/src/main/res/layout/template_shortcuts_list.xml +++ b/app/src/main/res/layout/template_shortcuts_list.xml @@ -57,7 +57,8 @@ diff --git a/app/src/main/res/layout/textinput.xml b/app/src/main/res/layout/textinput.xml index a2b99785..745e4006 100644 --- a/app/src/main/res/layout/textinput.xml +++ b/app/src/main/res/layout/textinput.xml @@ -2,6 +2,7 @@ + + + + + + diff --git a/app/src/main/res/menu/main_menu_context.xml b/app/src/main/res/menu/main_menu_context.xml index 3b227bea..eecfffeb 100644 --- a/app/src/main/res/menu/main_menu_context.xml +++ b/app/src/main/res/menu/main_menu_context.xml @@ -20,5 +20,10 @@ android:title="@string/select_all" app:showAsAction="never" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 429a423b..63da7497 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,4 +204,8 @@ Get Cookies Delete All Cookies Delete all the cookies, permanently + Download over Metered Networks + Let the download worker work on any network type. If disabled, it will wait for an unmetered network to start the downloads + Download worker will start after you are connected to an unmetered network + Invert Selected \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index cb6e080b..ce5ad3ba 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -65,6 +65,14 @@ app:summary="@string/download_card_summary" app:title="@string/show_download_card" /> + +