add showing available subtitles when configuring subtitles

This commit is contained in:
deniscerri 2025-06-22 19:23:27 +02:00
parent 82365bc920
commit 900728ac28
No known key found for this signature in database
GPG key ID: 95C43D517D830350
27 changed files with 954 additions and 64 deletions

View file

@ -11,8 +11,8 @@ plugins {
def properties = new Properties() def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 8 def versionMinor = 8
def versionPatch = 4 def versionPatch = 5
def versionBuild = 1 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def isBeta = true def isBeta = true
def versionExt = "" def versionExt = ""
@ -175,6 +175,7 @@ dependencies {
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("com.squareup.picasso:picasso:2.71828") implementation("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.4.0")
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'

View file

@ -0,0 +1,780 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "c154a34aea99b9cbcfcc1dbb3763d314",
"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, `availableSubtitles` 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": "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
},
{
"fieldPath": "availableSubtitles",
"columnName": "availableSubtitles",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"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, `filesize` INTEGER NOT NULL DEFAULT 0, `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": "filesize",
"columnName": "filesize",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"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, `incognito` INTEGER NOT NULL DEFAULT 0, `availableSubtitles` 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": "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
},
{
"fieldPath": "incognito",
"columnName": "incognito",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "availableSubtitles",
"columnName": "availableSubtitles",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"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, `useAsExtraCommandDataFetching` INTEGER NOT NULL DEFAULT 0, `preferredCommandTemplate` INTEGER NOT NULL DEFAULT 0, `urlRegex` TEXT 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": "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"
},
{
"fieldPath": "useAsExtraCommandDataFetching",
"columnName": "useAsExtraCommandDataFetching",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "preferredCommandTemplate",
"columnName": "preferredCommandTemplate",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "urlRegex",
"columnName": "urlRegex",
"affinity": "TEXT",
"notNull": true
}
],
"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": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyTime` INTEGER NOT NULL, `weeklyConfig` TEXT, `monthlyConfig` TEXT, `status` TEXT NOT NULL, `startsTime` INTEGER NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `getOnlyNewUploads` INTEGER NOT NULL DEFAULT 0, `retryMissingDownloads` INTEGER NOT NULL, `ignoredLinks` TEXT NOT NULL DEFAULT '[]', `alreadyProcessedLinks` TEXT NOT NULL, `syncWithSource` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadItemTemplate",
"columnName": "downloadItemTemplate",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "everyNr",
"columnName": "everyNr",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "everyCategory",
"columnName": "everyCategory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "everyTime",
"columnName": "everyTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "weeklyConfig",
"columnName": "weeklyConfig",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "monthlyConfig",
"columnName": "monthlyConfig",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "startsTime",
"columnName": "startsTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endsDate",
"columnName": "endsDate",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "endsAfterCount",
"columnName": "endsAfterCount",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "runCount",
"columnName": "runCount",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "getOnlyNewUploads",
"columnName": "getOnlyNewUploads",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "retryMissingDownloads",
"columnName": "retryMissingDownloads",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ignoredLinks",
"columnName": "ignoredLinks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "alreadyProcessedLinks",
"columnName": "alreadyProcessedLinks",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "syncWithSource",
"columnName": "syncWithSource",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"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, 'c154a34aea99b9cbcfcc1dbb3763d314')"
]
}
}

View file

@ -41,7 +41,7 @@ import com.deniscerri.ytdl.database.models.TerminalItem
TerminalItem::class, TerminalItem::class,
ObserveSourcesItem::class ObserveSourcesItem::class
], ],
version = 22, version = 23,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), AutoMigration (from = 2, to = 3),
@ -64,6 +64,7 @@ import com.deniscerri.ytdl.database.models.TerminalItem
AutoMigration (from = 19, to = 20), AutoMigration (from = 19, to = 20),
//AutoMigration (from = 20, to = 21) MANUALLY HANDLED //AutoMigration (from = 20, to = 21) MANUALLY HANDLED
//AutoMigration(from = 21, to = 22) MANUALLY HANDLED //AutoMigration(from = 21, to = 22) MANUALLY HANDLED
//AutoMigration(from = 22, to = 23) MANUALLY HANDLED
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){

View file

@ -51,6 +51,15 @@ object Migrations {
// Add `urlRegex` as a JSON string (since lists are not supported in SQLite) // Add `urlRegex` as a JSON string (since lists are not supported in SQLite)
database.execSQL("ALTER TABLE commandTemplates ADD COLUMN urlRegex TEXT NOT NULL DEFAULT '[]'") database.execSQL("ALTER TABLE commandTemplates ADD COLUMN urlRegex TEXT NOT NULL DEFAULT '[]'")
},
//add available subtitles list in result and download item
Migration(22, 23) { database ->
//add available subtitles for result item
database.execSQL("ALTER TABLE results ADD COLUMN availableSubtitles TEXT NOT NULL DEFAULT '[]'")
//add available subtitles for download item
database.execSQL("ALTER TABLE downloads ADD COLUMN availableSubtitles TEXT NOT NULL DEFAULT '[]'")
} }
) )

View file

@ -44,5 +44,7 @@ data class DownloadItem(
@ColumnInfo(defaultValue = "") @ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null, var playlistIndex: Int? = null,
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
var incognito: Boolean = false var incognito: Boolean = false,
@ColumnInfo(defaultValue = "[]")
var availableSubtitles: List<String> = listOf()
) : Parcelable ) : Parcelable

View file

@ -27,4 +27,6 @@ data class ResultItem(
@ColumnInfo(defaultValue = "") @ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null, var playlistIndex: Int? = null,
var creationTime: Long = System.currentTimeMillis() / 1000, var creationTime: Long = System.currentTimeMillis() / 1000,
@ColumnInfo(defaultValue = "[]")
var availableSubtitles: List<String> = listOf()
) : Parcelable ) : Parcelable

View file

@ -345,7 +345,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
null, null,
playlistURL = resultItem.playlistURL, playlistURL = resultItem.playlistURL,
playlistIndex = resultItem.playlistIndex, playlistIndex = resultItem.playlistIndex,
incognito = sharedPreferences.getBoolean("incognito", false) incognito = sharedPreferences.getBoolean("incognito", false),
availableSubtitles = resultItem.availableSubtitles
) )
} }

View file

@ -22,6 +22,7 @@ import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.dp import com.deniscerri.ytdl.util.Extensions.dp
import com.deniscerri.ytdl.util.Extensions.loadBlurryThumbnail
import com.deniscerri.ytdl.util.Extensions.loadThumbnail import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -66,7 +67,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
// THUMBNAIL ---------------------------------- // THUMBNAIL ----------------------------------
val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue") val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")
uiHandler.post { thumbnail.loadThumbnail(hideThumb, item.thumb) } uiHandler.post { thumbnail.loadBlurryThumbnail(activity, hideThumb, item.thumb) }
// PROGRESS BAR ---------------------------------------------------- // PROGRESS BAR ----------------------------------------------------
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress) val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)

View file

@ -31,7 +31,7 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
subtitleLanguages?.summary = prefs.getString("subs_lang", "en.*,.*-orig")!! subtitleLanguages?.summary = prefs.getString("subs_lang", "en.*,.*-orig")!!
subtitleLanguages?.setOnPreferenceClickListener { subtitleLanguages?.setOnPreferenceClickListener {
UiUtil.showSubtitleLanguagesDialog(requireActivity(), prefs.getString("subs_lang", "en.*,.*-orig")!!){ UiUtil.showSubtitleLanguagesDialog(requireActivity(), listOf(), prefs.getString("subs_lang", "en.*,.*-orig")!!){
editor.putString("subs_lang", it) editor.putString("subs_lang", it)
editor.apply() editor.apply()
subtitleLanguages.summary = it subtitleLanguages.summary = it

View file

@ -52,6 +52,7 @@ import com.neoutils.highlight.core.util.UiColor
import com.neoutils.highlight.view.extension.toSpannedString import com.neoutils.highlight.view.extension.toSpannedString
import com.neoutils.highlight.view.text.HighlightTextWatcher import com.neoutils.highlight.view.text.HighlightTextWatcher
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.BlurTransformation
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,7 +65,6 @@ import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.pow
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@ -314,6 +314,22 @@ object Extensions {
} }
} }
fun ImageView.loadBlurryThumbnail(context: Context, hideThumb: Boolean, imageURL: String) {
if(!hideThumb){
if (imageURL.isNotEmpty()) {
Picasso.get()
.load(imageURL)
.resize(1280, 0)
.transform(BlurTransformation(context, 10, 1))
.onlyScaleDown()
.into(this)
} else {
Picasso.get().load(R.color.black).into(this)
}
}
}
fun List<DownloadRepository.Status>.toListString() : List<String>{ fun List<DownloadRepository.Status>.toListString() : List<String>{
return this.map { it.toString() } return this.map { it.toString() }
@ -554,7 +570,7 @@ object Extensions {
combine(flow2, flow3, ::Pair), combine(flow2, flow3, ::Pair),
combine(flow4, flow5, ::Pair), combine(flow4, flow5, ::Pair),
combine(flow6, flow7, ::Pair), combine(flow6, flow7, ::Pair),
) { t1, t2, t3, t4, -> ) { t1, t2, t3, t4 ->
transform( transform(
t1, t1,
t2.first, t2.first,

View file

@ -1067,7 +1067,7 @@ object UiUtil {
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun showSubtitleLanguagesDialog(context: Activity, currentValue: String, ok: (newValue: String) -> Unit){ fun showSubtitleLanguagesDialog(context: Activity, availableSubtitles: List<String>, currentValue: String, ok: (newValue: String) -> Unit){
val builder = MaterialAlertDialogBuilder(context) val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.subtitle_languages)) builder.setTitle(context.getString(R.string.subtitle_languages))
val view = context.layoutInflater.inflate(R.layout.subtitle_dialog, null) val view = context.layoutInflater.inflate(R.layout.subtitle_dialog, null)
@ -1093,8 +1093,6 @@ object UiUtil {
context.startActivity(browserIntent) context.startActivity(browserIntent)
} }
view.findViewById<View>(R.id.suggested).visibility = View.GONE
val dialog = builder.create() val dialog = builder.create()
dialog.show() dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
@ -1105,9 +1103,12 @@ object UiUtil {
//handle suggestion chips //handle suggestion chips
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val chipGroup = view.findViewById<ChipGroup>(R.id.subtitle_suggested_chipgroup) val suggestedLinearLayout = view.findViewById<LinearLayout>(R.id.suggestedLinear)
val chips = mutableListOf<Chip>() val suggestedChipGroup = view.findViewById<ChipGroup>(R.id.subtitle_suggested_chipgroup)
context.getStringArray(R.array.subtitle_langs).forEachIndexed { index, s ->
val allChipGroup = view.findViewById<ChipGroup>(R.id.subtitle_all_chipgroup)
fun buildChip(chipGroup: ChipGroup, s: String, index: Int) : Chip {
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
s.split("-", ".*").run { s.split("-", ".*").run {
if (s.endsWith(".*")) { if (s.endsWith(".*")) {
@ -1140,17 +1141,38 @@ object UiUtil {
} }
} }
chips.add(tmp) return tmp
} }
val suggestedChips = mutableListOf<Chip>()
val allChips = mutableListOf<Chip>()
//populate suggested
if (availableSubtitles.isNotEmpty()) {
suggestedLinearLayout.isVisible = true
availableSubtitles.forEachIndexed { index, s ->
suggestedChips.add(buildChip(suggestedChipGroup, s, index))
}
}
//populate all
context.getStringArray(R.array.subtitle_langs).filter { !availableSubtitles.contains(it) }.forEachIndexed { index, s ->
allChips.add(buildChip(allChipGroup, s, index))
}
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
view.findViewById<View>(R.id.suggested).visibility = View.VISIBLE suggestedChips.forEach {
chips.forEach {
it.isChecked = editText.text.split(",").any { it2 -> it2 == it.tag.toString() } it.isChecked = editText.text.split(",").any { it2 -> it2 == it.tag.toString() }
chipGroup!!.addView(it) suggestedChipGroup!!.addView(it)
}
allChips.forEach {
it.isChecked = editText.text.split(",").any { it2 -> it2 == it.tag.toString() }
allChipGroup!!.addView(it)
} }
editText.textChanged { editText.textChanged {
chipGroup.children.forEach { (it as Chip).isChecked = editText.text.split(",").any { it2 -> it2 == it.tag.toString() } } suggestedChipGroup.children.forEach { (it as Chip).isChecked = editText.text.split(",").any { it2 -> it2 == it.tag.toString() } }
allChipGroup.children.forEach { (it as Chip).isChecked = editText.text.split(",").any { it2 -> it2 == it.tag.toString() } }
} }
} }
} }
@ -1362,7 +1384,8 @@ object UiUtil {
"" ""
}.ifEmpty { sharedPreferences.getString("subs_lang", "en.*,.*-orig")!! } }.ifEmpty { sharedPreferences.getString("subs_lang", "en.*,.*-orig")!! }
showSubtitleLanguagesDialog(context, currentSubtitleLang){ val availabeSubtitles = if (items.size == 1) items[0].availableSubtitles else listOf()
showSubtitleLanguagesDialog(context, availabeSubtitles, currentSubtitleLang){
subtitleLanguagesSet(it) subtitleLanguagesSet(it)
subtitleLanguagesDescription.text = it subtitleLanguagesDescription.text = it
} }

View file

@ -87,10 +87,13 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
var extraCommands = commandTemplateDao.getAllTemplatesAsDataFetchingExtraCommands() var extraCommands = commandTemplateDao.getAllTemplatesAsDataFetchingExtraCommands()
if (url != null) { extraCommands = if (url != null) {
extraCommands = extraCommands.filter { it.urlRegex.any { u -> Regex(u).containsMatchIn(url) } } extraCommands.filter {
it.urlRegex.any { u -> Regex(u).containsMatchIn(url) } ||
it.urlRegex.isEmpty()
}
}else{ }else{
extraCommands = extraCommands.filter { it.urlRegex.isEmpty() } extraCommands.filter { it.urlRegex.isEmpty() }
} }
if (extraCommands.isNotEmpty()){ if (extraCommands.isNotEmpty()){
@ -242,19 +245,28 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
playlistTitle = title playlistTitle = title
} }
val availableSubtitles = mutableListOf<String>()
if (jsonObject.has("automatic_captions")) {
availableSubtitles.addAll(jsonObject.getJSONObject("automatic_captions").keys().asSequence().toList())
}
if (jsonObject.has("subtitles")) {
availableSubtitles.addAll(jsonObject.getJSONObject("subtitles").keys().asSequence().toList())
}
val res = ResultItem(0, val res = ResultItem(0,
url, url = url,
title, title = title,
author, author = author,
duration, duration = duration,
thumb!!, thumb = thumb!!,
website, website = website,
playlistTitle, playlistTitle = playlistTitle,
formats, formats = formats,
urls, urls = urls,
chapters, chapters = chapters,
playlistURL, playlistURL = playlistURL,
playlistIndex playlistIndex = playlistIndex,
availableSubtitles = availableSubtitles
) )
items.add(res) items.add(res)

View file

@ -141,6 +141,7 @@
android:ellipsize="end" android:ellipsize="end"
android:gravity="bottom" android:gravity="bottom"
android:maxLines="1" android:maxLines="1"
android:textColor="#FFF"
android:paddingStart="10dp" android:paddingStart="10dp"
android:paddingEnd="10dp" android:paddingEnd="10dp"
android:paddingBottom="5dp" android:paddingBottom="5dp"

View file

@ -26,7 +26,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="false" android:singleLine="false"
android:text="@string/command_templates" android:text="@string/command_templates"
android:textSize="25sp" android:textSize="20sp"
android:maxLines="2" android:maxLines="2"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"

View file

@ -30,7 +30,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/download" android:text="@string/download"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -35,7 +35,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/template" android:text="@string/template"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -56,7 +56,7 @@
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:singleLine="false" android:singleLine="false"
android:text="@string/download" android:text="@string/download"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button" app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -70,7 +70,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/download" android:text="@string/download"
android:textSize="25sp" android:textSize="20sp"
android:maxLines="2" android:maxLines="2"
android:singleLine="false" android:singleLine="false"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button" app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"

View file

@ -26,7 +26,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/Open_File" android:text="@string/Open_File"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -31,7 +31,7 @@
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:singleLine="false" android:singleLine="false"
android:text="@string/format" android:text="@string/format"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/format_buttons" app:layout_constraintEnd_toStartOf="@+id/format_buttons"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -31,7 +31,7 @@
android:maxLines="2" android:maxLines="2"
android:singleLine="false" android:singleLine="false"
android:text="@string/observe_sources" android:text="@string/observe_sources"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -67,14 +67,12 @@
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/check_all" android:id="@+id/check_all"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Primary"
android:layout_width="55dp" android:layout_width="55dp"
app:elevation="0dp" app:elevation="0dp"
android:elevation="0dp" android:elevation="0dp"
app:borderWidth="0dp" app:borderWidth="0dp"
android:contentDescription="@string/select_all" android:contentDescription="@string/select_all"
android:layout_height="55dp" android:layout_height="55dp"
android:backgroundTint="@android:color/transparent"
app:layout_anchor="@id/bottomAppBar" app:layout_anchor="@id/bottomAppBar"
android:layout_marginHorizontal="5dp" android:layout_marginHorizontal="5dp"
android:src="@drawable/ic_select_all" android:src="@drawable/ic_select_all"
@ -106,7 +104,7 @@
android:maxLines="2" android:maxLines="2"
android:singleLine="false" android:singleLine="false"
android:text="@string/select" android:text="@string/select"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_ok_button" app:layout_constraintEnd_toStartOf="@+id/bottomsheet_ok_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -24,8 +24,50 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/suggestedLinear"
android:orientation="vertical"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:textColor="?attr/colorPrimary"
android:textStyle="bold"
android:paddingVertical="15dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitle"
android:text="@string/suggested" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="170dp"
app:layout_constraintHeight_max="50dp"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/suggested">
<com.google.android.material.chip.ChipGroup
android:id="@+id/subtitle_suggested_chipgroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingVertical="-7dp"
app:chipSpacingHorizontal="5dp"
app:selectionRequired="false"
app:singleSelection="false">
</com.google.android.material.chip.ChipGroup>
</ScrollView>
</LinearLayout>
<TextView <TextView
android:id="@+id/suggested" android:id="@+id/all_subs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:textStyle="bold" android:textStyle="bold"
@ -34,7 +76,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitle" app:layout_constraintTop_toBottomOf="@id/subtitle"
android:text="@string/suggested" /> android:text="@string/select" />
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -43,18 +85,18 @@
app:layout_constraintVertical_bias="0.0" app:layout_constraintVertical_bias="0.0"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/suggested"> app:layout_constraintTop_toBottomOf="@id/all_subs">
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/subtitle_suggested_chipgroup" android:id="@+id/subtitle_all_chipgroup"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipSpacingVertical="-7dp" app:chipSpacingVertical="-7dp"
app:chipSpacingHorizontal="5dp" app:chipSpacingHorizontal="5dp"
app:selectionRequired="false" app:selectionRequired="false"
app:singleSelection="false"> app:singleSelection="false">
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
</ScrollView> </ScrollView>

View file

@ -25,7 +25,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/shortcuts" android:text="@string/shortcuts"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -25,7 +25,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/shortcuts" android:text="@string/shortcuts"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -35,7 +35,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/player_client" android:text="@string/player_client"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>

View file

@ -21,17 +21,18 @@
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="20dp"> android:paddingTop="20dp">
<TextView <TextView
android:id="@+id/bottom_sheet_title" android:id="@+id/bottom_sheet_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/ytdl_source" android:text="@string/ytdl_source"
android:textSize="25sp" android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/add" app:layout_constraintEnd_toStartOf="@+id/add"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/bottom_sheet_subtitle" android:id="@+id/bottom_sheet_subtitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"