more stuff

Quick download item log has no title now, maybe you can use "null" or the link? 'Cause it looks hilarious, LOL
Fix search history not showing sometimes
Add download type in logs
fix video not showing in cut screen
fixed xhamster not showing results
This commit is contained in:
deniscerri 2023-07-12 22:16:52 +02:00
parent 366af9270e
commit f303a4f879
No known key found for this signature in database
GPG key ID: 95C43D517D830350
32 changed files with 1265 additions and 439 deletions

View file

@ -192,8 +192,18 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation "com.google.android.exoplayer:exoplayer:2.18.7"
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer
implementation("androidx.media3:media3-exoplayer:$media3_version")
// For DASH playback support with ExoPlayer
implementation("androidx.media3:media3-exoplayer-dash:$media3_version")
// For HLS playback support with ExoPlayer
implementation("androidx.media3:media3-exoplayer-hls:$media3_version")
implementation("androidx.media3:media3-ui:$media3_version")
// For RTSP playback support with ExoPlayer
implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version")
implementation("androidx.media3:media3-datasource-cronet:$media3_version")
}

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "b21f2aa737d29eef2c1bf6418b314726",
"identityHash": "3d472df655311f8e531602dca2886c34",
"entities": [
{
"tableName": "results",
@ -180,7 +180,7 @@
},
{
"tableName": "downloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `extraCommands` TEXT NOT NULL DEFAULT '', `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER)",
"fields": [
{
"fieldPath": "id",
@ -286,6 +286,13 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extraCommands",
"columnName": "extraCommands",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "customFileNameTemplate",
"columnName": "customFileNameTemplate",
@ -446,7 +453,7 @@
},
{
"tableName": "logs",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
@ -465,6 +472,24 @@
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadType",
"columnName": "downloadType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadTime",
"columnName": "downloadTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
@ -480,7 +505,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b21f2aa737d29eef2c1bf6418b314726')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3d472df655311f8e531602dca2886c34')"
]
}
}

View file

@ -16,16 +16,20 @@ import android.os.Handler
import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.widget.CheckBox
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope

View file

