1
0
Fork 0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2025-10-03 01:39:38 +02:00

Refactor codebase to use Instant instead of OffsetDateTime

This commit is contained in:
Isira Seneviratne 2025-09-13 05:35:03 +05:30
parent fd3f030d0b
commit 88b687f57b
25 changed files with 159 additions and 184 deletions

View file

@ -144,7 +144,7 @@ class DatabaseMigrationTest {
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
assertNull(streamFromMigratedDatabase.viewCount)
assertNull(streamFromMigratedDatabase.textualUploadDate)
assertNull(streamFromMigratedDatabase.uploadDate)
assertNull(streamFromMigratedDatabase.uploadInstant)
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
val secondStreamFromMigratedDatabase = listFromDB[1]
@ -158,7 +158,7 @@ class DatabaseMigrationTest {
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
assertNull(secondStreamFromMigratedDatabase.viewCount)
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
assertNull(secondStreamFromMigratedDatabase.uploadDate)
assertNull(secondStreamFromMigratedDatabase.uploadInstant)
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
}

View file

@ -4,31 +4,27 @@ import androidx.room.TypeConverter
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
class Converters {
/**
* Convert a long value to a [OffsetDateTime].
* Convert a long value to an [Instant].
*
* @param value the long value
* @return the `OffsetDateTime`
* @return the `Instant`
*/
@TypeConverter
fun offsetDateTimeFromTimestamp(value: Long?): OffsetDateTime? {
return value?.let { OffsetDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) }
fun timestampToInstant(value: Long?): Instant? {
return value?.let { Instant.ofEpochMilli(it) }
}
/**
* Convert a [OffsetDateTime] to a long value.
* Convert an [Instant] to a long value.
*
* @param offsetDateTime the `OffsetDateTime`
* @param instant the `Instant`
* @return the long value
*/
@TypeConverter
fun offsetDateTimeToTimestamp(offsetDateTime: OffsetDateTime?): Long? {
return offsetDateTime?.withOffsetSameInstant(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
}
fun instantToTimestamp(instant: Instant?) = instant?.toEpochMilli()
@TypeConverter
fun streamTypeOf(value: String): StreamType {

View file

@ -15,7 +15,7 @@ import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
import java.time.Instant
@Dao
abstract class FeedDAO {
@ -90,7 +90,7 @@ abstract class FeedDAO {
groupId: Long,
includePlayed: Boolean,
includePartiallyPlayed: Boolean,
uploadDateBefore: OffsetDateTime?
uploadDateBefore: Instant?
): Maybe<List<StreamWithState>>
/**
@ -99,7 +99,7 @@ abstract class FeedDAO {
*
* One stream per uploader is kept because it is needed as reference
* when fetching new streams to check if they are new or not.
* @param offsetDateTime the newest date to keep, older streams are removed
* @param instant the newest date to keep, older streams are removed
*/
@Query(
"""
@ -115,11 +115,11 @@ abstract class FeedDAO {
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :offsetDateTime
WHERE s.upload_date < :instant
AND s.upload_date <> max_upload_date))
"""
)
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
abstract fun unlinkStreamsOlderThan(instant: Instant)
@Query(
"""
@ -168,13 +168,13 @@ abstract class FeedDAO {
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
"""
)
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
abstract fun getOldestSubscriptionUpdate(groupId: Long): Flowable<List<Instant>>
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
abstract fun getOldestSubscriptionUpdateFromAll(): Flowable<List<Instant>>
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
abstract fun notLoadedCount(): Flowable<Long>
abstract fun getNotLoadedCount(): Flowable<Long>
@Query(
"""
@ -189,7 +189,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL
"""
)
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
abstract fun getNotLoadedCountForGroup(groupId: Long): Flowable<Long>
@Query(
"""
@ -201,7 +201,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
"""
)
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
abstract fun getAllOutdated(outdatedThreshold: Instant): Flowable<List<SubscriptionEntity>>
@Query(
"""
@ -216,7 +216,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
"""
)
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Instant): Flowable<List<SubscriptionEntity>>
@Query(
"""
@ -231,7 +231,7 @@ abstract class FeedDAO {
"""
)
abstract fun getOutdatedWithNotificationMode(
outdatedThreshold: OffsetDateTime,
outdatedThreshold: Instant,
@NotificationMode notificationMode: Int
): Flowable<List<SubscriptionEntity>>
}

View file

@ -7,7 +7,7 @@ import androidx.room.PrimaryKey
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
import java.time.Instant
@Entity(
tableName = FEED_LAST_UPDATED_TABLE,
@ -26,7 +26,7 @@ data class FeedLastUpdatedEntity(
var subscriptionId: Long,
@ColumnInfo(name = LAST_UPDATED)
var lastUpdated: OffsetDateTime? = null
var lastUpdated: Instant? = null
) {
companion object {
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"

View file

@ -5,18 +5,16 @@ import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import java.time.OffsetDateTime
import java.time.Instant
@Entity(
tableName = SearchHistoryEntry.TABLE_NAME,
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
)
data class SearchHistoryEntry(
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
@field:ColumnInfo(
name = SERVICE_ID
) var serviceId: Int,
@field:ColumnInfo(name = SEARCH) var search: String?
@ColumnInfo(name = CREATION_DATE) var creationInstant: Instant?,
@ColumnInfo(name = SERVICE_ID) var serviceId: Int,
@ColumnInfo(name = SEARCH) var search: String?
) {
@ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true)
@ -24,10 +22,7 @@ data class SearchHistoryEntry(
@Ignore
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
return (
serviceId == otherEntry.serviceId &&
search == otherEntry.search
)
return serviceId == otherEntry.serviceId && search == otherEntry.search
}
companion object {

View file

@ -8,7 +8,7 @@ import androidx.room.Index;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.time.OffsetDateTime;
import java.time.Instant;
import static androidx.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
@ -36,21 +36,21 @@ public class StreamHistoryEntity {
@NonNull
@ColumnInfo(name = STREAM_ACCESS_DATE)
private OffsetDateTime accessDate;
private Instant accessInstant;
@ColumnInfo(name = STREAM_REPEAT_COUNT)
private long repeatCount;
/**
* @param streamUid the stream id this history item will refer to
* @param accessDate the last time the stream was accessed
* @param accessInstant the last time the stream was accessed
* @param repeatCount the total number of views this stream received
*/
public StreamHistoryEntity(final long streamUid,
@NonNull final OffsetDateTime accessDate,
@NonNull final Instant accessInstant,
final long repeatCount) {
this.streamUid = streamUid;
this.accessDate = accessDate;
this.accessInstant = accessInstant;
this.repeatCount = repeatCount;
}
@ -63,12 +63,12 @@ public class StreamHistoryEntity {
}
@NonNull
public OffsetDateTime getAccessDate() {
return accessDate;
public Instant getAccessInstant() {
return accessInstant;
}
public void setAccessDate(@NonNull final OffsetDateTime accessDate) {
this.accessDate = accessDate;
public void setAccessInstant(@NonNull final Instant accessInstant) {
this.accessInstant = accessInstant;
}
public long getRepeatCount() {

View file

@ -5,7 +5,7 @@ import androidx.room.Embedded
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime
import java.time.Instant
data class StreamHistoryEntry(
@Embedded
@ -15,21 +15,11 @@ data class StreamHistoryEntry(
val streamId: Long,
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
val accessDate: OffsetDateTime,
val accessInstant: Instant,
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
val repeatCount: Long
) {
fun toStreamHistoryEntity(): StreamHistoryEntity {
return StreamHistoryEntity(streamId, accessDate, repeatCount)
}
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
accessDate.isEqual(other.accessDate)
}
fun toStreamInfoItem(): StreamInfoItem =
StreamInfoItem(
streamEntity.serviceId,

View file

@ -8,7 +8,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime
import java.time.Instant
class StreamStatisticsEntry(
@Embedded
@ -21,7 +21,7 @@ class StreamStatisticsEntry(
val streamId: Long,
@ColumnInfo(name = STREAM_LATEST_DATE)
val latestAccessDate: OffsetDateTime,
val latestAccessInstant: Instant,
@ColumnInfo(name = STREAM_WATCH_COUNT)
val watchCount: Long

View file

@ -13,7 +13,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime
import java.time.Instant
@Dao
abstract class StreamDAO : BasicDAO<StreamEntity> {
@ -95,9 +95,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
// Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes.
val hasBetterPrecision =
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
newerStream.uploadDate = existentMinimalStream.uploadDate
newerStream.uploadInstant != null && newerStream.isUploadDateApproximation != true
if (existentMinimalStream.uploadInstant != null && !hasBetterPrecision) {
newerStream.uploadInstant = existentMinimalStream.uploadInstant
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation
}
@ -138,7 +138,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
var textualUploadDate: String? = null,
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
var uploadDate: OffsetDateTime? = null,
var uploadInstant: Instant? = null,
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null,

View file

@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.player.playqueue.PlayQueueItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.io.Serializable
import java.time.OffsetDateTime
import java.time.Instant
@Entity(
tableName = STREAM_TABLE,
@ -59,7 +59,7 @@ data class StreamEntity(
var textualUploadDate: String? = null,
@ColumnInfo(name = STREAM_UPLOAD_DATE)
var uploadDate: OffsetDateTime? = null,
var uploadInstant: Instant? = null,
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null
@ -69,8 +69,9 @@ data class StreamEntity(
serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails),
viewCount = item.viewCount, textualUploadDate = item.textualUploadDate,
uploadInstant = item.uploadDate?.instant,
isUploadDateApproximation = item.uploadDate?.isApproximation
)
@ -79,8 +80,9 @@ data class StreamEntity(
serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
uploaderUrl = info.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails),
viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadInstant = info.uploadDate?.instant,
isUploadDateApproximation = info.uploadDate?.isApproximation
)
@ -101,7 +103,7 @@ data class StreamEntity(
if (viewCount != null) item.viewCount = viewCount as Long
item.textualUploadDate = textualUploadDate
item.uploadDate = uploadDate?.let {
item.uploadDate = uploadInstant?.let {
DateWrapper(it, isUploadDateApproximation ?: false)
}

View file

@ -65,11 +65,12 @@ public class DescriptionFragment extends BaseDescriptionFragment {
}
@Override
protected void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
if (streamInfo != null && streamInfo.getUploadDate() != null) {
binding.detailUploadDateView.setText(Localization
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
protected void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) {
final var uploadDate = streamInfo != null ? streamInfo.getUploadDate() : null;
if (uploadDate != null) {
final String formattedDate = activity.getString(R.string.upload_date_text,
Localization.formatDate(uploadDate));
binding.detailUploadDateView.setText(formattedDate);
} else {
binding.detailUploadDateView.setVisibility(View.GONE);
}

View file

@ -90,7 +90,7 @@ public final class CommentRepliesFragment
// setup author name and comment date
binding.authorName.setText(item.getUploaderName());
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
binding.uploadDate.setText(Localization.formatRelativeTimeOrTextual(
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
binding.authorTouchArea.setOnClickListener(
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));

View file

@ -82,11 +82,9 @@ public class CommentInfoItemHolder extends InfoItemHolder {
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
if (!(infoItem instanceof CommentsInfoItem item)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
@ -104,12 +102,9 @@ public class CommentInfoItemHolder extends InfoItemHolder {
// setup the top row, with pinned icon, author name and comment date
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
final String uploaderName = Localization.localizeUserName(item.getUploaderName());
itemTitleView.setText(Localization.concatenateStrings(
uploaderName,
Localization.relativeTimeOrTextual(
itemBuilder.getContext(),
item.getUploadDate(),
item.getTextualUploadDate())));
final String relativeTime = Localization.formatRelativeTimeOrTextual(
itemBuilder.getContext(), item.getUploadDate(), item.getTextualUploadDate());
itemTitleView.setText(Localization.concatenateStrings(uploaderName, relativeTime));
// setup bottom row, with likes, heart and replies button
itemLikesCountView.setText(

View file

@ -77,7 +77,7 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
}
}
final String uploadDate = Localization.relativeTimeOrTextual(itemBuilder.getContext(),
final String uploadDate = Localization.formatRelativeTimeOrTextual(itemBuilder.getContext(),
infoItem.getUploadDate(),
infoItem.getTextualUploadDate());
if (!TextUtils.isEmpty(uploadDate)) {

View file

@ -18,9 +18,9 @@ import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.ZoneId
class FeedDatabaseManager(context: Context) {
private val database = NewPipeDatabase.getInstance(context)
@ -32,8 +32,7 @@ class FeedDatabaseManager(context: Context) {
/**
* Only items that are newer than this will be saved.
*/
val FEED_OLDEST_ALLOWED_DATE: OffsetDateTime = LocalDate.now().minusWeeks(13)
.atStartOfDay().atOffset(ZoneOffset.UTC)
val FEED_OLDEST_ALLOWED_DATE: LocalDate = LocalDate.now().minusWeeks(13)
}
fun groups() = feedGroupTable.getAll()
@ -50,27 +49,27 @@ class FeedDatabaseManager(context: Context) {
groupId,
includePlayedStreams,
includePartiallyPlayedStreams,
if (includeFutureStreams) null else OffsetDateTime.now()
if (includeFutureStreams) null else Instant.now()
)
}
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
fun getOutdatedSubscriptions(outdatedThreshold: Instant) = feedTable.getAllOutdated(outdatedThreshold)
fun outdatedSubscriptionsWithNotificationMode(
outdatedThreshold: OffsetDateTime,
fun getOutdatedSubscriptionsWithNotificationMode(
outdatedThreshold: Instant,
@NotificationMode notificationMode: Int
) = feedTable.getOutdatedWithNotificationMode(outdatedThreshold, notificationMode)
fun notLoadedCount(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<Long> {
fun getNotLoadedCount(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<Long> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.notLoadedCount()
else -> feedTable.notLoadedCountForGroup(groupId)
FeedGroupEntity.GROUP_ALL_ID -> feedTable.getNotLoadedCount()
else -> feedTable.getNotLoadedCountForGroup(groupId)
}
}
fun outdatedSubscriptionsForGroup(
fun getOutdatedSubscriptionsForGroup(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
outdatedThreshold: OffsetDateTime
outdatedThreshold: Instant
) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable
@ -83,17 +82,14 @@ class FeedDatabaseManager(context: Context) {
fun upsertAll(
subscriptionId: Long,
items: List<StreamInfoItem>,
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE
oldestAllowedDate: LocalDate = FEED_OLDEST_ALLOWED_DATE
) {
val itemsToInsert = ArrayList<StreamInfoItem>()
loop@ for (streamItem in items) {
val uploadDate = streamItem.uploadDate
val zoneId = ZoneId.systemDefault()
val itemsToInsert = items.filter {
val uploadDate = it.uploadDate?.let { LocalDate.ofInstant(it.instant, zoneId) }
itemsToInsert += when {
uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> streamItem
else -> continue@loop
}
(uploadDate == null && it.streamType == StreamType.LIVE_STREAM) ||
(uploadDate != null && uploadDate >= oldestAllowedDate)
}
feedTable.unlinkOldLivestreams(subscriptionId)
@ -107,12 +103,13 @@ class FeedDatabaseManager(context: Context) {
}
feedTable.setLastUpdatedForSubscription(
FeedLastUpdatedEntity(subscriptionId, OffsetDateTime.now(ZoneOffset.UTC))
FeedLastUpdatedEntity(subscriptionId, Instant.now())
)
}
fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
feedTable.unlinkStreamsOlderThan(oldestAllowedDate)
fun removeOrphansOrOlderStreams(oldestAllowedDate: LocalDate = FEED_OLDEST_ALLOWED_DATE) {
val instant = oldestAllowedDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
feedTable.unlinkStreamsOlderThan(instant)
streamTable.deleteOrphans()
}
@ -177,10 +174,10 @@ class FeedDatabaseManager(context: Context) {
.observeOn(AndroidSchedulers.mainThread())
}
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> {
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Instant>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()
else -> feedTable.oldestSubscriptionUpdate(groupId)
FeedGroupEntity.GROUP_ALL_ID -> feedTable.getOldestSubscriptionUpdateFromAll()
else -> feedTable.getOldestSubscriptionUpdate(groupId)
}
}
}

View file

@ -81,7 +81,7 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.time.Instant
import java.util.function.Consumer
class FeedFragment : BaseStateFragment<FeedState>() {
@ -95,7 +95,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = ""
private var oldestSubscriptionUpdate: OffsetDateTime? = null
private var oldestSubscriptionUpdate: Instant? = null
private lateinit var groupAdapter: GroupieAdapter
@ -415,8 +415,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
groupAdapter.updateAsync(loadedState.items, false) {
oldOldestSubscriptionUpdate?.run {
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
oldOldestSubscriptionUpdate?.let {
highlightNewItemsAfter(it)
}
}
@ -543,14 +543,14 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private fun updateRefreshViewState() {
feedBinding.refreshText.text = getString(
R.string.feed_oldest_subscription_update,
oldestSubscriptionUpdate?.let { Localization.relativeTime(it) } ?: ""
oldestSubscriptionUpdate?.let { Localization.formatRelativeTime(it) } ?: ""
)
}
/**
* Highlights all items that are after the specified time
*/
private fun highlightNewItemsAfter(updateTime: OffsetDateTime) {
private fun highlightNewItemsAfter(updateTime: Instant) {
var highlightCount = 0
var doCheck = true
@ -563,8 +563,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
}
if (doCheck) {
val instant = item.streamWithState.stream.uploadInstant
// If the uploadDate is null or true we should highlight the item
if (item.streamWithState.stream.uploadDate?.isAfter(updateTime) != false) {
if (instant != null && instant > updateTime) {
highlightCount++
typeface = Typeface.DEFAULT_BOLD

View file

@ -2,7 +2,7 @@ package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes
import org.schabi.newpipe.local.feed.item.StreamItem
import java.time.OffsetDateTime
import java.time.Instant
sealed class FeedState {
data class ProgressState(
@ -13,7 +13,7 @@ sealed class FeedState {
data class LoadedState(
val items: List<StreamItem>,
val oldestUpdate: OffsetDateTime?,
val oldestUpdate: Instant?,
val notLoadedCount: Long,
val itemsErrors: List<Throwable>
) : FeedState()

View file

@ -25,7 +25,7 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime
import java.time.Instant
import java.util.concurrent.TimeUnit
class FeedViewModel(
@ -61,11 +61,11 @@ class FeedViewModel(
showPlayedItemsFlowable,
showPartiallyPlayedItemsFlowable,
showFutureItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.getNotLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean,
t5: Long, t6: List<OffsetDateTime> ->
t5: Long, t6: List<Instant> ->
return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
}
)
@ -109,14 +109,14 @@ class FeedViewModel(
val t3: Boolean,
val t4: Boolean,
val t5: Long,
val t6: OffsetDateTime?
val t6: Instant?
)
private data class CombineResultDataHolder(
val t1: FeedEventManager.Event,
val t2: List<StreamWithState>,
val t3: Long,
val t4: OffsetDateTime?
val t4: Instant?
)
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {

View file

@ -137,9 +137,9 @@ data class StreamItem(
}
private fun getFormattedRelativeUploadDate(context: Context): String? {
val uploadDate = stream.uploadDate
val uploadDate = stream.uploadInstant
return if (uploadDate != null) {
var formattedRelativeTime = Localization.relativeTime(uploadDate)
var formattedRelativeTime = Localization.formatRelativeTime(uploadDate)
if (MainActivity.DEBUG) {
val key = context.getString(R.string.show_original_time_ago_key)

View file

@ -27,8 +27,9 @@ import org.schabi.newpipe.util.ChannelTabHelper
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@ -69,26 +70,26 @@ class FeedLoadManager(private val context: Context) {
)
val outdatedThreshold = if (ignoreOutdatedThreshold) {
OffsetDateTime.now(ZoneOffset.UTC)
Instant.now()
} else {
val thresholdOutdatedSeconds = defaultSharedPreferences.getStringSafe(
context.getString(R.string.feed_update_threshold_key),
context.getString(R.string.feed_update_threshold_default_value)
).toInt()
OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong())
).toLong()
Instant.now().minusSeconds(thresholdOutdatedSeconds)
}
/**
* subscriptions which have not been updated within the feed updated threshold
*/
val outdatedSubscriptions = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.getOutdatedSubscriptions(
outdatedThreshold
)
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.getOutdatedSubscriptionsWithNotificationMode(
outdatedThreshold, NotificationMode.ENABLED
)
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
else -> feedDatabaseManager.getOutdatedSubscriptionsForGroup(groupId, outdatedThreshold)
}
// like `currentProgress`, but counts the number of YouTube extractions that have begun, so
@ -319,15 +320,14 @@ class FeedLoadManager(private val context: Context) {
}
private fun filterNewStreams(list: List<StreamInfoItem>): List<StreamInfoItem> {
val zoneId = ZoneId.systemDefault()
return list.filter {
// Streams older than this date are automatically removed from the feed.
// Therefore, streams which are not in the database,
// but older than this date, are considered old.
val date = it.uploadDate?.let { LocalDate.ofInstant(it.instant, zoneId) }
!feedDatabaseManager.doesStreamExist(it) &&
it.uploadDate != null &&
// Streams older than this date are automatically removed from the feed.
// Therefore, streams which are not in the database,
// but older than this date, are considered old.
it.uploadDate!!.offsetDateTime().isAfter(
FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE
)
date != null && date > FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE
}
}
}

View file

@ -47,8 +47,7 @@ import org.schabi.newpipe.local.feed.FeedViewModel;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@ -96,7 +95,7 @@ public class HistoryRecordManager {
return Maybe.empty();
}
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
final var currentTime = Instant.now();
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long streamId;
final long duration;
@ -139,14 +138,14 @@ public class HistoryRecordManager {
return Maybe.empty();
}
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
final var currentTime = Instant.now();
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
if (latestEntry != null) {
streamHistoryTable.delete(latestEntry);
latestEntry.setAccessDate(currentTime);
latestEntry.setAccessInstant(currentTime);
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
return streamHistoryTable.insert(latestEntry);
} else {
@ -194,13 +193,13 @@ public class HistoryRecordManager {
return Maybe.empty();
}
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
final var currentTime = Instant.now();
final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search);
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry();
if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) {
latestEntry.setCreationDate(currentTime);
latestEntry.setCreationInstant(currentTime);
return (long) searchHistoryTable.update(latestEntry);
} else {
return searchHistoryTable.insert(newEntry);

View file

@ -69,7 +69,7 @@ public class StatisticsPlaylistFragment
final Comparator<StreamStatisticsEntry> comparator;
switch (sortMode) {
case LAST_PLAYED:
comparator = Comparator.comparing(StreamStatisticsEntry::getLatestAccessDate);
comparator = Comparator.comparing(StreamStatisticsEntry::getLatestAccessInstant);
break;
case MOST_PLAYED:
comparator = Comparator.comparingLong(StreamStatisticsEntry::getWatchCount);

View file

@ -74,7 +74,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
return Localization.concatenateStrings(
// watchCount
Localization.shortViewCount(itemBuilder.getContext(), entry.getWatchCount()),
dateTimeFormatter.format(entry.getLatestAccessDate()),
dateTimeFormatter.format(entry.latestAccessInstant()),
// serviceName
ServiceHelper.getNameOfServiceById(entry.getStreamEntity().getServiceId()));
}

View file

@ -2,7 +2,6 @@ package org.schabi.newpipe.util;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
@ -34,7 +33,8 @@ import org.schabi.newpipe.extractor.stream.AudioTrackType;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.time.OffsetDateTime;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
@ -130,16 +130,11 @@ public final class Localization {
return NumberFormat.getInstance(getAppLocale()).format(number);
}
public static String formatDate(@NonNull final OffsetDateTime offsetDateTime) {
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(getAppLocale())
.format(offsetDateTime.atZoneSameInstant(ZoneId.systemDefault()));
}
@SuppressLint("StringFormatInvalid")
public static String localizeUploadDate(@NonNull final Context context,
@NonNull final OffsetDateTime offsetDateTime) {
return context.getString(R.string.upload_date_text, formatDate(offsetDateTime));
@NonNull
public static String formatDate(@NonNull final DateWrapper dateWrapper) {
final var localDate = LocalDate.ofInstant(dateWrapper.getInstant(), ZoneId.systemDefault());
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(getAppLocale())
.format(localDate);
}
public static String localizeViewCount(@NonNull final Context context, final long viewCount) {
@ -379,8 +374,8 @@ public final class Localization {
return new PrettyTime(getAppLocale());
}
public static String relativeTime(@NonNull final OffsetDateTime offsetDateTime) {
return prettyTime.formatUnrounded(offsetDateTime);
public static String formatRelativeTime(@NonNull final Instant instant) {
return prettyTime.formatUnrounded(instant);
}
/**
@ -389,23 +384,23 @@ public final class Localization {
* @param parsed the textual date or time ago parsed by NewPipeExtractor, or {@code null} if
* the extractor could not parse it
* @param textual the original textual date or time ago string as provided by services
* @return {@link #relativeTime(OffsetDateTime)} is used if {@code parsed != null}, otherwise
* @return {@link #formatRelativeTime(Instant)} is used if {@code parsed != null}, otherwise
* {@code textual} is returned. If in debug mode, {@code context != null},
* {@code parsed != null} and the relevant setting is enabled, {@code textual} will
* be appended to the returned string for debugging purposes.
*/
@Nullable
public static String relativeTimeOrTextual(@Nullable final Context context,
@Nullable final DateWrapper parsed,
@Nullable final String textual) {
public static String formatRelativeTimeOrTextual(@Nullable final Context context,
@Nullable final DateWrapper parsed,
@Nullable final String textual) {
if (parsed == null) {
return textual;
} else if (DEBUG && context != null && PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_original_time_ago_key), false)) {
return relativeTime(parsed.offsetDateTime()) + " (" + textual + ")";
return formatRelativeTime(parsed.getInstant()) + " (" + textual + ")";
} else {
return relativeTime(parsed.offsetDateTime());
return formatRelativeTime(parsed.getInstant());
}
}

View file

@ -3,25 +3,29 @@ package org.schabi.newpipe.util
import org.junit.Assert.assertEquals
import org.junit.Test
import org.ocpsoft.prettytime.PrettyTime
import java.time.Instant
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.Month
import java.time.ZoneId
import java.util.Locale
class LocalizationTest {
@Test(expected = NullPointerException::class)
fun `relativeTime() must fail without initializing pretty time`() {
Localization.relativeTime(OffsetDateTime.of(2021, 1, 6, 0, 0, 0, 0, ZoneOffset.UTC))
fun `formatRelativeTime() must fail without initializing pretty time`() {
val instant = Instant.now()
Localization.formatRelativeTime(instant)
}
@Test
fun `relativeTime() with a OffsetDateTime must work`() {
val prettyTime = PrettyTime(LocalDate.of(2021, 1, 1), ZoneOffset.UTC)
fun `formatRelativeTime() with an Instant must work`() {
val zoneId = ZoneId.systemDefault()
val date = LocalDate.of(2021, Month.JANUARY, 1)
val prettyTime = PrettyTime(date, zoneId)
prettyTime.locale = Locale.ENGLISH
Localization.initPrettyTime(prettyTime)
val offset = OffsetDateTime.of(2021, 1, 6, 0, 0, 0, 0, ZoneOffset.UTC)
val actual = Localization.relativeTime(offset)
val instant = date.plusDays(5).atStartOfDay(zoneId).toInstant()
val actual = Localization.formatRelativeTime(instant)
assertEquals("5 days from now", actual)
}