add compatible mode and burn subtitle postprocessors

pull/1362/head
deniscerri 1 week ago
parent 447645dbec
commit 5dc3e510ba
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -0,0 +1,35 @@
import os
from yt_dlp.postprocessor.ffmpeg import FFmpegPostProcessor
class BurnSubsPP(FFmpegPostProcessor):
def run(self, info):
subs = info.get('requested_subtitles') or {}
# Find subtitle language key and filepath
sub_info = next(((lang, s.get('filepath')) for lang, s in subs.items() if s.get('filepath')), None)
if not sub_info or not sub_info[1]:
self.to_screen('No subtitle file found; skipping burn-in')
return [], info
lang, sub = sub_info
path = info['filepath']
# Terminal updates
self.to_screen(f'Found subtitles [{lang}]: {os.path.basename(sub)}')
self.to_screen(f'Burning subtitles into {os.path.basename(path)}...')
temp = f'{path}.burn.mp4'
# libass parses this as a filter-graph arg: escape ':' and '\'
esc = sub.replace('\\', '/').replace(':', r'\:')
self.run_ffmpeg_multiple_files(
[path], temp,
['-vf', f"subtitles='{esc}':fontsdir=/system/fonts", '-c:a', 'copy']
)
os.replace(temp, path)
info['filepath'] = path
self.to_screen(f'Successfully completed subtitle burn-in.')
return [], info

@ -0,0 +1,65 @@
from pathlib import Path
from yt_dlp.postprocessor.ffmpeg import FFmpegPostProcessor
class CompatibleRecoderPP(FFmpegPostProcessor):
def run(self, info):
filepath = Path(info['filepath'])
vcodec = (info.get('vcodec') or '').lower()
acodec = (info.get('acodec') or '').lower()
video_ok = vcodec.startswith(('h264', 'avc'))
audio_ok = acodec.startswith(('aac', 'mp4a'))
if video_ok and audio_ok:
self.to_screen(f'Not converting {filepath}')
return [], info
if not filepath.exists():
self.report_error(f'{filepath} does not exist!')
return [], info
new_filepath = filepath.with_suffix('.new.mp4')
if new_filepath.exists():
self.report_error(f'{new_filepath} already exists')
return [], info
# Copy streams that already satisfy the codec requirements.
video_codec = 'copy' if video_ok else 'libx264'
audio_codec = 'copy' if audio_ok else 'aac'
self.to_screen(
f'Re-encoding {filepath} '
f'(video: {"copy" if video_ok else "H.264"}, '
f'audio: {"copy" if audio_ok else "AAC"})')
try:
self.run_ffmpeg(
str(filepath),
str(new_filepath),
[
'-c:v', video_codec,
'-c:a', audio_codec,
'-f', 'mp4',
],
)
except Exception as e:
self.report_error(f'ffmpeg failed: {e}')
return [], info
filepath.unlink()
final_filepath = filepath.with_suffix('.mp4')
if final_filepath.exists():
self.report_error(f'{final_filepath} already exists')
return [], info
new_filepath.replace(final_filepath)
info['filepath'] = str(final_filepath)
return [], info

