From cf79b7bd67c113b3cc254112c5d605089e75633a Mon Sep 17 00:00:00 2001 From: Bnyro Date: Tue, 22 Aug 2023 09:42:22 +0200 Subject: [PATCH] feat: allow downloading audio based on language --- .../15.json | 520 ++++++++++++++++++ .../github/libretube/api/obj/PipedStream.kt | 1 + .../com/github/libretube/api/obj/Streams.kt | 4 +- .../com/github/libretube/db/AppDatabase.kt | 2 +- .../com/github/libretube/db/DatabaseHolder.kt | 10 +- .../github/libretube/db/obj/DownloadItem.kt | 1 + .../libretube/parcelable/DownloadData.kt | 1 + .../libretube/services/DownloadService.kt | 30 +- .../libretube/ui/dialogs/DownloadDialog.kt | 11 +- 9 files changed, 559 insertions(+), 21 deletions(-) create mode 100644 app/schemas/com.github.libretube.db.AppDatabase/15.json diff --git a/app/schemas/com.github.libretube.db.AppDatabase/15.json b/app/schemas/com.github.libretube.db.AppDatabase/15.json new file mode 100644 index 000000000..137963189 --- /dev/null +++ b/app/schemas/com.github.libretube.db.AppDatabase/15.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt index 22ba3d385..65cc05376 100644 --- a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt +++ b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt @@ -41,6 +41,7 @@ data class PipedStream( url = url?.let { ProxyHelper.unwrapUrl(it) }, format = format, quality = quality, + language = audioTrackLocale, downloadSize = contentLength ) } diff --git a/app/src/main/java/com/github/libretube/api/obj/Streams.kt b/app/src/main/java/com/github/libretube/api/obj/Streams.kt index 548530f00..78427db8d 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Streams.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Streams.kt @@ -41,7 +41,7 @@ data class Streams( val previewFrames: List = emptyList() ) { fun toDownloadItems(downloadData: DownloadData): List { - val (id, name, videoFormat, videoQuality, audioFormat, audioQuality, subCode) = downloadData + val (id, name, videoFormat, videoQuality, audioFormat, audioQuality, audioTrackLocale, subCode) = downloadData val items = mutableListOf() if (!videoQuality.isNullOrEmpty() && !videoFormat.isNullOrEmpty()) { @@ -53,7 +53,7 @@ data class Streams( if (!audioQuality.isNullOrEmpty() && !audioFormat.isNullOrEmpty()) { 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) } } diff --git a/app/src/main/java/com/github/libretube/db/AppDatabase.kt b/app/src/main/java/com/github/libretube/db/AppDatabase.kt index 282322dbe..e540c44c8 100644 --- a/app/src/main/java/com/github/libretube/db/AppDatabase.kt +++ b/app/src/main/java/com/github/libretube/db/AppDatabase.kt @@ -39,7 +39,7 @@ import com.github.libretube.db.obj.WatchPosition DownloadItem::class, SubscriptionGroup::class ], - version = 14, + version = 15, autoMigrations = [ AutoMigration(from = 7, to = 8), AutoMigration(from = 8, to = 9), diff --git a/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt b/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt index 7b9264227..b931cfeb3 100644 --- a/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt +++ b/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt @@ -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 { 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() .build() } diff --git a/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt b/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt index c309657e5..a04c2631f 100644 --- a/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt +++ b/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt @@ -29,5 +29,6 @@ data class DownloadItem( var url: String? = null, var format: String? = null, var quality: String? = null, + var language: String? = null, var downloadSize: Long = -1L ) diff --git a/app/src/main/java/com/github/libretube/parcelable/DownloadData.kt b/app/src/main/java/com/github/libretube/parcelable/DownloadData.kt index 004564d75..11515fafb 100644 --- a/app/src/main/java/com/github/libretube/parcelable/DownloadData.kt +++ b/app/src/main/java/com/github/libretube/parcelable/DownloadData.kt @@ -11,5 +11,6 @@ data class DownloadData( val videoQuality: String?, val audioFormat: String?, val audioQuality: String?, + val audioLanguage: String?, val subtitleCode: String? ) : Parcelable diff --git a/app/src/main/java/com/github/libretube/services/DownloadService.kt b/app/src/main/java/com/github/libretube/services/DownloadService.kt index 8786513c7..35e7a1725 100644 --- a/app/src/main/java/com/github/libretube/services/DownloadService.kt +++ b/app/src/main/java/com/github/libretube/services/DownloadService.kt @@ -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_STOP 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.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -66,6 +54,18 @@ import kotlinx.coroutines.withContext import okio.buffer import okio.sink 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]. @@ -205,7 +205,7 @@ class DownloadService : LifecycleService() { notificationBuilder .setContentText( totalRead.formatAsFileSize() + " / " + - item.downloadSize.formatAsFileSize() + item.downloadSize.formatAsFileSize() ) .setProgress( item.downloadSize.toInt(), @@ -371,7 +371,9 @@ class DownloadService : LifecycleService() { FileType.VIDEO -> streams.videoStreams 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 } Database.downloadDao().updateDownloadItem(item) diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt index 761e6461f..a1cc7aa6a 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt @@ -120,7 +120,7 @@ class DownloadDialog( R.layout.dropdown_item, videoStreams.map { val fileSize = Formatter.formatShortFileSize(context, it.contentLength) - "${it.quality} ${it.format} ($fileSize)" + "${it.quality} ${it.codec} ($fileSize)" }.toMutableList().also { it.add(0, getString(R.string.no_video)) } @@ -130,8 +130,12 @@ class DownloadDialog( requireContext(), R.layout.dropdown_item, audioStreams.map { - val fileSize = Formatter.formatShortFileSize(context, it.contentLength) - "${it.quality} ${it.codec} ($fileSize)" + val fileSize = it.contentLength + .takeIf { l -> l > 0 } + ?.let { cl -> Formatter.formatShortFileSize(context, cl) } + val infoStr = listOfNotNull(it.audioTrackLocale, fileSize) + .joinToString(", ") + "${it.quality} ${it.format} ($infoStr)" }.toMutableList().also { it.add(0, getString(R.string.no_audio)) } @@ -179,6 +183,7 @@ class DownloadDialog( videoQuality = videoStream?.quality, audioFormat = audioStream?.format, audioQuality = audioStream?.quality, + audioLanguage = audioStream?.audioTrackLocale, subtitleCode = subtitle?.code ) DownloadHelper.startDownloadService(requireContext(), downloadData)