pull/332/head
zaednasr 3 years ago
parent 3b3873de49
commit ef5b68ffc1
No known key found for this signature in database
GPG Key ID: 92B1DE23AD3D0E9E

@ -0,0 +1,598 @@
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "5cea521e1a26473b2ec5298791f10ac4",
"entities": [
{
"tableName": "results",
"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, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER, `creationTime` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistTitle",
"columnName": "playlistTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formats",
"columnName": "formats",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "urls",
"columnName": "urls",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "chapters",
"columnName": "chapters",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "playlistURL",
"columnName": "playlistURL",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "playlistIndex",
"columnName": "playlistIndex",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "creationTime",
"columnName": "creationTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "history",
"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, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadId` INTEGER NOT NULL DEFAULT 0, `command` TEXT NOT NULL DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downloadPath",
"columnName": "downloadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadId",
"columnName": "downloadId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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, `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, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "container",
"columnName": "container",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Default'"
},
{
"fieldPath": "downloadSections",
"columnName": "downloadSections",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "allFormats",
"columnName": "allFormats",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadPath",
"columnName": "downloadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadSize",
"columnName": "downloadSize",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistTitle",
"columnName": "playlistTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "audioPreferences",
"columnName": "audioPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoPreferences",
"columnName": "videoPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extraCommands",
"columnName": "extraCommands",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "customFileNameTemplate",
"columnName": "customFileNameTemplate",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "SaveThumb",
"columnName": "SaveThumb",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Queued'"
},
{
"fieldPath": "downloadStartTime",
"columnName": "downloadStartTime",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "logID",
"columnName": "logID",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "playlistURL",
"columnName": "playlistURL",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "playlistIndex",
"columnName": "playlistIndex",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "commandTemplates",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `useAsExtraCommand` INTEGER NOT NULL DEFAULT 0, `useAsExtraCommandAudio` INTEGER NOT NULL DEFAULT 1, `useAsExtraCommandVideo` INTEGER NOT NULL DEFAULT 1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "useAsExtraCommand",
"columnName": "useAsExtraCommand",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "useAsExtraCommandAudio",
"columnName": "useAsExtraCommandAudio",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "useAsExtraCommandVideo",
"columnName": "useAsExtraCommandVideo",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistory",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "templateShortcuts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cookies",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"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": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "terminalDownloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `command` TEXT NOT NULL, `log` TEXT DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "log",
"columnName": "log",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"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, '5cea521e1a26473b2ec5298791f10ac4')"
]
}
}

@ -23,7 +23,7 @@ import com.deniscerri.ytdlnis.database.models.*
LogItem::class,
TerminalItem::class
],
version = 12,
version = 13,
autoMigrations = [
AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3),
@ -35,7 +35,8 @@ import com.deniscerri.ytdlnis.database.models.*
AutoMigration (from = 8, to = 9),
AutoMigration (from = 9, to = 10),
AutoMigration (from = 10, to = 11),
AutoMigration (from = 11, to = 12)
AutoMigration (from = 11, to = 12),
AutoMigration (from = 12, to = 13)
]
)
abstract class DBManager : RoomDatabase(){

@ -40,6 +40,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')")
fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedDownloads() : PagingSource<Int, DownloadItemSimple>
@ -55,6 +56,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloads() : PagingSource<Int, DownloadItemSimple>
@ -64,12 +66,14 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'")
fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloads() : PagingSource<Int, DownloadItemSimple>

@ -37,6 +37,6 @@ interface ResultDao {
suspend fun delete(id: Long)
@Query("SELECT * FROM results WHERE url=:url LIMIT 1")
fun getResultByURL(url: String) : ResultItem
fun getResultByURL(url: String) : ResultItem?
}

@ -38,5 +38,9 @@ data class DownloadItem(
var status: String,
@ColumnInfo(defaultValue = "0")
var downloadStartTime: Long,
var logID: Long?
var logID: Long?,
@ColumnInfo(defaultValue = "")
var playlistURL: String? = "",
@ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null
) : Parcelable

@ -22,5 +22,9 @@ data class ResultItem(
@ColumnInfo(defaultValue = "")
var urls: String,
var chapters: MutableList<ChapterItem>?,
@ColumnInfo(defaultValue = "")
var playlistURL: String? = "",
@ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null,
var creationTime: Long = System.currentTimeMillis() / 1000,
) : Parcelable

@ -52,7 +52,12 @@ class HistoryRepository(private val historyDao: HistoryDao) {
}
}
suspend fun deleteAll(){
suspend fun deleteAll(deleteFile: Boolean = false){
if (deleteFile){
historyDao.getAllHistoryList().forEach { item ->
FileUtil.deleteFile(item.downloadPath)
}
}
historyDao.deleteAll()
}

@ -44,7 +44,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll()
itemCount.value = v.size
}else{
v.forEach { it?.playlistTitle = "ytdlnis-Search" }
v.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it?.playlistTitle = "ytdlnis-Search" }
}
resultDao.insertMultiple(v)
return ArrayList(v)
@ -77,7 +77,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll()
itemCount.value = items.size
}else{
items.forEach { it!!.playlistTitle = "ytdlnis-Search" }
items.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it!!.playlistTitle = "ytdlnis-Search" }
}
resultDao.insertMultiple(items.toList())
return items
@ -96,7 +96,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
resultDao.update(item)
}
fun getItemByURL(url: String): ResultItem {
fun getItemByURL(url: String): ResultItem? {
return resultDao.getResultByURL(url)
}
}