@ -1,6 +1,8 @@
package com.deniscerri.ytdlnis.adapter
import android.annotation.SuppressLint
import android.app.Activity
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -12,29 +14,38 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<LogItem?, DownloadLogsAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private val checkedItems: ArrayList<Long>
init {
checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener
this.activity = activity
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val item: ConstraintLayout
val item: MaterialCardView
init {
item = itemView.findViewById(R.id.download_log_item_constraint)
item = itemView.findViewById(R.id.log_card_view)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = LayoutInflater.from(parent.context)
.inflate(R.layout.download_log_item, parent, false)
.inflate(R.layout.log_card, parent, false)
return ViewHolder(cardView, onItemClickListener)
}
@ -45,20 +56,112 @@ class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Ac
val title = card.findViewById<TextView>(R.id.title)
title.text = item?.title
val delete = card.findViewById<Button>(R.id.delete)
val downloadedTime = card.findViewById<TextView>(R.id.downloadedTime)
downloadedTime.text = SimpleDateFormat(
DateFormat.getBestDateTimePattern(
Locale.getDefault(), "ddMMMyyyy - HH:mm"), Locale.getDefault()).format(item!!.downloadTime)
delete.setOnClickListener {
onItemClickListener.onDeleteClick(item!!)
val downloadTypeIcon = card.findViewById<MaterialButton>(R.id.download_type)
when(item.downloadType){
DownloadViewModel.Type.audio -> downloadTypeIcon.setIconResource(R.drawable.ic_music)
DownloadViewModel.Type.video -> downloadTypeIcon.setIconResource(R.drawable.ic_video)
DownloadViewModel.Type.command -> downloadTypeIcon.setIconResource(R.drawable.ic_terminal)
}
title.setOnClickListener {
onItemClickListener.onItemClick(item!!)
val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
else formatNote!!.text = item.format.format_note.uppercase()
val codec = card.findViewById<TextView>(R.id.codec)
val codecText =
if (item.format.encoding != "") {
item.format.encoding.uppercase()
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
item.format.vcodec.uppercase()
} else {
item.format.acodec.uppercase()
}
if (codecText == "" || codecText == "none"){
codec.visibility = View.GONE
}else{
codec.visibility = View.VISIBLE
codec.text = codecText
}
val fileSize = card.findViewById<TextView>(R.id.file_size)
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
if (fileSizeReadable == "?") fileSize.visibility = View.GONE
else fileSize.text = fileSizeReadable
if (checkedItems.contains(item.id)) {
card.isChecked = true
card.strokeWidth = 5
} else {
card.isChecked = false
card.strokeWidth = 0
}
card.setOnClickListener {
if (checkedItems.size > 0) {
checkCard(card, item.id)
} else {
onItemClickListener.onItemClick(item.id)
}
}
card.setOnLongClickListener {
checkCard(card, item.id)
true
}
}
@SuppressLint("NotifyDataSetChanged")
fun clearCheckeditems() {
for (i in 0 until itemCount){
val item = getItem(i)
if (checkedItems.find { it == item?.id } != null){
checkedItems.remove(item?.id)
notifyItemChanged(i)
}
}
checkedItems.clear()
}
@SuppressLint("NotifyDataSetChanged")
fun checkAll(items: List<LogItem?>?){
checkedItems.clear()
checkedItems.addAll(items!!.map { it!!.id })
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
fun invertSelected(items: List<LogItem?>?){
val invertedList = mutableListOf<Long>()
items?.forEach {
if (!checkedItems.contains(it!!.id)) invertedList.add(it.id)
}
checkedItems.clear()
checkedItems.addAll(invertedList)
notifyDataSetChanged()
}
private fun checkCard(card: MaterialCardView, itemID: Long) {
if (card.isChecked) {
card.strokeWidth = 0
checkedItems.remove(itemID)
} else {
card.strokeWidth = 5
checkedItems.add(itemID)
}
card.isChecked = !card.isChecked
onItemClickListener.onCardSelect(itemID, card.isChecked)
}
interface OnItemClickListener {
fun onItemClick(item: LogItem)
fun onItemClick(itemID: Long)
fun onDeleteClick(item: LogItem)
fun onCardSelect(itemID: Long, isChecked: Boolean)
}
companion object {

View file

@ -27,6 +27,8 @@ data class DownloadItem(
val playlistTitle: String,
val audioPreferences : AudioPreferences,
val videoPreferences: VideoPreferences,
@ColumnInfo(defaultValue = "")
var extraCommands: String,
var customFileNameTemplate: String,
var SaveThumb: Boolean,
@ColumnInfo(defaultValue = "Queued")

View file

@ -1,12 +1,17 @@
package com.deniscerri.ytdlnis.database.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@Entity(tableName = "logs")
data class LogItem(
@PrimaryKey(autoGenerate = true)
var id: Long,
var title: String,
var content: String
var content: String,
var format: Format,
var downloadType: DownloadViewModel.Type,
var downloadTime: Long,
)

View file

@ -180,7 +180,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
sharedPreferences.getString("video_format", "Default")!!,
"",
resultItem.formats,
downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences, "", customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
)
}
@ -302,7 +302,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
container,
"",
ArrayList(),
path, historyItem.website, "", "", audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
path, historyItem.website, "", "", audioPreferences, videoPreferences, "", customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
)
}

View file

@ -4,6 +4,8 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.ThemeUtil

View file

@ -239,7 +239,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
arguments?.remove("url")
}
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
lifecycleScope.launch {
updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
}
}
}
@ -291,7 +293,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}else{
linkYouCopied!!.visibility = GONE
}
updateSearchViewItems(searchView!!.editText.text, linkYouCopied)
lifecycleScope.launch {
updateSearchViewItems(searchView!!.editText.text, linkYouCopied)
}
}catch (e: Exception){
e.printStackTrace()
linkYouCopied!!.visibility = GONE
@ -301,7 +305,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
searchView!!.editText.doAfterTextChanged {
if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged
updateSearchViewItems(it, linkYouCopied)
lifecycleScope.launch {
updateSearchViewItems(it, linkYouCopied)
}
}
searchView!!.editText.setOnTouchListener(OnTouchListener { _, event ->
@ -368,91 +374,89 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}
@SuppressLint("InflateParams")
private fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){
lifecycleScope.launch {
searchSuggestionsLinearLayout!!.removeAllViews()
searchHistoryLinearLayout!!.removeAllViews()
private suspend fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){
searchSuggestionsLinearLayout!!.removeAllViews()
searchHistoryLinearLayout!!.removeAllViews()
searchSuggestionsLinearLayout!!.visibility = GONE
searchHistoryLinearLayout!!.visibility = GONE
linkYouCopied!!.visibility = GONE
searchSuggestionsLinearLayout!!.visibility = GONE
searchHistoryLinearLayout!!.visibility = GONE
linkYouCopied!!.visibility = GONE
if (searchView!!.editText.text.isEmpty()){
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
if (searchView!!.editText.text.isEmpty()){
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
}else{
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0)
}
val suggestions = withContext(Dispatchers.IO){
if (it!!.isEmpty()) {
resultViewModel.getSearchHistory().map { it.query }
}else if (sharedPreferences!!.getBoolean("search_suggestions", false)){
infoUtil!!.getSearchSuggestions(it.toString())
}else{
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0)
}
val suggestions = withContext(Dispatchers.IO){
if (it!!.isEmpty()) {
resultViewModel.getSearchHistory().map { it.query }
}else if (sharedPreferences!!.getBoolean("search_suggestions", false)){
infoUtil!!.getSearchSuggestions(it.toString())
}else{
emptyList()
}
emptyList()
}
}
if (it!!.isEmpty()){
for (i in suggestions.indices) {
val v = LayoutInflater.from(fragmentContext)
.inflate(R.layout.search_suggestion_item, null)
val textView = v.findViewById<TextView>(R.id.suggestion_text)
textView.text = suggestions[i]
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
Handler(Looper.getMainLooper()).post {
searchHistoryLinearLayout!!.addView(
v
)
}
textView.setOnClickListener {
searchView!!.setText(textView.text)
initSearch(searchView!!)
}
textView.setOnLongClickListener {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + textView.text + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
searchHistoryLinearLayout!!.removeView(v)
resultViewModel.removeSearchQueryFromHistory(textView.text.toString())
}
deleteDialog.show()
true
if (it!!.isEmpty()){
for (i in suggestions.indices) {
val v = LayoutInflater.from(fragmentContext)
.inflate(R.layout.search_suggestion_item, null)
val textView = v.findViewById<TextView>(R.id.suggestion_text)
textView.text = suggestions[i]
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
Handler(Looper.getMainLooper()).post {
searchHistoryLinearLayout!!.addView(
v
)
}
textView.setOnClickListener {
searchView!!.setText(textView.text)
initSearch(searchView!!)
}
textView.setOnLongClickListener {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + textView.text + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
searchHistoryLinearLayout!!.removeView(v)
resultViewModel.removeSearchQueryFromHistory(textView.text.toString())
}
deleteDialog.show()
true
}
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setOnClickListener {
searchView!!.setText(textView.text)
searchView!!.editText.setSelection(searchView!!.editText.length())
}
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setOnClickListener {
searchView!!.setText(textView.text)
searchView!!.editText.setSelection(searchView!!.editText.length())
}
searchHistoryLinearLayout!!.visibility = VISIBLE
if (linkYouCopied.findViewById<TextView>(R.id.suggestion_text).text.isNotEmpty()){
linkYouCopied.visibility = VISIBLE
}
}else{
for (i in suggestions.indices) {
val v = LayoutInflater.from(fragmentContext)
.inflate(R.layout.search_suggestion_item, null)
val textView = v.findViewById<TextView>(R.id.suggestion_text)
textView.text = suggestions[i]
Handler(Looper.getMainLooper()).post {
searchSuggestionsLinearLayout!!.addView(
v
)
}
textView.setOnClickListener {
searchView!!.setText(textView.text)
initSearch(searchView!!)
}
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setOnClickListener {
searchView!!.setText(textView.text)
searchView!!.editText.setSelection(searchView!!.editText.length())
}
}
searchSuggestionsLinearLayout!!.visibility = VISIBLE
}
searchHistoryLinearLayout!!.visibility = VISIBLE
if (linkYouCopied.findViewById<TextView>(R.id.suggestion_text).text.isNotEmpty()){
linkYouCopied.visibility = VISIBLE
}
}else{
for (i in suggestions.indices) {
val v = LayoutInflater.from(fragmentContext)
.inflate(R.layout.search_suggestion_item, null)
val textView = v.findViewById<TextView>(R.id.suggestion_text)
textView.text = suggestions[i]
Handler(Looper.getMainLooper()).post {
searchSuggestionsLinearLayout!!.addView(
v
)
}
textView.setOnClickListener {
searchView!!.setText(textView.text)
initSearch(searchView!!)
}
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setOnClickListener {
searchView!!.setText(textView.text)
searchView!!.editText.setSelection(searchView!!.editText.length())
}
}
searchSuggestionsLinearLayout!!.visibility = VISIBLE
}
}

View file

@ -15,26 +15,23 @@ import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem.fromUri
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.cronet.CronetDataSource
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.ui.PlayerView
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.InfoUtil
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
@ -53,15 +50,17 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.chromium.net.CronetEngine
import java.lang.reflect.Type
import java.util.*
import java.util.concurrent.Executors
import kotlin.properties.Delegates
class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls : String?, private var chapters: List<ChapterItem>?, private val listener: VideoCutListener) : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil
private lateinit var player: Player
private lateinit var player: ExoPlayer
private lateinit var cutSection : ConstraintLayout
private lateinit var durationText: TextView
@ -92,6 +91,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
@SuppressLint("RestrictedApi", "SetTextI18n")
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun setupDialog(dialog: Dialog, style: Int) {
super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.cut_video_sheet, null)
@ -104,30 +104,53 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
behavior.peekHeight = displayMetrics.heightPixels
}
val renderersFactory = DefaultRenderersFactory(requireContext().applicationContext)
.setEnableDecoderFallback(true)
.setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
.setMediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder ->
var decoderInfo: MutableList<MediaCodecInfo> =
MediaCodecSelector.DEFAULT
.getDecoderInfos(
mimeType,
requiresSecureDecoder,
requiresTunnelingDecoder
)
if (MimeTypes.VIDEO_H264 == mimeType) {
// copy the list because MediaCodecSelector.DEFAULT returns an unmodifiable list
decoderInfo = ArrayList(decoderInfo)
decoderInfo.reverse()
}
decoderInfo
}
// val renderersFactory = DefaultRenderersFactory(requireContext().applicationContext)
// .setEnableDecoderFallback(true)
// .setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
// .setMediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder ->
// var decoderInfo: MutableList<MediaCodecInfo> =
// MediaCodecSelector.DEFAULT
// .getDecoderInfos(
// mimeType,
// requiresSecureDecoder,
// requiresTunnelingDecoder
// )
// if (MimeTypes.VIDEO_H264 == mimeType) {
// // copy the list because MediaCodecSelector.DEFAULT returns an unmodifiable list
// decoderInfo = ArrayList(decoderInfo)
// decoderInfo.reverse()
// }
// decoderInfo
// }
val cronetEngine: CronetEngine = CronetEngine.Builder(App.instance)
.enableHttp2(true)
.enableQuic(true)
.enableBrotli(true)
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 1024L * 1024L) // 1MiB
.build()
val trackSelector = DefaultTrackSelector(requireContext())
val loadControl = DefaultLoadControl()
val loadControl = DefaultLoadControl.Builder()
// cache the last three minutes
.setBackBuffer(1000 * 60 * 3, true)
.setBufferDurationsMs(
1000 * 10, // exo default is 50s
50000,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
)
.build()
val cronetDataSourceFactory = CronetDataSource.Factory(
cronetEngine,
Executors.newCachedThreadPool()
)
val dataSourceFactory = DefaultDataSource.Factory(requireContext(), cronetDataSourceFactory)
player = ExoPlayer.Builder(requireContext())
.setUsePlatformDiagnostics(false)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.setHandleAudioBecomingNoisy(true)
@ -139,8 +162,9 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
,
true)
.build()
val frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
val videoView = view.findViewById<StyledPlayerView>(R.id.video_view)
val videoView = view.findViewById<PlayerView>(R.id.video_view)
videoView.player = player
timeSeconds = convertStringToTimestamp(item.duration)
if (chapters == null) chapters = emptyList()
@ -199,7 +223,9 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
chapters = Gson().fromJson(data.first().toString(), listType)
data.removeFirst()
}catch (ignored: Exception) {}
}catch (ignored: Exception) {
data.removeFirst()
}
}
if (data.isEmpty()) throw Exception("No Streaming URL found!")
@ -210,7 +236,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
val videoSource: MediaSource =
DefaultMediaSourceFactory(requireContext())
.createMediaSource(fromUri(Uri.parse(data[1])))
(player as ExoPlayer).setMediaSource(MergingMediaSource(videoSource, audioSource))
player.setMediaSource(MergingMediaSource(videoSource, audioSource))
}else{
player.addMediaItem(fromUri(Uri.parse(data[0])))
}
@ -229,12 +255,12 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
}
//poll video progress
lifecycleScope.launch {
videoProgress(player).collect {
val currentTime = infoUtil.formatIntegerDuration(it, Locale.US)
videoProgress(player).collect { p ->
val currentTime = infoUtil.formatIntegerDuration(p, Locale.US)
durationText.text = "$currentTime / ${item.duration}"
val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
val endTimestamp = convertStringToTimestamp(toTextInput.editText!!.text.toString())
if (it >= endTimestamp){
if (p >= endTimestamp){
player.prepare()
player.seekTo((startTimestamp * 1000).toLong())
}
@ -536,7 +562,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
}
private fun videoProgress(player: Player?) = flow {
private fun videoProgress(player: ExoPlayer?) = flow {
while (true) {
emit((player!!.currentPosition / 1000).toInt())
delay(1000)

View file

@ -1,24 +1,29 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -33,7 +38,10 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -41,6 +49,7 @@ import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
@ -56,6 +65,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
private lateinit var author : TextInputLayout
private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView
private lateinit var infoUtil: InfoUtil
lateinit var downloadItem : DownloadItem
override fun onCreateView(
@ -68,7 +78,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext())
return fragmentView
}
@ -328,6 +338,47 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
}else{
cut.isEnabled = false
}
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommands.visibility = View.VISIBLE
extraCommands.setOnClickListener {
val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.extra_commands_bottom_sheet)
val text = bottomSheet.findViewById<EditText>(R.id.command)
val currentCommand = infoUtil.buildYoutubeDLRequest(downloadItem).buildCommand().joinToString(" ")
bottomSheet.findViewById<TextView>(R.id.currentText)?.text = currentCommand
text?.setText(downloadItem.extraCommands)
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
text!!.postDelayed({
text.setSelection(text.length())
text.requestFocus()
imm.showSoftInput(text, 0)
}, 300)
val ok = bottomSheet.findViewById<Button>(R.id.okButton)
ok?.setOnClickListener {
downloadItem.extraCommands = text.text.toString()
bottomSheet.dismiss()
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}else{
extraCommands.visibility = View.GONE
}
}catch (e : Exception){
e.printStackTrace()
}

View file

@ -1,24 +1,31 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.icu.text.IDNA.Info
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -33,7 +40,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -56,6 +65,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
private lateinit var author : TextInputLayout
private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView
private lateinit var infoUtil: InfoUtil
lateinit var downloadItem: DownloadItem
@ -69,10 +79,12 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this@DownloadVideoFragment)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext())
return fragmentView
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
@ -388,6 +400,45 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
downloadItem.videoPreferences.removeAudio = removeAudio.isChecked
}
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommands.visibility = View.VISIBLE
extraCommands.setOnClickListener {
val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.extra_commands_bottom_sheet)
val text = bottomSheet.findViewById<EditText>(R.id.command)
val currentCommand = infoUtil.buildYoutubeDLRequest(downloadItem).buildCommand().joinToString(" ")
bottomSheet.findViewById<TextView>(R.id.currentText)?.text = currentCommand
text?.setText(downloadItem.extraCommands)
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
text!!.postDelayed({
text.setSelection(text.length())
text.requestFocus()
imm.showSoftInput(text, 0)
}, 300)
val ok = bottomSheet.findViewById<Button>(R.id.okButton)
ok?.setOnClickListener {
downloadItem.extraCommands = text.text.toString()
bottomSheet.dismiss()
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}else{
extraCommands.visibility = View.GONE
}
} catch (e: Exception) {
e.printStackTrace()
}

