feat: allow downloading audio based on language

This commit is contained in:
Bnyro 2023-08-22 09:42:22 +02:00
parent d9d735051d
commit cf79b7bd67
9 changed files with 559 additions and 21 deletions

View File

@ -0,0 +1,520 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "95ac69d3a5e70c9b377c7abf89c61cc2",
"entities": [
{
"tableName": "watchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"videoId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "watchPosition",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"videoId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
"fields": [
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"query"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "customInstance",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiUrl",
"columnName": "apiUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "frontendUrl",
"columnName": "frontendUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "localSubscription",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
"fields": [
{
"fieldPath": "channelId",
"columnName": "channelId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"channelId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "playlistBookmark",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `playlistName` TEXT, `thumbnailUrl` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `videos` INTEGER NOT NULL, PRIMARY KEY(`playlistId`))",
"fields": [
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistName",
"columnName": "playlistName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "videos",
"columnName": "videos",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"playlistId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LocalPlaylist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL, `description` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LocalPlaylistItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `uploader` TEXT NOT NULL, `uploadDate` TEXT, `thumbnailPath` TEXT, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailPath",
"columnName": "thumbnailPath",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"videoId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "downloadItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `videoId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `path` TEXT NOT NULL, `url` TEXT, `format` TEXT, `quality` TEXT, `language` TEXT, `downloadSize` INTEGER NOT NULL, FOREIGN KEY(`videoId`) REFERENCES `download`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fileName",
"columnName": "fileName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "quality",
"columnName": "quality",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "downloadSize",
"columnName": "downloadSize",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_downloadItem_path",
"unique": true,
"columnNames": [
"path"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloadItem_path` ON `${TABLE_NAME}` (`path`)"
}
],
"foreignKeys": [
{
"table": "download",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"videoId"
],
"referencedColumns": [
"videoId"
]
}
]
},
{
"tableName": "subscriptionGroups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "channels",
"columnName": "channels",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
},
"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, '95ac69d3a5e70c9b377c7abf89c61cc2')"
]
}
}

View File

@ -41,6 +41,7 @@ data class PipedStream(
url = url?.let { ProxyHelper.unwrapUrl(it) }, url = url?.let { ProxyHelper.unwrapUrl(it) },
format = format, format = format,
quality = quality, quality = quality,
language = audioTrackLocale,
downloadSize = contentLength downloadSize = contentLength
) )
} }

View File

@ -41,7 +41,7 @@ data class Streams(
val previewFrames: List<PreviewFrames> = emptyList() val previewFrames: List<PreviewFrames> = emptyList()
) { ) {
fun toDownloadItems(downloadData: DownloadData): List<DownloadItem> { fun toDownloadItems(downloadData: DownloadData): List<DownloadItem> {
val (id, name, videoFormat, videoQuality, audioFormat, audioQuality, subCode) = downloadData val (id, name, videoFormat, videoQuality, audioFormat, audioQuality, audioTrackLocale, subCode) = downloadData
val items = mutableListOf<DownloadItem>() val items = mutableListOf<DownloadItem>()
if (!videoQuality.isNullOrEmpty() && !videoFormat.isNullOrEmpty()) { if (!videoQuality.isNullOrEmpty() && !videoFormat.isNullOrEmpty()) {
@ -53,7 +53,7 @@ data class Streams(
if (!audioQuality.isNullOrEmpty() && !audioFormat.isNullOrEmpty()) { if (!audioQuality.isNullOrEmpty() && !audioFormat.isNullOrEmpty()) {
val stream = audioStreams.find { val stream = audioStreams.find {
it.quality == audioQuality && it.format == audioFormat it.quality == audioQuality && it.format == audioFormat && it.audioTrackLocale == audioTrackLocale
} }
stream?.toDownloadItem(FileType.AUDIO, id, name)?.let { items.add(it) } stream?.toDownloadItem(FileType.AUDIO, id, name)?.let { items.add(it) }
} }

View File

@ -39,7 +39,7 @@ import com.github.libretube.db.obj.WatchPosition
DownloadItem::class, DownloadItem::class,
SubscriptionGroup::class SubscriptionGroup::class
], ],
version = 14, version = 15,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 7, to = 8), AutoMigration(from = 7, to = 8),
AutoMigration(from = 8, to = 9), AutoMigration(from = 8, to = 9),

View File

@ -31,9 +31,17 @@ object DatabaseHolder {
} }
} }
private val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE 'downloaditem' ADD COLUMN 'language' TEXT DEFAULT NULL"
)
}
}
val Database by lazy { val Database by lazy {
Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME) Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME)
.addMigrations(MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14) .addMigrations(MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
} }