@ -152,15 +152,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
Format(
"best",
audioContainer!!,
"",
"",
"",
0,
"best"
)
infoUtil.getGenericAudioFormats(resources).last()
}else{
Format(
audioFormatIDPreference.first().split("+").first(),
@ -260,23 +252,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
val preferredAudioFormats = arrayListOf<String>()
for (f in resultItem.formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { resultItem.formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(resultItem.formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
val videoPreferences = VideoPreferences(
embedSubs,
@ -303,7 +280,15 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
container!!,
"",
resultItem.formats,
downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences, extraCommands, customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Queued.toString(), 0, null
downloadPath!!, resultItem.website,
"",
resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
)
}
@ -321,6 +306,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
downloadItem.allFormats,
"",
arrayListOf(),
downloadItem.playlistURL,
downloadItem.playlistIndex,
System.currentTimeMillis()
)
@ -339,6 +326,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
@ -357,6 +346,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
@ -493,6 +484,26 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
}
fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
val preferredAudioFormats = arrayListOf<String>()
for (f in formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
return preferredAudioFormats
}
fun generateCommandFormat(c: CommandTemplate) : Format {
return Format(
c.title,

@ -92,8 +92,8 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
repository.delete(item, deleteFile)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
fun deleteAll(deleteFile: Boolean = false) = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll(deleteFile)
}
fun deleteDuplicates() = viewModelScope.launch(Dispatchers.IO) {

@ -151,7 +151,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
repository.update(item)
}
fun getItemByURL(url: String) : ResultItem {
fun getItemByURL(url: String) : ResultItem? {
return repository.getItemByURL(url)
}
@ -233,11 +233,12 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
val formats = infoUtil.getFormats(result.url)
updatingFormats.emit(false)
if (formats.isNotEmpty() && isActive) {
val res = getItemByURL(result.url)
res.formats = formats.toMutableList()
update(res)
getItemByURL(result.url)?.apply {
this.formats = formats.toMutableList()
update(this)
}
updateFormatsResultData.emit(formats.toMutableList())
}
updateFormatsResultData.emit(formats.toMutableList())
}
updateFormatsResultDataJob?.start()
updateFormatsResultDataJob?.invokeOnCompletion {

@ -74,7 +74,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
// TITLE ----------------------------------
val videoTitle = card.findViewById<TextView>(R.id.result_title)
var title = video!!.title
var title = video!!.title.ifBlank { video.url }
if (title.length > 100) {
title = title.substring(0, 40) + "..."
}

@ -198,24 +198,27 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) {
override fun onFormatClick(item: List<FormatTuple>) {
downloadItem.format = item.first().format
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null)
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem?.formats?.removeAll(formats.toSet())
resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
if (resultItem != null){
resultViewModel.update(resultItem!!)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(resultItem)
}
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
lifecycleScope.launch(Dispatchers.IO) {
resultItem?.formats?.removeAll(formats.toSet())
resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
if (resultItem != null){
resultViewModel.update(resultItem!!)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(resultItem)
}
}
}
formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList()
formats.removeAll(genericAudioFormats)
val preferredFormat = downloadViewModel.getFormat(formats, Type.audio)
UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, null)
}
}
formatCard.setOnClickListener{

@ -19,6 +19,7 @@ import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
@ -440,20 +441,42 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
delay(500)
runCatching {
val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = true
isClickable = true
setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
resultViewModel.cancelUpdateFormatsItemData()
}
}
}
}
runCatching {
val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = true
isClickable = true
setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
resultViewModel.cancelUpdateFormatsItemData()
}
}
}
}
}else{
runCatching {
val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = false
isClickable = false
}
}
runCatching {
val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = false
isClickable = false
}
}
}
}
@ -569,12 +592,22 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
when(viewPager2.currentItem){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
2 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
else -> {}
}