View file

@ -27,8 +27,6 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.CookieAdapter
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.util.FileUtil
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

View file

@ -19,6 +19,7 @@ import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
@ -65,6 +66,7 @@ class DownloadLogFragment : Fragment() {
val clipboard: ClipboardManager =
mainActivity.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setText(content.text)
Snackbar.make(content, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
}
val id = arguments?.getLong("logID")

View file

@ -1,16 +1,24 @@
package com.deniscerri.ytdlnis.ui.more.downloadLogs
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity
@ -20,17 +28,22 @@ import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListener {
private lateinit var recyclerView: RecyclerView
private lateinit var downloadLogAdapter: DownloadLogsAdapter
private lateinit var noResults: RelativeLayout
private lateinit var fileList: MutableList<File>
private lateinit var items: MutableList<LogItem>
private lateinit var topAppBar: MaterialToolbar
private lateinit var mainActivity: MainActivity
private lateinit var logViewModel: LogViewModel
private var selectedObjects: ArrayList<LogItem>? = null
private var actionMode : ActionMode? = null
override fun onCreateView(
inflater: LayoutInflater,
@ -39,6 +52,8 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
): View? {
mainActivity = activity as MainActivity
mainActivity.hideBottomNavigation()
items = mutableListOf()
selectedObjects = arrayListOf()
return inflater.inflate(R.layout.fragment_download_log_list, container, false)
}
@ -55,11 +70,18 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
recyclerView = view.findViewById(R.id.logs_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = downloadLogAdapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
noResults = view.findViewById(R.id.no_results)
noResults.visibility = View.GONE
logViewModel = ViewModelProvider(this)[LogViewModel::class.java]
logViewModel.items.observe(viewLifecycleOwner) {
items = it.toMutableList()
if (it.isEmpty()) noResults.visibility = View.VISIBLE
else noResults.visibility = View.GONE
downloadLogAdapter.submitList(it)
@ -89,9 +111,9 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
}
}
override fun onItemClick(item: LogItem) {
override fun onItemClick(itemID: Long) {
val bundle = Bundle()
bundle.putLong("logID", item.id)
bundle.putLong("logID", itemID)
findNavController().navigate(
R.id.downloadLogFragment,
bundle
@ -108,4 +130,151 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
}
deleteDialog.show()
}
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = items.find { it.id == itemID }
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (activity as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
}
}
else {
selectedObjects!!.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){
actionMode?.finish()
}
}
}
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.logs_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
return true
}
override fun onPrepareActionMode(
mode: ActionMode?,
menu: Menu?
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode?,
item: MenuItem?
): Boolean {
return when (item!!.itemId) {
R.id.delete_results -> {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){
logViewModel.delete(obj)
}
clearCheckedItems()
actionMode?.finish()
}
deleteDialog.show()
true
}
R.id.select_all -> {
downloadLogAdapter.checkAll(items)
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
mode?.title = getString(R.string.all_items_selected)
true
}
R.id.invert_selected -> {
downloadLogAdapter.invertSelected(items)
val invertedList = arrayListOf<LogItem>()
items.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish()
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
clearCheckedItems()
}
}
private fun clearCheckedItems(){
downloadLogAdapter.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
when (direction) {
ItemTouchHelper.LEFT -> {
val deletedItem = items[position]
logViewModel.delete(deletedItem)
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO){
logViewModel.insert(deletedItem)
}
}.show()
}
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
RecyclerViewSwipeDecorator.Builder(
requireContext(),
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
.addSwipeLeftBackgroundColor(Color.RED)
.addSwipeLeftActionIcon(R.drawable.baseline_delete_24)
.create()
.decorate()
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
}

