diff --git a/app/schemas/com.github.libretube.db.AppDatabase/19.json b/app/schemas/com.github.libretube.db.AppDatabase/19.json new file mode 100644 index 000000000..e0ce4d55d --- /dev/null +++ b/app/schemas/com.github.libretube.db.AppDatabase/19.json @@ -0,0 +1,663 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "74afbaf921a81ccd97447002122d5077", + "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, `isShort` INTEGER NOT NULL, 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 + }, + { + "fieldPath": "isShort", + "columnName": "isShort", + "affinity": "INTEGER", + "notNull": true + } + ], + "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, `duration` INTEGER DEFAULT 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": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "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": "downloadChapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `videoId` TEXT NOT NULL, `name` TEXT NOT NULL, `start` INTEGER NOT NULL, `thumbnailUrl` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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": [] + }, + { + "tableName": "feedItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `thumbnail` TEXT, `uploaderName` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `duration` INTEGER, `views` INTEGER, `uploaderVerified` INTEGER NOT NULL, `uploaded` INTEGER NOT NULL, `shortDescription` TEXT, `isShort` INTEGER NOT NULL, PRIMARY KEY(`videoId`))", + "fields": [ + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploaderName", + "columnName": "uploaderName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploaderUrl", + "columnName": "uploaderUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploaderAvatar", + "columnName": "uploaderAvatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "views", + "columnName": "views", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploaderVerified", + "columnName": "uploaderVerified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortDescription", + "columnName": "shortDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isShort", + "columnName": "isShort", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "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, '74afbaf921a81ccd97447002122d5077')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/LibreTubeApp.kt b/app/src/main/java/com/github/libretube/LibreTubeApp.kt index 2b1d918e2..9a4620e06 100644 --- a/app/src/main/java/com/github/libretube/LibreTubeApp.kt +++ b/app/src/main/java/com/github/libretube/LibreTubeApp.kt @@ -5,6 +5,7 @@ import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationManagerCompat import androidx.work.ExistingPeriodicWorkPolicy import com.github.libretube.helpers.ImageHelper +import com.github.libretube.helpers.NewPipeExtractorInstance import com.github.libretube.helpers.NotificationHelper import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.ProxyHelper @@ -55,6 +56,8 @@ class LibreTubeApp : Application() { * Dynamically create App Shortcuts */ ShortcutHelper.createShortcuts(this) + + NewPipeExtractorInstance.init() } /** diff --git a/app/src/main/java/com/github/libretube/api/StreamsExtractor.kt b/app/src/main/java/com/github/libretube/api/StreamsExtractor.kt index 0cbeb04c7..c9fdecde8 100644 --- a/app/src/main/java/com/github/libretube/api/StreamsExtractor.kt +++ b/app/src/main/java/com/github/libretube/api/StreamsExtractor.kt @@ -11,15 +11,13 @@ import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Subtitle import com.github.libretube.helpers.PlayerHelper -import com.github.libretube.util.NewPipeDownloaderImpl +import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL import kotlinx.datetime.toKotlinInstant -import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.VideoStream import retrofit2.HttpException import java.io.IOException -import java.lang.Exception fun VideoStream.toPipedStream(): PipedStream = PipedStream( url = content, @@ -39,26 +37,18 @@ fun VideoStream.toPipedStream(): PipedStream = PipedStream( ) object StreamsExtractor { -// val npe by lazy { -// NewPipe.getService(ServiceList.YouTube.serviceId) -// } - - init { - NewPipe.init(NewPipeDownloaderImpl()) - } - suspend fun extractStreams(videoId: String): Streams { if (!PlayerHelper.disablePipedProxy || !PlayerHelper.localStreamExtraction) { return RetrofitInstance.api.getStreams(videoId) } - val resp = StreamInfo.getInfo("https://www.youtube.com/watch?v=$videoId") + val resp = StreamInfo.getInfo("${YOUTUBE_FRONTEND_URL}/watch?v=$videoId") return Streams( title = resp.name, description = resp.description.content, uploader = resp.uploaderName, uploaderAvatar = resp.uploaderAvatars.maxBy { it.height }.url, - uploaderUrl = resp.uploaderUrl.replace("https://www.youtube.com", ""), + uploaderUrl = resp.uploaderUrl.replace(YOUTUBE_FRONTEND_URL, ""), uploaderVerified = resp.isUploaderVerified, uploaderSubscriberCount = resp.uploaderSubscriberCount, category = resp.category, @@ -86,12 +76,12 @@ object StreamsExtractor { thumbnailUrl = resp.thumbnails.maxBy { it.height }.url, relatedStreams = resp.relatedItems.filterIsInstance().map { StreamItem( - it.url.replace("https://www.youtube.com", ""), + it.url.replace(YOUTUBE_FRONTEND_URL, ""), StreamItem.TYPE_STREAM, it.name, it.thumbnails.maxBy { image -> image.height }.url, it.uploaderName, - it.uploaderUrl.replace("https://www.youtube.com", ""), + it.uploaderUrl.replace(YOUTUBE_FRONTEND_URL, ""), it.uploaderAvatars.maxBy { image -> image.height }.url, it.textualUploadDate, it.duration, diff --git a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt index 15d142ca8..d932fca71 100644 --- a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt +++ b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt @@ -6,6 +6,7 @@ import com.github.libretube.constants.PreferenceKeys import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.repo.AccountSubscriptionsRepository import com.github.libretube.repo.FeedRepository +import com.github.libretube.repo.LocalFeedRepository import com.github.libretube.repo.LocalSubscriptionsRepository import com.github.libretube.repo.PipedAccountFeedRepository import com.github.libretube.repo.PipedNoAccountFeedRepository @@ -26,6 +27,7 @@ object SubscriptionHelper { else -> LocalSubscriptionsRepository() } private val feedRepository: FeedRepository get() = when { + PreferenceHelper.getBoolean(PreferenceKeys.LOCAL_FEED_EXTRACTION, false) -> LocalFeedRepository() token.isNotEmpty() -> PipedAccountFeedRepository() else -> PipedNoAccountFeedRepository() } @@ -35,7 +37,8 @@ object SubscriptionHelper { suspend fun isSubscribed(channelId: String) = subscriptionsRepository.isSubscribed(channelId) suspend fun importSubscriptions(newChannels: List) = subscriptionsRepository.importSubscriptions(newChannels) suspend fun getSubscriptions() = subscriptionsRepository.getSubscriptions() - suspend fun getFeed() = feedRepository.getFeed(false) + suspend fun getSubscriptionChannelIds() = subscriptionsRepository.getSubscriptionChannelIds() + suspend fun getFeed(forceRefresh: Boolean) = feedRepository.getFeed(forceRefresh) fun handleUnsubscribe( context: Context, diff --git a/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt b/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt index 31f439ed6..7003ed7b8 100644 --- a/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt +++ b/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt @@ -2,6 +2,7 @@ package com.github.libretube.api.obj import android.os.Parcelable import com.github.libretube.db.obj.LocalPlaylistItem +import com.github.libretube.db.obj.SubscriptionsFeedItem import com.github.libretube.extensions.toID import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -40,6 +41,21 @@ data class StreamItem( ) } + fun toFeedItem() = SubscriptionsFeedItem( + videoId = url!!.toID(), + title = title, + thumbnail = thumbnail, + uploaderName = uploaderName, + uploaded = uploaded, + uploaderAvatar = uploaderAvatar, + uploaderUrl = uploaderUrl, + duration = duration, + uploaderVerified = uploaderVerified ?: false, + shortDescription = shortDescription, + views = views, + isShort = isShort + ) + companion object { const val TYPE_STREAM = "stream" const val TYPE_CHANNEL = "channel" 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 aa8fd0d16..17ce5bac0 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 @@ -3,12 +3,11 @@ package com.github.libretube.api.obj import android.os.Parcelable import com.github.libretube.db.obj.DownloadItem import com.github.libretube.enums.FileType +import com.github.libretube.extensions.toLocalDate import com.github.libretube.helpers.ProxyHelper import com.github.libretube.json.SafeInstantSerializer import com.github.libretube.parcelable.DownloadData import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import kotlinx.serialization.SerialName @@ -100,8 +99,7 @@ data class Streams( uploaderName = uploader, uploaderUrl = uploaderUrl, uploaderAvatar = uploaderAvatar, - uploadedDate = uploadTimestamp?.toLocalDateTime(TimeZone.currentSystemDefault())?.date - ?.toString(), + uploadedDate = uploadTimestamp?.toLocalDate()?.toString(), uploaded = uploaded ?: uploadTimestamp?.toEpochMilliseconds() ?: 0, duration = duration, views = views, @@ -111,6 +109,6 @@ data class Streams( } companion object { - const val categoryMusic = "Music" + const val CATEGORY_MUSIC = "Music" } } diff --git a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt index 18db42ce2..afe69ffeb 100644 --- a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt @@ -113,6 +113,8 @@ object PreferenceKeys { const val HIDE_WATCHED_FROM_FEED = "hide_watched_from_feed" const val SELECTED_FEED_FILTERS = "filter_feed" const val FEED_SORT_ORDER = "sort_oder_feed" + const val LOCAL_FEED_EXTRACTION = "local_feed_extraction" + const val LAST_FEED_REFRESH_TIMESTAMP_MILLIS = "last_feed_refresh_timestamp_millis" // Advanced const val AUTOMATIC_UPDATE_CHECKS = "automatic_update_checks" 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 1f877ffaa..9a016dae5 100644 --- a/app/src/main/java/com/github/libretube/db/AppDatabase.kt +++ b/app/src/main/java/com/github/libretube/db/AppDatabase.kt @@ -11,6 +11,7 @@ import com.github.libretube.db.dao.LocalSubscriptionDao import com.github.libretube.db.dao.PlaylistBookmarkDao import com.github.libretube.db.dao.SearchHistoryDao import com.github.libretube.db.dao.SubscriptionGroupsDao +import com.github.libretube.db.dao.SubscriptionsFeedDao import com.github.libretube.db.dao.WatchHistoryDao import com.github.libretube.db.dao.WatchPositionDao import com.github.libretube.db.obj.CustomInstance @@ -23,6 +24,7 @@ import com.github.libretube.db.obj.LocalSubscription import com.github.libretube.db.obj.PlaylistBookmark import com.github.libretube.db.obj.SearchHistoryItem import com.github.libretube.db.obj.SubscriptionGroup +import com.github.libretube.db.obj.SubscriptionsFeedItem import com.github.libretube.db.obj.WatchHistoryItem import com.github.libretube.db.obj.WatchPosition @@ -39,15 +41,17 @@ import com.github.libretube.db.obj.WatchPosition Download::class, DownloadItem::class, DownloadChapter::class, - SubscriptionGroup::class + SubscriptionGroup::class, + SubscriptionsFeedItem::class ], - version = 18, + version = 19, autoMigrations = [ AutoMigration(from = 7, to = 8), AutoMigration(from = 8, to = 9), AutoMigration(from = 9, to = 10), AutoMigration(from = 10, to = 11), - AutoMigration(from = 16, to = 17) + AutoMigration(from = 16, to = 17), + AutoMigration(from = 18, to = 19) ] ) @TypeConverters(Converters::class) @@ -96,4 +100,6 @@ abstract class AppDatabase : RoomDatabase() { * Subscription groups */ abstract fun subscriptionGroupsDao(): SubscriptionGroupsDao + + abstract fun feedDao(): SubscriptionsFeedDao } diff --git a/app/src/main/java/com/github/libretube/db/Converters.kt b/app/src/main/java/com/github/libretube/db/Converters.kt index 71bfb289a..fd7dc9bc4 100644 --- a/app/src/main/java/com/github/libretube/db/Converters.kt +++ b/app/src/main/java/com/github/libretube/db/Converters.kt @@ -3,7 +3,6 @@ package com.github.libretube.db import androidx.room.TypeConverter import com.github.libretube.api.JsonHelper import kotlinx.datetime.LocalDate -import kotlinx.datetime.toLocalDate import kotlinx.serialization.encodeToString import java.nio.file.Path import kotlin.io.path.Path @@ -13,7 +12,7 @@ object Converters { fun localDateToString(localDate: LocalDate?) = localDate?.toString() @TypeConverter - fun stringToLocalDate(string: String?) = string?.toLocalDate() + fun stringToLocalDate(string: String?) = string?.let { LocalDate.parse(it) } @TypeConverter fun pathToString(path: Path?) = path?.toString() diff --git a/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt b/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt index 26b3b2900..c0d422a8f 100644 --- a/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt +++ b/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt @@ -8,13 +8,11 @@ import com.github.libretube.db.obj.SearchHistoryItem import com.github.libretube.db.obj.WatchHistoryItem import com.github.libretube.enums.ContentFilter import com.github.libretube.extensions.toID +import com.github.libretube.extensions.toLocalDate import com.github.libretube.helpers.PreferenceHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime object DatabaseHelper { private const val MAX_SEARCH_HISTORY_SIZE = 20 @@ -28,8 +26,7 @@ object DatabaseHelper { val watchHistoryItem = WatchHistoryItem( videoId, stream.title, - Instant.fromEpochMilliseconds(stream.uploaded) - .toLocalDateTime(TimeZone.currentSystemDefault()).date, + stream.uploaded.toLocalDate(), stream.uploaderName, stream.uploaderUrl?.toID(), stream.uploaderAvatar, 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 b314eec6c..b8440e624 100644 --- a/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt +++ b/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt @@ -66,7 +66,6 @@ object DatabaseHolder { MIGRATION_15_16, MIGRATION_17_18 ) - .fallbackToDestructiveMigration() .build() } } diff --git a/app/src/main/java/com/github/libretube/db/dao/SubscriptionsFeedDao.kt b/app/src/main/java/com/github/libretube/db/dao/SubscriptionsFeedDao.kt new file mode 100644 index 000000000..45396e615 --- /dev/null +++ b/app/src/main/java/com/github/libretube/db/dao/SubscriptionsFeedDao.kt @@ -0,0 +1,22 @@ +package com.github.libretube.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.github.libretube.db.obj.SubscriptionsFeedItem + +@Dao +interface SubscriptionsFeedDao { + @Query("SELECT * FROM feedItem ORDER BY uploaded DESC") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(feedItems: List) + + @Query("DELETE FROM feedItem WHERE uploaded < :olderThan") + suspend fun cleanUpOlderThan(olderThan: Long) + + @Query("DELETE FROM feedItem") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/db/obj/SubscriptionsFeedItem.kt b/app/src/main/java/com/github/libretube/db/obj/SubscriptionsFeedItem.kt new file mode 100644 index 000000000..fa787556f --- /dev/null +++ b/app/src/main/java/com/github/libretube/db/obj/SubscriptionsFeedItem.kt @@ -0,0 +1,40 @@ +package com.github.libretube.db.obj + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.github.libretube.api.obj.StreamItem +import com.github.libretube.extensions.toLocalDate + +@Entity(tableName = "feedItem") +data class SubscriptionsFeedItem( + @PrimaryKey + val videoId: String, + val title: String? = null, + val thumbnail: String? = null, + val uploaderName: String? = null, + val uploaderUrl: String? = null, + val uploaderAvatar: String? = null, + val duration: Long? = null, + val views: Long? = null, + val uploaderVerified: Boolean, + val uploaded: Long = 0, + val shortDescription: String? = null, + val isShort: Boolean = false +) { + fun toStreamItem() = StreamItem( + url = videoId, + type = StreamItem.TYPE_STREAM, + title = title, + thumbnail = thumbnail, + uploaderName = uploaderName, + uploaded = uploaded, + uploadedDate = uploaded.toLocalDate().toString(), + uploaderAvatar = uploaderAvatar, + uploaderUrl = uploaderUrl, + duration = duration, + uploaderVerified = uploaderVerified, + shortDescription = shortDescription, + views = views, + isShort = isShort + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt b/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt index 79b41fcb9..93b3e68c9 100644 --- a/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt +++ b/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt @@ -30,6 +30,7 @@ data class WatchHistoryItem( thumbnail = thumbnailUrl, uploaderName = uploader, uploaded = uploadDate?.toMillis() ?: 0, + uploadedDate = uploadDate?.toString(), uploaderAvatar = uploaderAvatar, uploaderUrl = uploaderUrl, duration = duration, diff --git a/app/src/main/java/com/github/libretube/extensions/LocalDate.kt b/app/src/main/java/com/github/libretube/extensions/LocalDate.kt index 370d9a17a..7974711e9 100644 --- a/app/src/main/java/com/github/libretube/extensions/LocalDate.kt +++ b/app/src/main/java/com/github/libretube/extensions/LocalDate.kt @@ -1,7 +1,19 @@ package com.github.libretube.extensions +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.toLocalDateTime fun LocalDate.toMillis() = this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds() + +fun Long.toLocalDateTime() = + Instant.fromEpochMilliseconds(this).toLocalDateTime() + +fun Long.toLocalDate() = + Instant.fromEpochMilliseconds(this).toLocalDate() + +fun Instant.toLocalDateTime() = this.toLocalDateTime(TimeZone.currentSystemDefault()) + +fun Instant.toLocalDate() = this.toLocalDateTime().date \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/helpers/NewPipeExtractorInstance.kt b/app/src/main/java/com/github/libretube/helpers/NewPipeExtractorInstance.kt new file mode 100644 index 000000000..1bf1a62d5 --- /dev/null +++ b/app/src/main/java/com/github/libretube/helpers/NewPipeExtractorInstance.kt @@ -0,0 +1,16 @@ +package com.github.libretube.helpers + +import com.github.libretube.util.NewPipeDownloaderImpl +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.ServiceList +import org.schabi.newpipe.extractor.StreamingService + +object NewPipeExtractorInstance { + val extractor: StreamingService by lazy { + NewPipe.getService(ServiceList.YouTube.serviceId) + } + + fun init() { + NewPipe.init(NewPipeDownloaderImpl()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/json/SafeInstantSerializer.kt b/app/src/main/java/com/github/libretube/json/SafeInstantSerializer.kt index 0d063bb20..5fd01ef69 100644 --- a/app/src/main/java/com/github/libretube/json/SafeInstantSerializer.kt +++ b/app/src/main/java/com/github/libretube/json/SafeInstantSerializer.kt @@ -3,6 +3,7 @@ package com.github.libretube.json import android.util.Log import com.github.libretube.extensions.TAG import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.toInstant @@ -19,10 +20,10 @@ object SafeInstantSerializer : KSerializer { override fun deserialize(decoder: Decoder): Instant { val string = decoder.decodeString() return try { - string.toInstant() + Instant.parse(string) } catch (e: IllegalArgumentException) { Log.e(TAG(), "Error parsing date '$string'", e) - string.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()) + LocalDate.parse(string).atStartOfDayIn(TimeZone.currentSystemDefault()) } } diff --git a/app/src/main/java/com/github/libretube/repo/AccountSubscriptionsRepository.kt b/app/src/main/java/com/github/libretube/repo/AccountSubscriptionsRepository.kt index f9264665b..270de8b19 100644 --- a/app/src/main/java/com/github/libretube/repo/AccountSubscriptionsRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/AccountSubscriptionsRepository.kt @@ -3,6 +3,7 @@ package com.github.libretube.repo import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.obj.Subscribe import com.github.libretube.api.obj.Subscription +import com.github.libretube.extensions.toID import com.github.libretube.helpers.PreferenceHelper class AccountSubscriptionsRepository: SubscriptionsRepository { @@ -33,4 +34,8 @@ class AccountSubscriptionsRepository: SubscriptionsRepository { override suspend fun getSubscriptions(): List { return RetrofitInstance.authApi.subscriptions(token) } + + override suspend fun getSubscriptionChannelIds(): List { + return getSubscriptions().map { it.url.toID() } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt b/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt new file mode 100644 index 000000000..215f11ff0 --- /dev/null +++ b/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt @@ -0,0 +1,103 @@ +package com.github.libretube.repo + +import android.util.Log +import com.github.libretube.api.SubscriptionHelper +import com.github.libretube.api.obj.StreamItem +import com.github.libretube.constants.PreferenceKeys +import com.github.libretube.db.DatabaseHolder +import com.github.libretube.db.obj.SubscriptionsFeedItem +import com.github.libretube.extensions.parallelMap +import com.github.libretube.helpers.NewPipeExtractorInstance +import com.github.libretube.helpers.PreferenceHelper +import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL +import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import java.time.Duration +import java.time.Instant + +class LocalFeedRepository : FeedRepository { + private val relevantTabs = + arrayOf(ChannelTabs.LIVESTREAMS, ChannelTabs.VIDEOS, ChannelTabs.SHORTS) + + override suspend fun getFeed(forceRefresh: Boolean): List { + val nowMillis = Instant.now().toEpochMilli() + + if (!forceRefresh) { + val feed = DatabaseHolder.Database.feedDao().getAll() + val oneDayAgo = nowMillis - Duration.ofDays(1).toMillis() + + // only refresh if feed is empty or last refresh was more than a day ago + val lastRefresh = + PreferenceHelper.getLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, 0) + if (feed.isNotEmpty() && lastRefresh > oneDayAgo) { + return DatabaseHolder.Database.feedDao().getAll() + .map(SubscriptionsFeedItem::toStreamItem) + } + } + + val minimumDateMillis = nowMillis - Duration.ofDays(MAX_FEED_AGE_DAYS).toMillis() + DatabaseHolder.Database.feedDao().cleanUpOlderThan(minimumDateMillis) + + refreshFeed(minimumDateMillis) + PreferenceHelper.putLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, nowMillis) + + return DatabaseHolder.Database.feedDao().getAll().map(SubscriptionsFeedItem::toStreamItem) + } + + private suspend fun refreshFeed(minimumDateMillis: Long) { + val channelIds = SubscriptionHelper.getSubscriptionChannelIds() + + for (channelIdChunk in channelIds.chunked(CHUNK_SIZE)) { + val collectedFeedItems = channelIdChunk.parallelMap { channelId -> + try { + getRelatedStreams(channelId) + } catch (e: Exception) { + Log.e(channelId, e.stackTraceToString()) + null + } + }.filterNotNull().flatten().map(StreamItem::toFeedItem) + .filter { it.uploaded > minimumDateMillis } + + DatabaseHolder.Database.feedDao().insertAll(collectedFeedItems) + } + } + + private suspend fun getRelatedStreams(channelId: String): List { + val channelInfo = ChannelInfo.getInfo("$YOUTUBE_FRONTEND_URL/channel/${channelId}") + val relevantInfoTabs = channelInfo.tabs.filter { tab -> + relevantTabs.any { tab.contentFilters.contains(it) } + } + + val related = relevantInfoTabs.parallelMap { tab -> + runCatching { + ChannelTabInfo.getInfo(NewPipeExtractorInstance.extractor, tab).relatedItems + }.getOrElse { emptyList() } + }.flatten().filterIsInstance() + + return related.map { item -> + StreamItem( + type = StreamItem.TYPE_STREAM, + url = item.url.replace(YOUTUBE_FRONTEND_URL, ""), + title = item.name, + uploaded = item.uploadDate?.offsetDateTime()?.toEpochSecond()?.times(1000) ?: 0, + uploadedDate = item.uploadDate?.offsetDateTime()?.toLocalDateTime()?.toLocalDate() + ?.toString(), + uploaderName = item.uploaderName, + uploaderUrl = item.uploaderUrl.replace(YOUTUBE_FRONTEND_URL, ""), + uploaderAvatar = channelInfo.avatars.maxByOrNull { it.height }?.url, + thumbnail = item.thumbnails.maxByOrNull { it.height }?.url, + duration = item.duration, + views = item.viewCount, + uploaderVerified = item.isUploaderVerified, + shortDescription = item.shortDescription + ) + } + } + + companion object { + private const val CHUNK_SIZE = 2 + private const val MAX_FEED_AGE_DAYS = 30L // 30 days + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/repo/LocalSubscriptionsRepository.kt b/app/src/main/java/com/github/libretube/repo/LocalSubscriptionsRepository.kt index 93cf8bd08..f64d59963 100644 --- a/app/src/main/java/com/github/libretube/repo/LocalSubscriptionsRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/LocalSubscriptionsRepository.kt @@ -24,16 +24,20 @@ class LocalSubscriptionsRepository: SubscriptionsRepository { } override suspend fun getSubscriptions(): List { - val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId } + val channelIds = getSubscriptionChannelIds() return when { - subscriptions.size > GET_SUBSCRIPTIONS_LIMIT -> + channelIds.size > GET_SUBSCRIPTIONS_LIMIT -> RetrofitInstance.authApi - .unauthenticatedSubscriptions(subscriptions) + .unauthenticatedSubscriptions(channelIds) else -> RetrofitInstance.authApi.unauthenticatedSubscriptions( - subscriptions.joinToString(",") + channelIds.joinToString(",") ) } } + + override suspend fun getSubscriptionChannelIds(): List { + return Database.localSubscriptionDao().getAll().map { it.channelId } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt b/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt index 153636cfc..29f48af86 100644 --- a/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt @@ -4,19 +4,18 @@ import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper.GET_SUBSCRIPTIONS_LIMIT import com.github.libretube.api.obj.StreamItem -import com.github.libretube.extensions.toID class PipedNoAccountFeedRepository: FeedRepository { override suspend fun getFeed(forceRefresh: Boolean): List { - val subscriptions = SubscriptionHelper.getSubscriptions().map { it.url.toID() } + val channelIds = SubscriptionHelper.getSubscriptionChannelIds() return when { - subscriptions.size > GET_SUBSCRIPTIONS_LIMIT -> + channelIds.size > GET_SUBSCRIPTIONS_LIMIT -> RetrofitInstance.authApi - .getUnauthenticatedFeed(subscriptions) + .getUnauthenticatedFeed(channelIds) else -> RetrofitInstance.authApi.getUnauthenticatedFeed( - subscriptions.joinToString(",") + channelIds.joinToString(",") ) } } diff --git a/app/src/main/java/com/github/libretube/repo/SubscriptionsRepository.kt b/app/src/main/java/com/github/libretube/repo/SubscriptionsRepository.kt index 4138b6c81..e97b0e919 100644 --- a/app/src/main/java/com/github/libretube/repo/SubscriptionsRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/SubscriptionsRepository.kt @@ -8,4 +8,5 @@ interface SubscriptionsRepository { suspend fun isSubscribed(channelId: String): Boolean? suspend fun importSubscriptions(newChannels: List) suspend fun getSubscriptions(): List + suspend fun getSubscriptionChannelIds(): List } \ No newline at end of file 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 2698280a3..c5403aaf4 100644 --- a/app/src/main/java/com/github/libretube/services/DownloadService.kt +++ b/app/src/main/java/com/github/libretube/services/DownloadService.kt @@ -33,6 +33,7 @@ import com.github.libretube.enums.NotificationId import com.github.libretube.extensions.formatAsFileSize import com.github.libretube.extensions.getContentLength import com.github.libretube.extensions.parcelableExtra +import com.github.libretube.extensions.toLocalDate import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.helpers.DownloadHelper @@ -56,8 +57,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -159,7 +158,7 @@ class DownloadService : LifecycleService() { streams.description, streams.uploader, streams.duration, - streams.uploadTimestamp?.toLocalDateTime(TimeZone.currentSystemDefault())?.date, + streams.uploadTimestamp?.toLocalDate(), thumbnailTargetPath ) Database.downloadDao().insertDownload(download) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 7bb4eec36..ae8a74e96 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -1059,7 +1059,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { // set media source and resolution in the beginning updateResolution(commonPlayerViewModel.isFullscreen.value == true) - if (streams.category == Streams.categoryMusic) { + if (streams.category == Streams.CATEGORY_MUSIC) { playerController.setPlaybackSpeed(1f) } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt index c31d88b8a..c9d53abfe 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt @@ -118,7 +118,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() { binding.subProgress.isVisible = true if (viewModel.videoFeed.value == null) { - viewModel.fetchFeed(requireContext()) + viewModel.fetchFeed(requireContext(), forceRefresh = false) } if (viewModel.subscriptions.value == null) { viewModel.fetchSubscriptions(requireContext()) @@ -134,7 +134,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() { binding.subRefresh.setOnRefreshListener { viewModel.fetchSubscriptions(requireContext()) - viewModel.fetchFeed(requireContext()) + viewModel.fetchFeed(requireContext(), forceRefresh = true) } binding.toggleSubs.isVisible = true diff --git a/app/src/main/java/com/github/libretube/ui/fragments/WatchHistoryFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/WatchHistoryFragment.kt index 0dcb7fd07..c04ef7b37 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/WatchHistoryFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/WatchHistoryFragment.kt @@ -18,7 +18,6 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.room.withTransaction import com.github.libretube.R -import com.github.libretube.api.obj.StreamItem import com.github.libretube.constants.PreferenceKeys import com.github.libretube.databinding.FragmentWatchHistoryBinding import com.github.libretube.db.DatabaseHelper @@ -180,18 +179,7 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment() { binding.playAll.setOnClickListener { PlayingQueue.resetToDefaults() PlayingQueue.add( - *watchHistory.reversed().map { - StreamItem( - url = "/watch?v=${it.videoId}", - title = it.title, - thumbnail = it.thumbnailUrl, - uploaderName = it.uploader, - uploaderUrl = it.uploaderUrl, - uploaderAvatar = it.uploaderAvatar, - uploadedDate = it.uploadDate?.toString(), - duration = it.duration - ) - }.toTypedArray() + *watchHistory.reversed().map(WatchHistoryItem::toStreamItem).toTypedArray() ) NavigationHelper.navigateVideo( requireContext(), diff --git a/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt index 9a055e8f8..6f862aaba 100644 --- a/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt @@ -122,7 +122,7 @@ class HomeViewModel : ViewModel() { private suspend fun tryLoadFeed(subscriptionsViewModel: SubscriptionsViewModel): List { subscriptionsViewModel.videoFeed.value?.let { return it } - val feed = SubscriptionHelper.getFeed() + val feed = SubscriptionHelper.getFeed(forceRefresh = false) subscriptionsViewModel.videoFeed.postValue(feed) return if (hideWatched) feed.filterWatched() else feed diff --git a/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt index 2f74a4d30..f5aa3c75d 100644 --- a/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt @@ -21,10 +21,10 @@ class SubscriptionsViewModel : ViewModel() { var subscriptions = MutableLiveData?>() - fun fetchFeed(context: Context) { + fun fetchFeed(context: Context, forceRefresh: Boolean) { viewModelScope.launch(Dispatchers.IO) { val videoFeed = try { - SubscriptionHelper.getFeed() + SubscriptionHelper.getFeed(forceRefresh = forceRefresh) } catch (e: Exception) { context.toastFromMainDispatcher(R.string.server_error) Log.e(TAG(), e.toString()) diff --git a/app/src/main/java/com/github/libretube/util/TextUtils.kt b/app/src/main/java/com/github/libretube/util/TextUtils.kt index 5a25f3e57..aaef968e5 100644 --- a/app/src/main/java/com/github/libretube/util/TextUtils.kt +++ b/app/src/main/java/com/github/libretube/util/TextUtils.kt @@ -9,10 +9,9 @@ import androidx.core.text.isDigitsOnly import com.github.libretube.BuildConfig import com.github.libretube.R import com.github.libretube.extensions.formatShort +import com.github.libretube.extensions.toLocalDate import com.google.common.math.IntMath.pow -import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDate -import kotlinx.datetime.toLocalDateTime import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId @@ -51,9 +50,7 @@ object TextUtils { } fun localizeInstant(instant: kotlinx.datetime.Instant): String { - val date = instant.toLocalDateTime(TimeZone.currentSystemDefault()).date - - return localizeDate(date) + return localizeDate(instant.toLocalDate()) } /** diff --git a/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt b/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt index 252771e8c..202234bb6 100644 --- a/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt +++ b/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt @@ -82,7 +82,7 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) : // fetch the users feed val videoFeed = try { withContext(Dispatchers.IO) { - SubscriptionHelper.getFeed() + SubscriptionHelper.getFeed(forceRefresh = true) } } catch (e: Exception) { return false diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a4be619a..5a4ff5625 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -526,6 +526,8 @@ Would you like to play the video from the download folder? %1$s views Only delete already watched videos + Local feed extraction + Directly fetch the feed from YouTube. This may be significantly slower. Download Service diff --git a/app/src/main/res/xml/instance_settings.xml b/app/src/main/res/xml/instance_settings.xml index 37034dc1a..8823f57e1 100644 --- a/app/src/main/res/xml/instance_settings.xml +++ b/app/src/main/res/xml/instance_settings.xml @@ -91,4 +91,15 @@ + + + + + + \ No newline at end of file