@ -44,7 +44,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class DownloadCommandFragment(private val resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment() {
class DownloadCommandFragment(private val resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment(), GUISync {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
@ -296,4 +296,12 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
))
}
}
override fun updateTitleAuthor(t: String, a: String) {
downloadItem.title = t
downloadItem.author = a
}
override fun updateUI(res: ResultItem?) {
}
}

@ -208,18 +208,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
val formatListener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, selectedFormats: List<FormatTuple>) {
val formatCollection = mutableListOf<List<Format>>()
allFormats.forEach {f ->
formatCollection.add(f.mapTo(mutableListOf()) {it.copy()})
}
override fun onFormatClick(selectedFormats: List<FormatTuple>) {
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
}
i.format = selectedFormats[index].format
if (i.type == DownloadViewModel.Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
}
@ -229,6 +219,35 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
lifecycleScope.launch(Dispatchers.IO){
val formatCollection = mutableListOf<List<Format>>()
allFormats.forEach {f ->
formatCollection.add(f.mapTo(mutableListOf()) {it.copy()})
}
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
}
i.format = downloadViewModel.getFormat(i.allFormats, i.type)
kotlin.runCatching {
val resultItem = resultViewModel.getItemByURL(i.url)!!
resultItem.formats = formatCollection[index].toMutableList()
resultViewModel.update(resultItem)
}
}
withContext(Dispatchers.Main){
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
}
}
}
override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch {
val ids = mutableListOf<Long>()
@ -637,7 +656,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch{
val downloadItem = items.find { it.url == itemURL }
val resultItem = withContext(Dispatchers.IO){
resultViewModel.getItemByURL(downloadItem!!.url)
resultViewModel.getItemByURL(downloadItem!!.url)!!
}
if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem!!, this@DownloadMultipleBottomSheetDialog)

@ -210,12 +210,17 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) {
override fun onFormatClick(item: List<FormatTuple>) {
downloadItem.format = item.first().format
item.first().audioFormats?.map { it.format_id }?.let {
downloadItem.videoPreferences.audioFormatIDs.clear()
downloadItem.videoPreferences.audioFormatIDs.addAll(it)
}
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats)
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem?.formats?.removeAll(formats)
@ -230,8 +235,11 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
}
}
formats = allFormats.first().filter { !genericVideoFormats.contains(it) }.toMutableList()
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats)
val preferredFormat = downloadViewModel.getFormat(formats, Type.video)
val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats)
UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, formats.filter { preferredAudioFormats.contains(it.format_id) })
}
}
formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){

@ -24,6 +24,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
@ -48,6 +50,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var continueInBackgroundSnackBar : Snackbar
private lateinit var view: View
private var updateFormatsJob: Job? = null
enum class FormatSorting {
filesize, container, codec, id
@ -88,7 +91,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
okBtn = view.findViewById(R.id.format_ok)
shimmers.visibility = View.GONE
val hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" }
val hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" }
if (items.size > 1){
if (!hasGenericFormats){
@ -141,56 +144,77 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE
shimmers.startShimmer()
updateFormatsJob = launch(Dispatchers.IO) {
//simple download
if (items.size == 1) {
val res = infoUtil.getFormats(items.first()!!.url)
res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) {
res.filter { it.format_note.contains("audio", ignoreCase = true) }
} else {
res
}
if (chosenFormats.isEmpty()) throw Exception()
//simple download
if (items.size == 1){
val res = withContext(Dispatchers.IO){
infoUtil.getFormats(items.first()!!.url)
}
res.filter { it.format_note != "storyboard" }
chosenFormats = if(items.first()?.type == Type.audio){
res.filter { it.format_note.contains("audio", ignoreCase = true) }
formats = listOf(res)
withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats)
}
//list format filtering
}else{
res
}
if (chosenFormats.isEmpty()) throw Exception()
formats = listOf(res)
//list format filtering
}else{
var progress = "0/${items.size}"
formatCollection.clear()
refreshBtn.text = progress
withContext(Dispatchers.IO){
var progress = "0/${items.size}"
formatCollection.clear()
withContext(Dispatchers.Main) {
refreshBtn.text = progress
}
infoUtil.getFormatsMultiple(items.map { it!!.url }) {
lifecycleScope.launch(Dispatchers.Main){
progress = "${formatCollection.size}/${items.size}"
refreshBtn.text = progress
formatCollection.add(it)
if (isActive) {
lifecycleScope.launch(Dispatchers.Main) {
formatCollection.add(it)
progress = "${formatCollection.size}/${items.size}"
refreshBtn.text = progress
}
}
}
formats = formatCollection
withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats)
}
val flatFormatCollection = formatCollection.flatten()
val commonFormats =
flatFormatCollection.groupingBy { it.format_id }.eachCount()
.filter { it.value == items.size }
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }
.map { it.value }
chosenFormats = commonFormats.filter { it.filesize != 0L }
.mapTo(mutableListOf()) { it.copy() }
chosenFormats = when (items.first()?.type) {
Type.audio -> chosenFormats.filter {
it.format_note.contains(
"audio",
ignoreCase = true
)
}
else -> chosenFormats
}
if (chosenFormats.isEmpty()) throw Exception()
chosenFormats.forEach {
it.filesize =
flatFormatCollection.filter { f -> f.format_id == it.format_id }
.sumOf { itt -> itt.filesize }
}
}
formats = formatCollection
val flatFormatCollection = formatCollection.flatten()
val commonFormats = flatFormatCollection.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }.map { it.value }
chosenFormats = commonFormats.filter { it.filesize != 0L }.mapTo(mutableListOf()) {it.copy()}
chosenFormats = when(items.first()?.type){
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
else -> chosenFormats
}
if (chosenFormats.isEmpty()) throw Exception()
chosenFormats.forEach {
it.filesize =
flatFormatCollection.filter { f -> f.format_id == it.format_id }
.sumOf { itt -> itt.filesize }
withContext(Dispatchers.Main){
shimmers.visibility = View.GONE
shimmers.stopShimmer()
addFormatsToView()
refreshBtn.visibility = View.GONE
formatListLinearLayout.visibility = View.VISIBLE
}
}
shimmers.visibility = View.GONE
shimmers.stopShimmer()
addFormatsToView()
refreshBtn.visibility = View.GONE
formatListLinearLayout.visibility = View.VISIBLE
updateFormatsJob?.start()
}catch (e: Exception){
runCatching {
refreshBtn.isEnabled = true
@ -223,7 +247,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private fun returnFormats(){
//simple video format selection
if (items.size == 1){
listener.onFormatClick(formats, listOf(FormatTuple(selectedVideo, selectedAudios)))
listener.onFormatClick(listOf(FormatTuple(selectedVideo, selectedAudios)))
}else{
//playlist format selection
val selectedFormats = mutableListOf<Format>()
@ -235,7 +259,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
selectedFormats.add(selectedVideo)
}
}
listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, selectedAudios) })
listener.onFormatClick(selectedFormats.map { FormatTuple(it, selectedAudios) })
}
}
@ -317,7 +341,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}
}else{
if (items.size == 1){
listener.onFormatClick(formats, listOf(FormatTuple(format, null)))
listener.onFormatClick(listOf(FormatTuple(format, null)))
}else{
val selectedFormats = mutableListOf<Format>()
formatCollection.forEach {
@ -328,7 +352,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
selectedFormats.add(format)
}
}
listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, null) })
listener.onFormatClick(selectedFormats.map { FormatTuple(it, null) })
}
dismiss()
}
@ -360,14 +384,16 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private fun cleanUp(){
kotlin.runCatching {
updateFormatsJob?.cancel()
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit()
}
}
}
interface OnFormatClickListener{
fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>)
fun onFormatClick(selectedFormats: List<FormatTuple>)
fun onContinueOnBackground() {}
fun onFormatsUpdated(allFormats: List<List<Format>>)
}
class FormatTuple internal constructor(

@ -178,7 +178,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)!!
withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
Pair("result", resultItem),