View file

@ -200,7 +200,10 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
}
version = findPreference("version")
version!!.summary = "${BuildConfig.VERSION_NAME} [${Build.SUPPORTED_ABIS[0]}] ${BuildConfig.BUILD_TYPE}"
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
val installedABI = Build.SUPPORTED_ABIS.first { nativeLibraryDir!!.contains(it) }
version!!.summary = "${BuildConfig.VERSION_NAME} [${installedABI}] ${BuildConfig.BUILD_TYPE}"
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {

View file

@ -9,10 +9,12 @@ import android.widget.Toast
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
@ -560,9 +562,16 @@ class InfoUtil(private val context: Context) {
val formats : ArrayList<Format> = ArrayList()
for (f in 0 until jsonArray.length()){
val format = jsonArray.getJSONObject(f)
if (format.has("filesize") && format.get("filesize") == "None"){
format.remove("filesize")
format.put("filesize", 0)
if (format.has("filesize")){
if (format.get("filesize") == "None"){
format.remove("filesize")
format.put("filesize", 0)
}
try{
val size = format.get("filesize").toString().toFloat()
format.remove("filesize")
format.put("filesize", size)
}catch (ignored: Exception){}
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (format.has("format_note")){
@ -626,9 +635,16 @@ class InfoUtil(private val context: Context) {
try{
if (format.getString("filesize") == "None") continue
}catch (e: Exception) { continue }
if (format.has("filesize") && format.get("filesize") == "None"){
format.remove("filesize")
format.put("filesize", 0)
if (format.has("filesize")){
if (format.get("filesize") == "None"){
format.remove("filesize")
format.put("filesize", 0)
}
try{
val size = format.get("filesize").toString().toFloat()
format.remove("filesize")
format.put("filesize", size)
}catch (ignored: Exception){}
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if ( !formatProper!!.format_note.contains("audio only", true)) {
@ -733,9 +749,16 @@ class InfoUtil(private val context: Context) {
if (formatsInJSON != null) {
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if (format.has("filesize") && format.get("filesize") == "None"){
format.remove("filesize")
format.put("filesize", 0)
if (format.has("filesize")){
if (format.get("filesize") == "None"){
format.remove("filesize")
format.put("filesize", 0)
}
try{
val size = format.get("filesize").toString().toFloat()
format.remove("filesize")
format.put("filesize", size)
}catch (ignored: Exception){}
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.format_note == null) continue
@ -768,7 +791,7 @@ class InfoUtil(private val context: Context) {
items.add(ResultItem(0,
url,
title,
author!!,
author,
duration,
thumb!!,
website,
@ -844,7 +867,7 @@ class InfoUtil(private val context: Context) {
}else{
jsonObject.getString("title")
},
author!!,
author,
duration,
thumb!!,
jsonObject.getString("extractor"),
@ -969,6 +992,244 @@ class InfoUtil(private val context: Context) {
}
}
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val tempFileDir = File(context.cacheDir.absolutePath + "/downloads/" + downloadItem.id)
val url = downloadItem.url
val request = YoutubeDLRequest(url)
val type = downloadItem.type
val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) {
request.addOption("--downloader", "libaria2c.so")
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
} else {
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
}
val retries = sharedPreferences.getString("--retries", "")!!
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
if(retries.isNotEmpty()) request.addOption("retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries)
val limitRate = sharedPreferences.getString("limit_rate", "")
if (limitRate != "") request.addOption("-r", limitRate!!)
if(downloadItem.type != DownloadViewModel.Type.command){
if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail")
request.addOption("--convert-thumbnails", "png")
}
if (!sharedPreferences.getBoolean("mtime", false)){
request.addOption("--no-mtime")
}
val sponsorBlockFilters : ArrayList<String> = when(downloadItem.type) {
DownloadViewModel.Type.audio -> {
downloadItem.audioPreferences.sponsorBlockFilters
}
//video
else -> {
downloadItem.videoPreferences.sponsorBlockFilters
}
}
if (sponsorBlockFilters.isNotEmpty()) {
val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() })
if (filters.isNotBlank()) request.addOption("--sponsorblock-remove", filters)
}
if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title.take(200)))
}
if (downloadItem.author.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25)))
}
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader)s - %(title)s"
if (downloadItem.downloadSections.isNotBlank()){
downloadItem.downloadSections.split(";").forEach {
if (it.isBlank()) return@forEach
if (it.contains(":"))
request.addOption("--download-sections", "*$it")
else
request.addOption("--download-sections", it)
if (sharedPreferences.getBoolean("force_keyframes", false)){
request.addOption("--force-keyframes-at-cuts")
}
}
downloadItem.customFileNameTemplate += " %(section_title)s %(autonumber)s"
request.addOption("--output-na-placeholder", " ")
}
if (sharedPreferences.getBoolean("use_audio_quality", false)){
request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0))
}
if (downloadItem.extraCommands.isNotBlank()){
val conf = File(context.cacheDir.absolutePath + "/downloads/configExtraCommands${System.currentTimeMillis()}.txt")
conf.createNewFile()
conf.writeText(downloadItem.extraCommands)
request.addOption(
"--config-locations",
conf.absolutePath
)
}
}
if (sharedPreferences.getBoolean("restrict_filenames", true)) {
request.addOption("--restrict-filenames")
}
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()){
request.addOption("--cookies", cookiesFile.absolutePath)
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
if(keepCache){
request.addOption("--part")
request.addOption("--keep-fragments")
}
when(type){
DownloadViewModel.Type.audio -> {
val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = ""
else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio"
val ext = downloadItem.container
if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId)
request.addOption("-x")
if(ext.isNotBlank()){
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
request.addOption("--audio-format", ext)
}
}
request.addOption("--embed-metadata")
if (downloadItem.audioPreferences.embedThumb) {
request.addOption("--embed-thumbnail")
request.addOption("--convert-thumbnails", "jpg")
if (sharedPreferences.getBoolean("crop_thumbnail", true)){
try {
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
request.addOption("--config", config.absolutePath)
} catch (ignored: Exception) {}
}
}
request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s")
if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s")
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track)s")
} else {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
}
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
}else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
}
}
DownloadViewModel.Type.video -> {
val supportedContainers = context.resources.getStringArray(R.array.video_containers)
if (downloadItem.videoPreferences.addChapters) {
request.addOption("--sponsorblock-mark", "all")
request.addOption("--embed-chapters")
}
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
}
val defaultFormats = context.resources.getStringArray(R.array.video_formats)
if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()) request.addOption("--audio-multistreams")
var videoFormatID = downloadItem.format.format_id
Log.e(DownloadWorker.TAG, videoFormatID)
var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best"
if (videoFormatID.isNotEmpty()) {
if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo"
else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst"
else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]"
formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty() && ! downloadItem.videoPreferences.removeAudio){
val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
"$videoFormatID+$audioIds/best/$videoFormatID"
}else{
"$videoFormatID+bestaudio/best/$videoFormatID"
}
}
Log.e(DownloadWorker.TAG, formatArgument)
request.addOption("-f", formatArgument)
val outputFormat = downloadItem.container
if(outputFormat.isNotEmpty() && outputFormat != "Default" && outputFormat != context.getString(R.string.defaultValue) && supportedContainers.contains(outputFormat)){
request.addOption("--merge-output-format", outputFormat.lowercase())
if (outputFormat != "webm") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) {
request.addOption("--embed-thumbnail")
}
}
}
if (downloadItem.videoPreferences.writeSubs){
request.addOption("--write-subs")
request.addOption("--write-auto-subs")
request.addOption("--sub-format", "str/ass/best")
request.addOption("--convert-subtitles", "srt")
if (!downloadItem.videoPreferences.embedSubs) {
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
}
}
if (downloadItem.videoPreferences.removeAudio){
request.addOption("--ppa", "ffmpeg:-an")
}
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
}else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
}
}
DownloadViewModel.Type.command -> {
request.addOption(
"--config-locations",
File(context.cacheDir.absolutePath + "/downloads/config${System.currentTimeMillis()}.txt").apply {
writeText(downloadItem.format.format_note)
}.absolutePath
)
request.addOption("-P", tempFileDir.absolutePath)
}
}
return request
}
private val pipedURL = sharedPreferences.getString("piped_instance", defaultPipedURL)?.ifEmpty { defaultPipedURL }?.removeSuffix("/")
class PlaylistTuple internal constructor(

View file

@ -21,12 +21,10 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -42,6 +40,7 @@ class DownloadWorker(
itemId = inputData.getLong("id", 0)
if (itemId == 0L) return Result.failure()
val notificationUtil = NotificationUtil(context)
val infoUtil = InfoUtil(context)
val dbManager = DBManager.getInstance(context)
@ -52,7 +51,6 @@ class DownloadWorker(
val logRepo = LogRepository(dbManager.logDao)
val handler = Handler(Looper.getMainLooper())
val downloadItem: DownloadItem?
try {
downloadItem = repository.getItemByID(itemId)
@ -63,6 +61,15 @@ class DownloadWorker(
if (downloadItem.status != DownloadRepository.Status.Queued.toString()) return Result.failure()
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.createPendingIntent()
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
val foregroundInfo = ForegroundInfo(downloadItem.id.toInt(), notification)
setForegroundAsync(foregroundInfo)
Log.e(TAG, downloadItem.toString())
runBlocking{
@ -75,254 +82,28 @@ class DownloadWorker(
updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
}
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.createPendingIntent()
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
val foregroundInfo = ForegroundInfo(downloadItem.id.toInt(), notification)
setForegroundAsync(foregroundInfo)
val url = downloadItem.url
val request = YoutubeDLRequest(url)
val type = downloadItem.type
val downloadLocation = downloadItem.downloadPath
val request = infoUtil.buildYoutubeDLRequest(downloadItem)
val tempFileDir = File(context.cacheDir.absolutePath + "/downloads/" + downloadItem.id)
tempFileDir.delete()
tempFileDir.mkdirs()
val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) {
request.addOption("--downloader", "libaria2c.so")
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
} else {
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
}
val retries = sharedPreferences.getString("--retries", "")!!
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
if(retries.isNotEmpty()) request.addOption("retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries)
val limitRate = sharedPreferences.getString("limit_rate", "")
if (limitRate != "") request.addOption("-r", limitRate!!)
if(downloadItem.type != DownloadViewModel.Type.command){
if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail")
request.addOption("--convert-thumbnails", "png")
}
if (!sharedPreferences.getBoolean("mtime", false)){
request.addOption("--no-mtime")
}
val sponsorBlockFilters : ArrayList<String> = when(downloadItem.type) {
DownloadViewModel.Type.audio -> {
downloadItem.audioPreferences.sponsorBlockFilters
}
//video
else -> {
downloadItem.videoPreferences.sponsorBlockFilters
}
}
if (sponsorBlockFilters.isNotEmpty()) {
val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() })
if (filters.isNotBlank()) request.addOption("--sponsorblock-remove", filters)
}
if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title.take(200)))
}
if (downloadItem.author.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25)))
}
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader)s - %(title)s"
if (downloadItem.downloadSections.isNotBlank()){
downloadItem.downloadSections.split(";").forEach {
if (it.isBlank()) return@forEach
if (it.contains(":"))
request.addOption("--download-sections", "*$it")
else
request.addOption("--download-sections", it)
if (sharedPreferences.getBoolean("force_keyframes", false)){
request.addOption("--force-keyframes-at-cuts")
}
}
downloadItem.customFileNameTemplate += " %(section_title)s %(autonumber)s"
request.addOption("--output-na-placeholder", " ")
}
if (sharedPreferences.getBoolean("use_audio_quality", false)){
request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0))
}
}
if (sharedPreferences.getBoolean("restrict_filenames", true)) {
request.addOption("--restrict-filenames")
}
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()){
request.addOption("--cookies", cookiesFile.absolutePath)
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
val downloadLocation = downloadItem.downloadPath
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
if(keepCache){
request.addOption("--part")
request.addOption("--keep-fragments")
}
when(type){
DownloadViewModel.Type.audio -> {
val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = ""
else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio"
val ext = downloadItem.container
if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId)
request.addOption("-x")
if(ext.isNotBlank()){
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
request.addOption("--audio-format", ext)
}
}
request.addOption("--embed-metadata")
if (downloadItem.audioPreferences.embedThumb) {
request.addOption("--embed-thumbnail")
request.addOption("--convert-thumbnails", "jpg")
if (sharedPreferences.getBoolean("crop_thumbnail", true)){
try {
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
request.addOption("--config", config.absolutePath)
} catch (ignored: Exception) {}
}
}
request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s")
if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s")
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track)s")
} else {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
}
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
}else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
}
}
DownloadViewModel.Type.video -> {
val supportedContainers = context.resources.getStringArray(R.array.video_containers)
if (downloadItem.videoPreferences.addChapters) {
request.addOption("--sponsorblock-mark", "all")
request.addOption("--embed-chapters")
}
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
}
val defaultFormats = context.resources.getStringArray(R.array.video_formats)
if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()) request.addOption("--audio-multistreams")
var videoFormatID = downloadItem.format.format_id
Log.e(TAG, videoFormatID)
var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best"
if (videoFormatID.isNotEmpty()) {
if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo"
else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst"
else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]"
formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty() && ! downloadItem.videoPreferences.removeAudio){
val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
"$videoFormatID+$audioIds/best/$videoFormatID"
}else{
"$videoFormatID+bestaudio/best/$videoFormatID"
}
}
Log.e(TAG, formatArgument)
request.addOption("-f", formatArgument)
val outputFormat = downloadItem.container
if(outputFormat.isNotEmpty() && outputFormat != "Default" && outputFormat != context.getString(R.string.defaultValue) && supportedContainers.contains(outputFormat)){
request.addOption("--merge-output-format", outputFormat.lowercase())
if (outputFormat != "webm") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) {
request.addOption("--embed-thumbnail")
}
}
}
if (downloadItem.videoPreferences.writeSubs){
request.addOption("--write-subs")
request.addOption("--write-auto-subs")
request.addOption("--sub-format", "str/ass/best")
request.addOption("--convert-subtitles", "srt")
if (!downloadItem.videoPreferences.embedSubs) {
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
}
}
if (downloadItem.videoPreferences.removeAudio){
request.addOption("--ppa", "ffmpeg:-an")
}
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
}else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
}
}
DownloadViewModel.Type.command -> {
request.addOption(
"--config-locations",
File(context.cacheDir.absolutePath + "/downloads/config${System.currentTimeMillis()}.txt").apply {
writeText(downloadItem.format.format_note)
}.absolutePath
)
request.addOption("-P", tempFileDir.absolutePath)
}
}
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val logItem = LogItem(
0,
downloadItem.title,
downloadItem.title.ifEmpty { downloadItem.url },
"Downloading:\n" +
"Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" +
"Type: ${downloadItem.type}\n" +
"Format: ${downloadItem.format}\n\n" +
"Command: ${java.lang.String.join(" ", request.buildCommand())}\n\n"
"Command: ${java.lang.String.join(" ", request.buildCommand())}\n\n",
downloadItem.format,
downloadItem.type,
System.currentTimeMillis(),
)