View File

@ -29,5 +29,6 @@ data class DownloadItem(
var url: String? = null, var url: String? = null,
var format: String? = null, var format: String? = null,
var quality: String? = null, var quality: String? = null,
var language: String? = null,
var downloadSize: Long = -1L var downloadSize: Long = -1L
) )

View File

@ -11,5 +11,6 @@ data class DownloadData(
val videoQuality: String?, val videoQuality: String?,
val audioFormat: String?, val audioFormat: String?,
val audioQuality: String?, val audioQuality: String?,
val audioLanguage: String?,
val subtitleCode: String? val subtitleCode: String?
) : Parcelable ) : Parcelable

View File

@ -40,18 +40,6 @@ import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWN
import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_RESUME import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_RESUME
import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_STOP import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_STOP
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import java.io.File
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.concurrent.Executors
import kotlin.io.path.absolute
import kotlin.io.path.createFile
import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize
import kotlin.math.min
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -66,6 +54,18 @@ import kotlinx.coroutines.withContext
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
import java.io.File
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.concurrent.Executors
import kotlin.io.path.absolute
import kotlin.io.path.createFile
import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize
import kotlin.math.min
/** /**
* Download service with custom implementation of downloading using [HttpURLConnection]. * Download service with custom implementation of downloading using [HttpURLConnection].
@ -205,7 +205,7 @@ class DownloadService : LifecycleService() {
notificationBuilder notificationBuilder
.setContentText( .setContentText(
totalRead.formatAsFileSize() + " / " + totalRead.formatAsFileSize() + " / " +
item.downloadSize.formatAsFileSize() item.downloadSize.formatAsFileSize()
) )
.setProgress( .setProgress(
item.downloadSize.toInt(), item.downloadSize.toInt(),
@ -371,7 +371,9 @@ class DownloadService : LifecycleService() {
FileType.VIDEO -> streams.videoStreams FileType.VIDEO -> streams.videoStreams
else -> null else -> null
} }
stream?.find { it.format == item.format && it.quality == item.quality }?.let { stream?.find {
it.format == item.format && it.quality == item.quality && it.audioTrackLocale == item.language
}?.let {
item.url = it.url item.url = it.url
} }
Database.downloadDao().updateDownloadItem(item) Database.downloadDao().updateDownloadItem(item)

View File

@ -120,7 +120,7 @@ class DownloadDialog(
R.layout.dropdown_item, R.layout.dropdown_item,
videoStreams.map { videoStreams.map {
val fileSize = Formatter.formatShortFileSize(context, it.contentLength) val fileSize = Formatter.formatShortFileSize(context, it.contentLength)
"${it.quality} ${it.format} ($fileSize)" "${it.quality} ${it.codec} ($fileSize)"
}.toMutableList().also { }.toMutableList().also {
it.add(0, getString(R.string.no_video)) it.add(0, getString(R.string.no_video))
} }
@ -130,8 +130,12 @@ class DownloadDialog(
requireContext(), requireContext(),
R.layout.dropdown_item, R.layout.dropdown_item,
audioStreams.map { audioStreams.map {
val fileSize = Formatter.formatShortFileSize(context, it.contentLength) val fileSize = it.contentLength
"${it.quality} ${it.codec} ($fileSize)" .takeIf { l -> l > 0 }
?.let { cl -> Formatter.formatShortFileSize(context, cl) }
val infoStr = listOfNotNull(it.audioTrackLocale, fileSize)
.joinToString(", ")
"${it.quality} ${it.format} ($infoStr)"
}.toMutableList().also { }.toMutableList().also {
it.add(0, getString(R.string.no_audio)) it.add(0, getString(R.string.no_audio))
} }
@ -179,6 +183,7 @@ class DownloadDialog(
videoQuality = videoStream?.quality, videoQuality = videoStream?.quality,
audioFormat = audioStream?.format, audioFormat = audioStream?.format,
audioQuality = audioStream?.quality, audioQuality = audioStream?.quality,
audioLanguage = audioStream?.audioTrackLocale,
subtitleCode = subtitle?.code subtitleCode = subtitle?.code
) )
DownloadHelper.startDownloadService(requireContext(), downloadData) DownloadHelper.startDownloadService(requireContext(), downloadData)