@ -252,12 +252,16 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
if(allhistoryList!!.isEmpty()){
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
}else{
val deleteFile = booleanArrayOf(false)
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
deleteDialog.setTitle(getString(R.string.confirm_delete_history))
deleteDialog.setMessage(getString(R.string.confirm_delete_history_desc))
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setMultiChoiceItems(
arrayOf(getString(R.string.delete_files_too)),
booleanArrayOf(false)
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
historyViewModel.deleteAll()
historyViewModel.deleteAll(deleteFile[0])
}
deleteDialog.show()
}

@ -1,10 +1,14 @@
package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri
import android.util.DisplayMetrics
import android.view.MotionEvent
import android.view.View
@ -15,6 +19,7 @@ import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.io.File
import java.util.regex.Pattern
@ -91,4 +96,14 @@ object Extensions {
.setTrackDrawable(drawable)
.build()
}
fun File.getMediaDuration(context: Context): Int {
if (!exists()) return 0
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, Uri.parse(absolutePath))
val duration = retriever.extractMetadata(METADATA_KEY_DURATION)
retriever.release()
return duration?.toIntOrNull()?.div(1000) ?: 0
}
}

@ -247,7 +247,7 @@ object FileUtil {
fun scanMedia(files: List<String>, context: Context) : List<String> {
try {
val paths = files.sortedByDescending { File(it).length() }
val paths = files.distinct().sortedByDescending { File(it).length() }
runCatching {
paths.forEach {
MediaScannerConnection.scanFile(context, arrayOf(it), null, null)

@ -20,6 +20,7 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@ -115,7 +116,9 @@ class InfoUtil(private val context: Context) {
kotlin.runCatching {
val obj = dataArray.getJSONObject(i)
val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))
itm?.playlistTitle = res.getString("name") + "[${i+1}]"
itm?.playlistTitle = res.getString("name")
itm?.playlistURL = "https://www.youtube.com/playlist?list=$id"
itm?.playlistIndex = (i+1)
items.add(itm)
}
}
@ -432,6 +435,7 @@ class InfoUtil(private val context: Context) {
getFormatsFromYTDL(url)
}
}catch (e: Exception){
if (e is CancellationException) throw e
getFormatsFromYTDL(url)
}
}
@ -607,7 +611,9 @@ class InfoUtil(private val context: Context) {
youtubeDLResponse.out.split(lineSeparator!!)
} catch (e: Exception) {
listOf(youtubeDLResponse.out)
}.filter { it.isNotBlank() }
}.filter { it.isNotBlank() }.apply {
if (this.isEmpty()) throw Exception("Command Used: \n yt-dlp ${parseYTDLRequestString(request)}")
}
for (result in results) {
if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
@ -641,12 +647,17 @@ class InfoUtil(private val context: Context) {
}
}
val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) })
var playlistTitle: String? = ""
var playlistURL: String? = ""
var playlistIndex: Int? = null
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
if(playlistTitle?.removeSurrounding("\"").equals(query)) playlistTitle = ""
if (playlistTitle?.isNotBlank() == true){
playlistTitle += "[${jsonObject.getString("playlist_index")}]"
playlistURL = query
kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") }
}
val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
@ -692,7 +703,9 @@ class InfoUtil(private val context: Context) {
playlistTitle!!,
formats,
urls,
chapters
chapters,
playlistURL,
playlistIndex
)
)
}
@ -814,6 +827,8 @@ class InfoUtil(private val context: Context) {
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
}
@ -961,8 +976,15 @@ class InfoUtil(private val context: Context) {
}
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val url = downloadItem.url
val request = YoutubeDLRequest(url)
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank() || downloadItem.playlistIndex == null){
YoutubeDLRequest(downloadItem.url)
}else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply {
addOption("--playlist-start", downloadItem.playlistIndex!!)
addOption("--playlist-end", downloadItem.playlistIndex!!)
}
}
val type = downloadItem.type
val downDir : File
@ -996,6 +1018,12 @@ class InfoUtil(private val context: Context) {
val limitRate = sharedPreferences.getString("limit_rate", "")!!
if (limitRate.isNotBlank()) request.addOption("-r", limitRate)
val bufferSize = sharedPreferences.getString("buffer_size", "")!!
if (bufferSize.isNotBlank()){
request.addOption("--buffer-size", bufferSize)
request.addOption("--no-resize-buffer")
}
val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
@ -1019,6 +1047,8 @@ class InfoUtil(private val context: Context) {
val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!!
val aCodecPref = "ba[acodec~='^($preferredAudioCodec)']"
val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true)
var filenameTemplate = downloadItem.customFileNameTemplate
if(downloadItem.type != DownloadViewModel.Type.command){
request.addOption("--trim-filenames", downDir.absolutePath.length + 117)
@ -1075,9 +1105,9 @@ class InfoUtil(private val context: Context) {
request.addOption("--force-keyframes-at-cuts")
}
}
downloadItem.customFileNameTemplate += " %(section_title|)s "
filenameTemplate += " %(section_title|)s "
if (downloadItem.downloadSections.split(";").size > 1){
downloadItem.customFileNameTemplate += "%(autonumber)s"
filenameTemplate += "%(autonumber)s"
}
request.addOption("--output-na-placeholder", " ")
}
@ -1102,18 +1132,14 @@ class InfoUtil(private val context: Context) {
request.addOption("--write-description")
}
downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader,channel)s")
filenameTemplate = filenameTemplate.replace("%(uploader)s", "%(uploader,channel)s")
}
if (downloadItem.playlistTitle.isNotBlank()){
request.addOption("--parse-metadata","${downloadItem.playlistTitle.split("[")[0]}:%(playlist)s")
request.addOption("--parse-metadata","${downloadItem.playlistTitle}:%(playlist)s")
runCatching {
request.addOption("--parse-metadata",
downloadItem.playlistTitle
.substring(
downloadItem.playlistTitle.indexOf("[") + 1,
downloadItem.playlistTitle.indexOf("]"),
) + ":%(playlist_index)s"
downloadItem.playlistIndex.toString() + ":%(playlist_index)s"
)
}
}
@ -1149,7 +1175,7 @@ class InfoUtil(private val context: Context) {
request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
}else{
if (sharedPreferences.getBoolean("embed_metadata", true)){
if (embedMetadata){
request.addOption("--embed-metadata")
request.addOption("--parse-metadata", "%(release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s")
@ -1180,8 +1206,8 @@ class InfoUtil(private val context: Context) {
}
}
if (downloadItem.customFileNameTemplate.isNotBlank()){
request.addOption("-o", "${downloadItem.customFileNameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
}
}
@ -1355,8 +1381,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--split-chapters")
request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
}else{
if (downloadItem.customFileNameTemplate.isNotBlank()){
request.addOption("-o", "${downloadItem.customFileNameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
}
}
@ -1407,10 +1433,12 @@ class InfoUtil(private val context: Context) {
fun getGenericAudioFormats(resources: Resources) : MutableList<Format>{
val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val audioFormats = resources.getStringArray(R.array.audio_formats)
val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("audio_format", "")
val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")
audioFormats.reversed().forEach { formats.add(Format(it, containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormats.reverse()
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",acodecPreference!!, "",1, it)) }
return formats
}

@ -32,6 +32,7 @@ import java.util.Locale
class NotificationUtil(var context: Context) {
private val downloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_SERVICE_CHANNEL_ID)
private val workerNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_WORKER_CHANNEL_ID)
private val commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID)
private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID)
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
@ -97,6 +98,7 @@ class NotificationUtil(var context: Context) {
DOWNLOAD_SERVICE_CHANNEL_ID -> { return downloadNotificationBuilder}
COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID -> { return commandDownloadNotificationBuilder }
DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder }
DOWNLOAD_WORKER_CHANNEL_ID -> { return workerNotificationBuilder }
}
return downloadNotificationBuilder
}

@ -27,6 +27,7 @@ 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.util.Extensions.getMediaDuration
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
@ -217,10 +218,36 @@ class DownloadWorker(
}
}else{
val unixTime = System.currentTimeMillis() / 1000
val file = File(finalPaths?.first()!!)
downloadItem.format.filesize = file.length()
val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixTime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id, commandString)
historyDao.insert(historyItem)
finalPaths?.forEachIndexed {idx, ff ->
val file = File(ff)
var index = ""
var duration = downloadItem.duration
if (idx > 0) {
index = "[${idx + 1}] "
downloadItem.author = file.nameWithoutExtension
}
val d = file.getMediaDuration(context)
if (d > 0) duration = infoUtil.formatIntegerDuration(d, Locale.US)
downloadItem.format.filesize = file.length()
downloadItem.format.container = file.extension
downloadItem.duration = duration
val historyItem = HistoryItem(0,
downloadItem.url,
index + downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.type,
unixTime,
ff,
downloadItem.website,
downloadItem.format,
downloadItem.id,
commandString)
historyDao.insert(historyItem)
}
}
}