View file

@ -15,8 +15,10 @@ import androidx.work.workDataOf
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.more.TerminalActivity
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
@ -83,10 +85,13 @@ class TerminalDownloadWorker(
val logItem = LogItem(
0,
"Terminal Download at " + Calendar.getInstance().time,
"Terminal Download",
"Downloading:\n" +
"Terminal Download\n" +
"Command: ${command}\n\n"
"Command: ${command}\n\n",
Format(),
DownloadViewModel.Type.command,
System.currentTimeMillis(),
)
runCatching {

View file

@ -15,7 +15,9 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<com.google.android.exoplayer2.ui.StyledPlayerView
<androidx.media3.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -159,7 +161,7 @@
app:layout_constraintStart_toEndOf="@+id/colon">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text" />

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/download_log_item_constraint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="15dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/title"
android:clickable="true"
android:background="?android:attr/selectableItemBackground"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:padding="10dp"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:focusable="true" />
<Button
android:id="@+id/delete"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:padding="15dp"
android:layout_height="wrap_content"
app:icon="@drawable/ic_delete_all"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_extra_commands"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/current"
android:layout_width="match_parent"
android:layout_marginVertical="10dp"
app:layout_constraintTop_toBottomOf="@+id/title"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:padding="10dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/currentTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/current"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/currentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_marginVertical="10dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/current">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/command"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:fontFamily="monospace"
android:maxLines="2000" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/okButton"
android:layout_marginVertical="10dp"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok"
app:icon="@drawable/ic_check"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -10,7 +10,7 @@
<TextView
android:layout_width="wrap_content"
android:text="@string/format"
android:textSize="12sp"
android:textSize="14sp"
android:layout_margin="20dp"
android:layout_height="wrap_content"/>

View file

@ -218,6 +218,22 @@
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.chip.Chip
android:id="@+id/extra_commands"
android:visibility="gone"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/use_extra_commands"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_terminal"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

View file

@ -296,6 +296,22 @@
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.chip.Chip
android:id="@+id/extra_commands"
android:visibility="gone"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/use_extra_commands"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_terminal"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

View file

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView android:id="@+id/log_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:checkedIcon="@null"
app:strokeColor="?attr/colorPrimary"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
app:strokeWidth="0dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:layout_height="wrap_content">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/log_icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="20dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_baseline_file_open_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
app:layout_constraintHorizontal_weight="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_item_data"
android:layout_width="0dp"
android:clickable="false"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.7"
app:layout_constraintStart_toEndOf="@+id/log_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="false"
android:ellipsize="end"
android:maxLines="2"
android:paddingHorizontal="5dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/action_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="title,action_button" />
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:clickable="false"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/downloadedTime"
style="@style/Widget.Material3.FloatingActionButton.Large.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</HorizontalScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/download_type"
android:clickable="false"
style="?attr/materialIconButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:padding="0dp"
app:cornerRadius="10dp"
app:icon="@drawable/ic_video"
app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,23 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<item
android:id="@+id/delete_results"
android:title="@string/remove_results"
android:icon="@drawable/baseline_delete_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/select_all"
android:title="@string/select_all"
app:showAsAction="never" />
<item
android:id="@+id/invert_selected"
android:title="@string/invert_selected"
app:showAsAction="never" />
</menu>

View file

@ -273,7 +273,7 @@
<string name="custom_audio_quality">Përdor cilësinë e personalizuar të audios</string>
<string name="piped_instance">Shembull me tubacione</string>
<string name="piped_instance_summary">Shkruani një Server API PIPED, aplikacioni mund të përdorë për kërkimet dhe formatet në youtube</string>
<string name="retries">Riprovon</string>
<string name="retries">Riprovimet</string>
<string name="fragment_retries">Riprovimet e fragmentit</string>
<string name="format_order">Renditja e formatit</string>
</resources>

View file

@ -284,4 +284,7 @@
<string name="format_order">Format Order</string>
<string name="force_keyframes">Force Keyframes At Cuts</string>
<string name="force_keyframes_summary">Accurate Video Cuts. Slower</string>
<string name="use_extra_commands">Add Extra Commands</string>
<string name="use_extra_commands_summary">For Audio / Video downloads. Add extra commands along with the GUI configuration</string>
<string name="current">Current Command</string>
</resources>

View file

@ -140,4 +140,13 @@
app:defaultValue=""
app:summary="@string/preferred_format_id_summary"
app:title="@string/preferred_format_id" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_terminal"
app:key="use_extra_commands"
android:summary="@string/use_extra_commands_summary"
app:title="@string/use_extra_commands" />
</PreferenceScreen>

View file

@ -14,7 +14,7 @@
<Preference
android:key="downloading"
app:icon="@drawable/ic_downloads"
android:title="@string/downloading"/>
android:title="@string/downloads"/>
<Preference
android:key="processing"

View file

@ -22,7 +22,7 @@ buildscript {
retrofitVer = "2.9.0"
kodeinVer = "7.16.0"
navVer = "2.5.3"
media3_version = "1.0.0"
media3_version = "1.1.0"
}
repositories {
mavenCentral()