@ -32,6 +32,7 @@ import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.util.Collections
@ -180,6 +181,9 @@ object RuntimeManager {
// NODE_OPTIONS = "--require ${optionsFile.absolutePath}"
// }
val ytdlpPluginsFolder = File(FileUtil.getBundledYTDLPPluginsPath(appContext))
copyAssetFolder(appContext, "yt_dlp_plugins", ytdlpPluginsFolder)
initialized = true
} catch (e: Exception) {
e.printStackTrace()
@ -192,6 +196,38 @@ object RuntimeManager {
}
}
fun copyAssetFolder(context: Context, assetFolderPath: String, targetFolder: File) {
val assetManager = context.assets
val files = assetManager.list(assetFolderPath) ?: return
if (!targetFolder.exists()) {
targetFolder.mkdirs()
}
for (file in files) {
val assetPath = if (assetFolderPath.isEmpty()) file else "$assetFolderPath/$file"
val subFiles = assetManager.list(assetPath)
if (subFiles != null && subFiles.isNotEmpty()) {
// It's a directory -> recursively copy it
copyAssetFolder(context, assetPath, File(targetFolder, file))
} else {
// It's a file -> write to internal storage
copyAssetFile(context, assetPath, File(targetFolder, file))
}
}
}
private fun copyAssetFile(context: Context, assetPath: String, outFile: File) {
if (outFile.exists()) return // Skip if already copied
context.assets.open(assetPath).use { input ->
FileOutputStream(outFile).use { output ->
input.copyTo(output)
}
}
}
fun reInit(context: Context) {
synchronized(initLock) {
initialized = false

@ -11,6 +11,7 @@ data class VideoPreferences (
var sponsorBlockFilters: ArrayList<String> = arrayListOf(),
var writeSubs: Boolean = false,
var writeAutoSubs: Boolean = false,
var burnSubs: Boolean = false,
var subsLanguages: String = ".*-orig",
var audioFormatIDs : ArrayList<String> = arrayListOf(),
var removeAudio: Boolean = false,

@ -263,6 +263,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val burnSubs = sharedPreferences.getBoolean("burn_subtitles", false)
val recodeVideo = sharedPreferences.getBoolean("recode_video", false)
val compatibilityMode = sharedPreferences.getBoolean("compatible_video", false)
val removeAudio = sharedPreferences.getBoolean("remove_audio", false)
@ -310,6 +311,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
ArrayList(sponsorblock),
saveSubs,
saveAutoSubs,
burnSubs,
subsLanguages,
audioFormatIDs = preferredAudioFormats,
recodeVideo = recodeVideo,
@ -454,6 +456,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val burnSubs = sharedPreferences.getBoolean("burn_subtitles", false)
val recodeVideo = sharedPreferences.getBoolean("recode_video", false)
val removeAudio = sharedPreferences.getBoolean("remove_audio", false)
val compatibilityMode = sharedPreferences.getBoolean("compatible_video", false)
@ -511,6 +514,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
sponsorBlockFilters = ArrayList(sponsorblock),
writeSubs = saveSubs,
writeAutoSubs = saveAutoSubs,
burnSubs = burnSubs,
subsLanguages = subsLanguages,
recodeVideo = recodeVideo,
compatibilityMode = compatibilityMode,

@ -715,6 +715,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
items.forEach { it.videoPreferences.writeAutoSubs = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
burnSubtitlesClicked = { checked ->
items.forEach { it.videoPreferences.burnSubs = checked }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }
},
subtitleLanguagesSet = {value ->
items.forEach { it.videoPreferences.subsLanguages = value }
CoroutineScope(Dispatchers.IO).launch { items.forEach { downloadViewModel.updateDownload(it) } }

@ -477,6 +477,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
saveAutoSubtitlesClicked = {
downloadItem.videoPreferences.writeAutoSubs = it
},
burnSubtitlesClicked = {
downloadItem.videoPreferences.burnSubs = it
},
subtitleLanguagesSet = {
downloadItem.videoPreferences.subsLanguages = it
},

@ -389,6 +389,10 @@ object FileUtil {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/TERMINAL_CACHE"
}
fun getBundledYTDLPPluginsPath(context: Context) : String {
return File(context.filesDir, "yt_dlp_plugins").absolutePath
}
fun getCookieFile(context : Context, ignoreIfExists: Boolean = false, path: (path: String) -> Unit){
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (ignoreIfExists || cookiesFile.exists()){

@ -1333,6 +1333,7 @@ object UiUtil {
filenameTemplateSet: (String) -> Unit,
saveSubtitlesClicked: (Boolean) -> Unit,
saveAutoSubtitlesClicked: (Boolean) -> Unit,
burnSubtitlesClicked: (Boolean) -> Unit,
subtitleLanguagesSet: (String) -> Unit,
removeAudioClicked: (Boolean) -> Unit,
recodeVideoClicked: (Boolean) -> Unit,
@ -1456,6 +1457,7 @@ object UiUtil {
firstItem.videoPreferences.embedSubs,
firstItem.videoPreferences.writeSubs,
firstItem.videoPreferences.writeAutoSubs,
firstItem.videoPreferences.burnSubs,
)
adjustSubtitles.createBadge(context, count.filter { it }.size)
}
@ -1466,6 +1468,7 @@ object UiUtil {
val embedSubs = adjustSubtitleView.findViewById<MaterialSwitch>(R.id.embed_subtitles)
val saveSubtitles = adjustSubtitleView.findViewById<MaterialSwitch>(R.id.save_subs)
val saveAutoSubtitles = adjustSubtitleView.findViewById<MaterialSwitch>(R.id.save_auto_subs)
val burnSubtitles = adjustSubtitleView.findViewById<MaterialSwitch>(R.id.burn_subs)
val subtitleLanguages = adjustSubtitleView.findViewById<ConstraintLayout>(R.id.subtitle_languages)
val subtitleLanguagesDescription = adjustSubtitleView.findViewById<TextView>(R.id.subtitle)
subtitleLanguagesDescription.text = items.first().videoPreferences.subsLanguages
@ -1474,6 +1477,7 @@ object UiUtil {
embedSubs!!.isChecked = items.all { it.videoPreferences.embedSubs }
embedSubs.setOnClickListener {
subtitleLanguages.isClickable = embedSubs.isChecked || saveSubtitles.isChecked
burnSubtitles.isEnabled = embedSubs.isChecked
embedSubsClicked(embedSubs.isChecked)
items.forEach { it.videoPreferences.embedSubs = embedSubs.isChecked }
@ -1490,10 +1494,22 @@ object UiUtil {
subtitleLanguages.visibility = View.VISIBLE
}
if (items.all { it.videoPreferences.burnSubs}) {
burnSubtitles.isChecked = true
}
saveSubtitles.setOnCheckedChangeListener { _, _ ->
subtitleLanguages.isClickable = embedSubs.isChecked || saveSubtitles.isChecked || saveAutoSubtitles.isChecked
saveSubtitlesClicked(saveSubtitles.isChecked)
items.forEach { it.videoPreferences.writeSubs = saveSubtitles.isChecked }
burnSubtitles.isEnabled = saveSubtitles.isChecked || saveAutoSubtitles.isChecked
if (!burnSubtitles.isEnabled) {
burnSubtitles.isChecked = false
burnSubtitlesClicked(burnSubtitles.isChecked)
items.forEach { it.videoPreferences.burnSubs = burnSubtitles.isChecked }
}
calculateAdjustSubtitlesChangeCount()
}
@ -1501,6 +1517,14 @@ object UiUtil {
subtitleLanguages.isClickable = embedSubs.isChecked || saveSubtitles.isChecked || saveAutoSubtitles.isChecked
saveAutoSubtitlesClicked(saveAutoSubtitles.isChecked)
items.forEach { it.videoPreferences.writeAutoSubs = saveAutoSubtitles.isChecked }
burnSubtitles.isEnabled = saveSubtitles.isChecked || saveAutoSubtitles.isChecked
if (!burnSubtitles.isEnabled) {
burnSubtitles.isChecked = false
burnSubtitlesClicked(burnSubtitles.isChecked)
items.forEach { it.videoPreferences.burnSubs = burnSubtitles.isChecked }
}
calculateAdjustSubtitlesChangeCount()
}
@ -1519,6 +1543,13 @@ object UiUtil {
}
}
burnSubtitles.isEnabled = saveSubtitles.isChecked || saveAutoSubtitles.isChecked
burnSubtitles.setOnCheckedChangeListener { _, _ ->
burnSubtitlesClicked(burnSubtitles.isChecked)
items.forEach { it.videoPreferences.burnSubs = burnSubtitles.isChecked }
calculateAdjustSubtitlesChangeCount()
}
val adjustSubtitleDialog = MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.subtitles))
.setView(adjustSubtitleView)

@ -929,6 +929,7 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
}
request.addOption("--newline")
request.addOption("--plugin-dirs", FileUtil.getBundledYTDLPPluginsPath(context))
val metadataCommands = StringJoiner(" ")
@ -1387,9 +1388,9 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
var vCodecPref = context.getStringArray(R.array.video_codec_values_ytdlp)[vCodecPrefIndex]
if (downloadItem.videoPreferences.compatibilityMode) {
request.addOption("--recode-video", "mp4")
request.addOption("--merge-output-format", "mp4/mkv")
request.addOption("--ppa", "VideoConvertor+ffmpeg_o:-profile:v baseline")
request.addOption("--merge-output-format", "mp4")
request.addOption("--remux-video", "mp4")
request.addOption("--use-postprocessor", "CompatibleRecoder:when=after_move")
vCodecPref = "h264"
aCodecPref = "aac"
}
@ -1570,6 +1571,8 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
request.addOption("--write-auto-subs")
}
if (downloadItem.videoPreferences.embedSubs) {
if (sharedPreferences.getBoolean("no_keep_subs", false) && (downloadItem.videoPreferences.writeSubs || downloadItem.videoPreferences.writeAutoSubs)) {
request.addOption("--compat-options", "no-keep-subs")
@ -1587,6 +1590,10 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { ".*-orig" })
}
if (downloadItem.videoPreferences.burnSubs) {
request.addOption("--use-postprocessor", "BurnSubs:when=after_move")
}
var copyStream = ""
if (downloadItem.videoPreferences.cropValues.isNotBlank()){

@ -27,6 +27,14 @@
android:text="@string/save_auto_subs"
android:layout_height="wrap_content" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/burn_subs"
android:layout_width="match_parent"
android:layout_marginHorizontal="20dp"
android:enabled="false"
android:text="@string/burn_subs"
android:layout_height="wrap_content" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"

@ -582,6 +582,7 @@
<string name="exit">Exit</string>
<string name="packages_description">Packages are helper applications YTDLnis uses to help with yt-dlp commands. Some of them are already bundled, but you can install newer versions that are published without the need to update the app. The installed apk\'s replace the bundled version that the app uses.</string>
<string name="bgutils_pot_generation_script_info">In order to use the BgUtils POT Provider, the app needs to download Brainicism/bgutil-ytdlp-pot-provider repo from github and initiate the installation. This might take a while to finish. Proceed now?</string>
<string name="burn_subs">Burn subtitles</string>
<plurals name="every_hours"><item quantity="one">Every hour</item><item quantity="other">Every %d hours</item></plurals>
<plurals name="every_days"><item quantity="one">Every day</item><item quantity="other">Every %d days</item></plurals>
<plurals name="every_weeks"><item quantity="one">Every week</item><item quantity="other">Every %d weeks</item></plurals>

Loading…
Cancel
Save