@ -38,7 +38,7 @@ class UpdatePlaylistFormatsWorker(
ids.forEach {
if (!isStopped){
val d = dao.getDownloadById(it)
val r = resDao.getResultByURL(d.url)
val r = resDao.getResultByURL(d.url)!!
if (d.allFormats.isNotEmpty()){
count++

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M5,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4L1,6v6h6L7,6L5,6L5,2zM9,16c0,1.3 0.84,2.4 2,2.82L11,23h2v-4.18c1.16,-0.41 2,-1.51 2,-2.82v-2L9,14v2zM1,16c0,1.3 0.84,2.4 2,2.82L3,23h2v-4.18C6.16,18.4 7,17.3 7,16v-2L1,14v2zM21,6L21,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4h-2v6h6L23,6h-2zM13,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4L9,6v6h6L15,6h-2L13,2zM17,16c0,1.3 0.84,2.4 2,2.82L19,23h2v-4.18c1.16,-0.41 2,-1.51 2,-2.82v-2h-6v2z"/>
</vector>

@ -62,6 +62,11 @@
<item>best</item>
</string-array>
<string-array name="audio_formats_values">
<item>wa</item>
<item>ba</item>
</string-array>
<string-array name="format_ordering">
<item>@string/file_size</item>
<item>@string/container</item>
@ -222,33 +227,29 @@
<array name="search_engines">
<item>YouTube</item>
<item>YouTube Music</item>
<item>Soundcloud</item>
<item>Bilibili</item>
<item>Nico video</item>
<item>PRX Series</item>
<item>PRX Stories</item>
<item>Rokfin</item>
<item>Yahoo screen</item>
<item>Soundcloud</item>
<item>Google Video</item>
<item>Bilibili</item>
<item>Netverse</item>
<item>All Clips of a trovo.live channel</item>
<item>All VODs of a trovo.live channel</item>
</array>
<array name="search_engines_values">
<item>ytsearch</item>
<item>ytsearchmusic</item>
<item>scsearch</item>
<item>bilisearch</item>
<item>nicosearch</item>
<item>PRX Series</item>
<item>PRX Stories</item>
<item>prxseries</item>
<item>prxstories</item>
<item>rkfnsearch</item>
<item>yvsearch</item>
<item>scsearch</item>
<item>gvsearch</item>
<item>bilisearch</item>
<item>netsearch</item>
<item>trovoclip</item>
<item>trovovod</item>
</array>
<array name="start_destination">

@ -323,4 +323,6 @@
<string name="export_file">Export File</string>
<string name="ytdl_source">yt-dlp Source</string>
<string name="update_ytdl_master">Master Version of yt-dlp</string>
<string name="buffer_size">Buffer Size</string>
<string name="buffer_size_summary">Size of download buffer, e.g. 1024 or 16K (default is 1024)</string>
</resources>

@ -99,6 +99,12 @@
app:summary="@string/limit_rate_summary"
app:title="@string/limit_rate" />
<EditTextPreference
android:icon="@drawable/baseline_settings_input_component_24"
app:key="buffer_size"
app:summary="@string/buffer_size_summary"
app:title="@string/buffer_size" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"

Loading…
Cancel
Save