1
0
Fork 0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2025-10-03 01:39:38 +02:00
This commit is contained in:
Isira Seneviratne 2025-10-02 17:36:36 +02:00 committed by GitHub
commit 910ad17422
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 208 additions and 271 deletions

View file

@ -214,7 +214,7 @@ dependencies {
// the corresponding commit hash, since JitPack sometimes deletes artifacts. // the corresponding commit hash, since JitPack sometimes deletes artifacts.
// If theres already a git hash, just add more of it to the end (or remove a letter) // If theres already a git hash, just add more of it to the end (or remove a letter)
// to cause jitpack to regenerate the artifact. // to cause jitpack to regenerate the artifact.
implementation 'com.github.TeamNewPipe:NewPipeExtractor:0023b22095a2d62a60cdfc87f4b5cd85c8b266c3' implementation 'com.github.TeamNewPipe:NewPipeExtractor:c79afc00764a5f2deb8f73e78244d93920cb3456'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/ /** Checkstyle **/
@ -229,6 +229,7 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-i18n:1.0.0'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.6.2' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"

View file

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

View file

@ -4,31 +4,27 @@ import androidx.room.TypeConverter
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
class Converters { class Converters {
/** /**
* Convert a long value to a [OffsetDateTime]. * Convert a long value to an [Instant].
* *
* @param value the long value * @param value the long value
* @return the `OffsetDateTime` * @return the `Instant`
*/ */
@TypeConverter @TypeConverter
fun offsetDateTimeFromTimestamp(value: Long?): OffsetDateTime? { fun timestampToInstant(value: Long?): Instant? {
return value?.let { OffsetDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) } 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 * @return the long value
*/ */
@TypeConverter @TypeConverter
fun offsetDateTimeToTimestamp(offsetDateTime: OffsetDateTime?): Long? { fun instantToTimestamp(instant: Instant?) = instant?.toEpochMilli()
return offsetDateTime?.withOffsetSameInstant(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
}
@TypeConverter @TypeConverter
fun streamTypeOf(value: String): StreamType { 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.stream.model.StreamStateEntity
import org.schabi.newpipe.database.subscription.NotificationMode import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime import java.time.Instant
@Dao @Dao
abstract class FeedDAO { abstract class FeedDAO {
@ -90,7 +90,7 @@ abstract class FeedDAO {
groupId: Long, groupId: Long,
includePlayed: Boolean, includePlayed: Boolean,
includePartiallyPlayed: Boolean, includePartiallyPlayed: Boolean,
uploadDateBefore: OffsetDateTime? uploadDateBefore: Instant?
): Maybe<List<StreamWithState>> ): Maybe<List<StreamWithState>>
/** /**
@ -99,7 +99,7 @@ abstract class FeedDAO {
* *
* One stream per uploader is kept because it is needed as reference * One stream per uploader is kept because it is needed as reference
* when fetching new streams to check if they are new or not. * 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( @Query(
""" """
@ -115,11 +115,11 @@ abstract class FeedDAO {
INNER JOIN feed f INNER JOIN feed f
ON s.uid = f.stream_id ON s.uid = f.stream_id
WHERE s.upload_date < :offsetDateTime WHERE s.upload_date < :instant
AND s.upload_date <> max_upload_date)) AND s.upload_date <> max_upload_date))
""" """
) )
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime) abstract fun unlinkStreamsOlderThan(instant: Instant)
@Query( @Query(
""" """
@ -168,13 +168,13 @@ abstract class FeedDAO {
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId 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") @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") @Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
abstract fun notLoadedCount(): Flowable<Long> abstract fun getNotLoadedCount(): Flowable<Long>
@Query( @Query(
""" """
@ -189,7 +189,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL WHERE lu.last_updated IS NULL
""" """
) )
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long> abstract fun getNotLoadedCountForGroup(groupId: Long): Flowable<Long>
@Query( @Query(
""" """
@ -201,7 +201,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold 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( @Query(
""" """
@ -216,7 +216,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold 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( @Query(
""" """
@ -231,7 +231,7 @@ abstract class FeedDAO {
""" """
) )
abstract fun getOutdatedWithNotificationMode( abstract fun getOutdatedWithNotificationMode(
outdatedThreshold: OffsetDateTime, outdatedThreshold: Instant,
@NotificationMode notificationMode: Int @NotificationMode notificationMode: Int
): Flowable<List<SubscriptionEntity>> ): 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.FEED_LAST_UPDATED_TABLE
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime import java.time.Instant
@Entity( @Entity(
tableName = FEED_LAST_UPDATED_TABLE, tableName = FEED_LAST_UPDATED_TABLE,
@ -26,7 +26,7 @@ data class FeedLastUpdatedEntity(
var subscriptionId: Long, var subscriptionId: Long,
@ColumnInfo(name = LAST_UPDATED) @ColumnInfo(name = LAST_UPDATED)
var lastUpdated: OffsetDateTime? = null var lastUpdated: Instant? = null
) { ) {
companion object { companion object {
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated" 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.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.time.OffsetDateTime import java.time.Instant
@Entity( @Entity(
tableName = SearchHistoryEntry.TABLE_NAME, tableName = SearchHistoryEntry.TABLE_NAME,
indices = [Index(value = [SearchHistoryEntry.SEARCH])] indices = [Index(value = [SearchHistoryEntry.SEARCH])]
) )
data class SearchHistoryEntry( data class SearchHistoryEntry(
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?, @ColumnInfo(name = CREATION_DATE) var creationInstant: Instant?,
@field:ColumnInfo( @ColumnInfo(name = SERVICE_ID) var serviceId: Int,
name = SERVICE_ID @ColumnInfo(name = SEARCH) var search: String?
) var serviceId: Int,
@field:ColumnInfo(name = SEARCH) var search: String?
) { ) {
@ColumnInfo(name = ID) @ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -24,10 +22,7 @@ data class SearchHistoryEntry(
@Ignore @Ignore
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean { fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
return ( return serviceId == otherEntry.serviceId && search == otherEntry.search
serviceId == otherEntry.serviceId &&
search == otherEntry.search
)
} }
companion object { companion object {

View file

@ -8,7 +8,7 @@ import androidx.room.Index;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.time.OffsetDateTime; import java.time.Instant;
import static androidx.room.ForeignKey.CASCADE; import static androidx.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
@ -36,21 +36,21 @@ public class StreamHistoryEntity {
@NonNull @NonNull
@ColumnInfo(name = STREAM_ACCESS_DATE) @ColumnInfo(name = STREAM_ACCESS_DATE)
private OffsetDateTime accessDate; private Instant accessInstant;
@ColumnInfo(name = STREAM_REPEAT_COUNT) @ColumnInfo(name = STREAM_REPEAT_COUNT)
private long repeatCount; private long repeatCount;
/** /**
* @param streamUid the stream id this history item will refer to * @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 * @param repeatCount the total number of views this stream received
*/ */
public StreamHistoryEntity(final long streamUid, public StreamHistoryEntity(final long streamUid,
@NonNull final OffsetDateTime accessDate, @NonNull final Instant accessInstant,
final long repeatCount) { final long repeatCount) {
this.streamUid = streamUid; this.streamUid = streamUid;
this.accessDate = accessDate; this.accessInstant = accessInstant;
this.repeatCount = repeatCount; this.repeatCount = repeatCount;
} }
@ -63,12 +63,12 @@ public class StreamHistoryEntity {
} }
@NonNull @NonNull
public OffsetDateTime getAccessDate() { public Instant getAccessInstant() {
return accessDate; return accessInstant;
} }
public void setAccessDate(@NonNull final OffsetDateTime accessDate) { public void setAccessInstant(@NonNull final Instant accessInstant) {
this.accessDate = accessDate; this.accessInstant = accessInstant;
} }
public long getRepeatCount() { 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.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime import java.time.Instant
data class StreamHistoryEntry( data class StreamHistoryEntry(
@Embedded @Embedded
@ -15,21 +15,11 @@ data class StreamHistoryEntry(
val streamId: Long, val streamId: Long,
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
val accessDate: OffsetDateTime, val accessInstant: Instant,
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT) @ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
val repeatCount: Long 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 = fun toStreamInfoItem(): StreamInfoItem =
StreamInfoItem( StreamInfoItem(
streamEntity.serviceId, 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.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime import java.time.Instant
class StreamStatisticsEntry( class StreamStatisticsEntry(
@Embedded @Embedded
@ -21,7 +21,7 @@ class StreamStatisticsEntry(
val streamId: Long, val streamId: Long,
@ColumnInfo(name = STREAM_LATEST_DATE) @ColumnInfo(name = STREAM_LATEST_DATE)
val latestAccessDate: OffsetDateTime, val latestAccessInstant: Instant,
@ColumnInfo(name = STREAM_WATCH_COUNT) @ColumnInfo(name = STREAM_WATCH_COUNT)
val watchCount: Long 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.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.util.StreamTypeUtil import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime import java.time.Instant
@Dao @Dao
abstract class StreamDAO : BasicDAO<StreamEntity> { 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 // 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. // (i.e. is an approximation). This is done to prevent unnecessary changes.
val hasBetterPrecision = val hasBetterPrecision =
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true newerStream.uploadInstant != null && newerStream.isUploadDateApproximation != true
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) { if (existentMinimalStream.uploadInstant != null && !hasBetterPrecision) {
newerStream.uploadDate = existentMinimalStream.uploadDate newerStream.uploadInstant = existentMinimalStream.uploadInstant
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation
} }
@ -138,7 +138,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
var textualUploadDate: String? = null, var textualUploadDate: String? = null,
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE) @ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
var uploadDate: OffsetDateTime? = null, var uploadInstant: Instant? = null,
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION) @ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null, 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.player.playqueue.PlayQueueItem
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import java.io.Serializable import java.io.Serializable
import java.time.OffsetDateTime import java.time.Instant
@Entity( @Entity(
tableName = STREAM_TABLE, tableName = STREAM_TABLE,
@ -59,7 +59,7 @@ data class StreamEntity(
var textualUploadDate: String? = null, var textualUploadDate: String? = null,
@ColumnInfo(name = STREAM_UPLOAD_DATE) @ColumnInfo(name = STREAM_UPLOAD_DATE)
var uploadDate: OffsetDateTime? = null, var uploadInstant: Instant? = null,
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION) @ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null var isUploadDateApproximation: Boolean? = null
@ -69,8 +69,9 @@ data class StreamEntity(
serviceId = item.serviceId, url = item.url, title = item.name, serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
uploaderUrl = item.uploaderUrl, uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount, thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails),
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(), viewCount = item.viewCount, textualUploadDate = item.textualUploadDate,
uploadInstant = item.uploadDate?.instant,
isUploadDateApproximation = item.uploadDate?.isApproximation isUploadDateApproximation = item.uploadDate?.isApproximation
) )
@ -79,8 +80,9 @@ data class StreamEntity(
serviceId = info.serviceId, url = info.url, title = info.name, serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
uploaderUrl = info.uploaderUrl, uploaderUrl = info.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount, thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails),
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(), viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadInstant = info.uploadDate?.instant,
isUploadDateApproximation = info.uploadDate?.isApproximation isUploadDateApproximation = info.uploadDate?.isApproximation
) )
@ -101,7 +103,7 @@ data class StreamEntity(
if (viewCount != null) item.viewCount = viewCount as Long if (viewCount != null) item.viewCount = viewCount as Long
item.textualUploadDate = textualUploadDate item.textualUploadDate = textualUploadDate
item.uploadDate = uploadDate?.let { item.uploadDate = uploadInstant?.let {
DateWrapper(it, isUploadDateApproximation ?: false) DateWrapper(it, isUploadDateApproximation ?: false)
} }

View file

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

View file

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

View file

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

View file

@ -30,11 +30,8 @@ import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
import org.schabi.newpipe.util.FallbackViewHolder; import org.schabi.newpipe.util.FallbackViewHolder;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -85,7 +82,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private final LocalItemBuilder localItemBuilder; private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems; private final ArrayList<LocalItem> localItems;
private final HistoryRecordManager recordManager; private final HistoryRecordManager recordManager;
private final DateTimeFormatter dateTimeFormatter;
private boolean showFooter = false; private boolean showFooter = false;
private View header = null; private View header = null;
@ -97,8 +93,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
recordManager = new HistoryRecordManager(context); recordManager = new HistoryRecordManager(context);
localItemBuilder = new LocalItemBuilder(context); localItemBuilder = new LocalItemBuilder(context);
localItems = new ArrayList<>(); localItems = new ArrayList<>();
dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Localization.getPreferredLocale(context));
} }
public void setSelectedListener(final OnClickGesture<LocalItem> listener) { public void setSelectedListener(final OnClickGesture<LocalItem> listener) {
@ -364,19 +358,19 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
+ "position = [" + position + "]"); + "position = [" + position + "]");
} }
if (holder instanceof LocalItemHolder) { if (holder instanceof LocalItemHolder localItemHolder) {
// If header isn't null, offset the items by -1 // If header isn't null, offset the items by -1
if (header != null) { if (header != null) {
position--; position--;
} }
((LocalItemHolder) holder) localItemHolder.updateFromItem(localItems.get(position), recordManager);
.updateFromItem(localItems.get(position), recordManager, dateTimeFormatter); } else if (holder instanceof HeaderFooterHolder headerFooterHolder) {
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) { if (position == 0 && header != null) {
((HeaderFooterHolder) holder).view = header; headerFooterHolder.view = header;
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader() } else if (footer != null && showFooter) {
&& footer != null && showFooter) { headerFooterHolder.view = footer;
((HeaderFooterHolder) holder).view = footer; }
} }
} }

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.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.OffsetDateTime import java.time.ZoneId
import java.time.ZoneOffset
class FeedDatabaseManager(context: Context) { class FeedDatabaseManager(context: Context) {
private val database = NewPipeDatabase.getInstance(context) private val database = NewPipeDatabase.getInstance(context)
@ -28,14 +28,6 @@ class FeedDatabaseManager(context: Context) {
private val feedGroupTable = database.feedGroupDAO() private val feedGroupTable = database.feedGroupDAO()
private val streamTable = database.streamDAO() private val streamTable = database.streamDAO()
companion object {
/**
* Only items that are newer than this will be saved.
*/
val FEED_OLDEST_ALLOWED_DATE: OffsetDateTime = LocalDate.now().minusWeeks(13)
.atStartOfDay().atOffset(ZoneOffset.UTC)
}
fun groups() = feedGroupTable.getAll() fun groups() = feedGroupTable.getAll()
fun database() = database fun database() = database
@ -50,27 +42,27 @@ class FeedDatabaseManager(context: Context) {
groupId, groupId,
includePlayedStreams, includePlayedStreams,
includePartiallyPlayedStreams, 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( fun getOutdatedSubscriptionsWithNotificationMode(
outdatedThreshold: OffsetDateTime, outdatedThreshold: Instant,
@NotificationMode notificationMode: Int @NotificationMode notificationMode: Int
) = feedTable.getOutdatedWithNotificationMode(outdatedThreshold, notificationMode) ) = 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) { return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.notLoadedCount() FeedGroupEntity.GROUP_ALL_ID -> feedTable.getNotLoadedCount()
else -> feedTable.notLoadedCountForGroup(groupId) else -> feedTable.getNotLoadedCountForGroup(groupId)
} }
} }
fun outdatedSubscriptionsForGroup( fun getOutdatedSubscriptionsForGroup(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
outdatedThreshold: OffsetDateTime outdatedThreshold: Instant
) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) ) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable fun markAsOutdated(subscriptionId: Long) = feedTable
@ -80,20 +72,13 @@ class FeedDatabaseManager(context: Context) {
return streamTable.exists(stream.serviceId, stream.url) return streamTable.exists(stream.serviceId, stream.url)
} }
fun upsertAll( fun upsertAll(subscriptionId: Long, items: List<StreamInfoItem>) {
subscriptionId: Long, val oldestAllowedDate = LocalDate.now().minusWeeks(13)
items: List<StreamInfoItem>, val itemsToInsert = items.filter {
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE val uploadDate = it.uploadDate?.localDateTime?.toLocalDate()
) {
val itemsToInsert = ArrayList<StreamInfoItem>()
loop@ for (streamItem in items) {
val uploadDate = streamItem.uploadDate
itemsToInsert += when { (uploadDate == null && it.streamType == StreamType.LIVE_STREAM) ||
uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem (uploadDate != null && uploadDate >= oldestAllowedDate)
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> streamItem
else -> continue@loop
}
} }
feedTable.unlinkOldLivestreams(subscriptionId) feedTable.unlinkOldLivestreams(subscriptionId)
@ -107,12 +92,14 @@ class FeedDatabaseManager(context: Context) {
} }
feedTable.setLastUpdatedForSubscription( feedTable.setLastUpdatedForSubscription(
FeedLastUpdatedEntity(subscriptionId, OffsetDateTime.now(ZoneOffset.UTC)) FeedLastUpdatedEntity(subscriptionId, Instant.now())
) )
} }
fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) { fun removeOrphansOrOlderStreams() {
feedTable.unlinkStreamsOlderThan(oldestAllowedDate) val oldestAllowedDate = LocalDate.now().minusWeeks(13)
val instant = oldestAllowedDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
feedTable.unlinkStreamsOlderThan(instant)
streamTable.deleteOrphans() streamTable.deleteOrphans()
} }
@ -177,10 +164,10 @@ class FeedDatabaseManager(context: Context) {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> { fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Instant>> {
return when (groupId) { return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll() FeedGroupEntity.GROUP_ALL_ID -> feedTable.getOldestSubscriptionUpdateFromAll()
else -> feedTable.oldestSubscriptionUpdate(groupId) 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.getItemViewMode
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime import java.time.Instant
import java.util.function.Consumer import java.util.function.Consumer
class FeedFragment : BaseStateFragment<FeedState>() { class FeedFragment : BaseStateFragment<FeedState>() {
@ -95,7 +95,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private var groupId = FeedGroupEntity.GROUP_ALL_ID private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = "" private var groupName = ""
private var oldestSubscriptionUpdate: OffsetDateTime? = null private var oldestSubscriptionUpdate: Instant? = null
private lateinit var groupAdapter: GroupieAdapter private lateinit var groupAdapter: GroupieAdapter
@ -415,8 +415,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
groupAdapter.updateAsync(loadedState.items, false) { groupAdapter.updateAsync(loadedState.items, false) {
oldOldestSubscriptionUpdate?.run { oldOldestSubscriptionUpdate?.let {
highlightNewItemsAfter(oldOldestSubscriptionUpdate) highlightNewItemsAfter(it)
} }
} }
@ -543,14 +543,14 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private fun updateRefreshViewState() { private fun updateRefreshViewState() {
feedBinding.refreshText.text = getString( feedBinding.refreshText.text = getString(
R.string.feed_oldest_subscription_update, 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 * Highlights all items that are after the specified time
*/ */
private fun highlightNewItemsAfter(updateTime: OffsetDateTime) { private fun highlightNewItemsAfter(updateTime: Instant) {
var highlightCount = 0 var highlightCount = 0
var doCheck = true var doCheck = true
@ -563,8 +563,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
resolveDrawable(ctx, android.R.attr.selectableItemBackground) resolveDrawable(ctx, android.R.attr.selectableItemBackground)
} }
if (doCheck) { if (doCheck) {
val instant = item.streamWithState.stream.uploadInstant
// If the uploadDate is null or true we should highlight the item // 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++ highlightCount++
typeface = Typeface.DEFAULT_BOLD typeface = Typeface.DEFAULT_BOLD

View file

@ -2,7 +2,7 @@ package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.schabi.newpipe.local.feed.item.StreamItem import org.schabi.newpipe.local.feed.item.StreamItem
import java.time.OffsetDateTime import java.time.Instant
sealed class FeedState { sealed class FeedState {
data class ProgressState( data class ProgressState(
@ -13,7 +13,7 @@ sealed class FeedState {
data class LoadedState( data class LoadedState(
val items: List<StreamItem>, val items: List<StreamItem>,
val oldestUpdate: OffsetDateTime?, val oldestUpdate: Instant?,
val notLoadedCount: Long, val notLoadedCount: Long,
val itemsErrors: List<Throwable> val itemsErrors: List<Throwable>
) : FeedState() ) : 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.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime import java.time.Instant
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class FeedViewModel( class FeedViewModel(
@ -61,11 +61,11 @@ class FeedViewModel(
showPlayedItemsFlowable, showPlayedItemsFlowable,
showPartiallyPlayedItemsFlowable, showPartiallyPlayedItemsFlowable,
showFutureItemsFlowable, showFutureItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.getNotLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean, 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()) return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
} }
) )
@ -109,14 +109,14 @@ class FeedViewModel(
val t3: Boolean, val t3: Boolean,
val t4: Boolean, val t4: Boolean,
val t5: Long, val t5: Long,
val t6: OffsetDateTime? val t6: Instant?
) )
private data class CombineResultDataHolder( private data class CombineResultDataHolder(
val t1: FeedEventManager.Event, val t1: FeedEventManager.Event,
val t2: List<StreamWithState>, val t2: List<StreamWithState>,
val t3: Long, val t3: Long,
val t4: OffsetDateTime? val t4: Instant?
) )
fun setSaveShowPlayedItems(showPlayedItems: Boolean) { fun setSaveShowPlayedItems(showPlayedItems: Boolean) {

View file

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

View file

@ -27,8 +27,8 @@ import org.schabi.newpipe.util.ChannelTabHelper
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
import java.time.OffsetDateTime import java.time.Instant
import java.time.ZoneOffset import java.time.LocalDate
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -69,26 +69,26 @@ class FeedLoadManager(private val context: Context) {
) )
val outdatedThreshold = if (ignoreOutdatedThreshold) { val outdatedThreshold = if (ignoreOutdatedThreshold) {
OffsetDateTime.now(ZoneOffset.UTC) Instant.now()
} else { } else {
val thresholdOutdatedSeconds = defaultSharedPreferences.getStringSafe( val thresholdOutdatedSeconds = defaultSharedPreferences.getStringSafe(
context.getString(R.string.feed_update_threshold_key), context.getString(R.string.feed_update_threshold_key),
context.getString(R.string.feed_update_threshold_default_value) context.getString(R.string.feed_update_threshold_default_value)
).toInt() ).toLong()
OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong()) Instant.now().minusSeconds(thresholdOutdatedSeconds)
} }
/** /**
* subscriptions which have not been updated within the feed updated threshold * subscriptions which have not been updated within the feed updated threshold
*/ */
val outdatedSubscriptions = when (groupId) { val outdatedSubscriptions = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions( FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.getOutdatedSubscriptions(
outdatedThreshold outdatedThreshold
) )
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode( GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.getOutdatedSubscriptionsWithNotificationMode(
outdatedThreshold, NotificationMode.ENABLED 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 // like `currentProgress`, but counts the number of YouTube extractions that have begun, so
@ -253,7 +253,7 @@ class FeedLoadManager(private val context: Context) {
* Keep the feed and the stream tables small * Keep the feed and the stream tables small
* to reduce loading times when trying to display the feed. * to reduce loading times when trying to display the feed.
* <br> * <br>
* Remove streams from the feed which are older than [FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE]. * Remove streams from the feed which are older than 13 weeks.
* Remove streams from the database which are not linked / used by any table. * Remove streams from the database which are not linked / used by any table.
*/ */
private fun postProcessFeed() = Completable.fromRunnable { private fun postProcessFeed() = Completable.fromRunnable {
@ -319,15 +319,13 @@ class FeedLoadManager(private val context: Context) {
} }
private fun filterNewStreams(list: List<StreamInfoItem>): List<StreamInfoItem> { private fun filterNewStreams(list: List<StreamInfoItem>): List<StreamInfoItem> {
val oldestAllowedDate = LocalDate.now().minusWeeks(13)
return list.filter { return list.filter {
!feedDatabaseManager.doesStreamExist(it) && // Streams older than this date are automatically removed from the feed.
it.uploadDate != null && // Therefore, streams which are not in the database,
// Streams older than this date are automatically removed from the feed. // but older than this date, are considered old.
// Therefore, streams which are not in the database, val date = it.uploadDate?.localDateTime?.toLocalDate()
// but older than this date, are considered old. !feedDatabaseManager.doesStreamExist(it) && date != null && date > oldestAllowedDate
it.uploadDate!!.offsetDateTime().isAfter(
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.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import java.time.OffsetDateTime; import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -96,7 +95,7 @@ public class HistoryRecordManager {
return Maybe.empty(); return Maybe.empty();
} }
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC); final var currentTime = Instant.now();
return Maybe.fromCallable(() -> database.runInTransaction(() -> { return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long streamId; final long streamId;
final long duration; final long duration;
@ -139,14 +138,14 @@ public class HistoryRecordManager {
return Maybe.empty(); return Maybe.empty();
} }
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC); final var currentTime = Instant.now();
return Maybe.fromCallable(() -> database.runInTransaction(() -> { return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info)); final long streamId = streamTable.upsert(new StreamEntity(info));
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId); final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
if (latestEntry != null) { if (latestEntry != null) {
streamHistoryTable.delete(latestEntry); streamHistoryTable.delete(latestEntry);
latestEntry.setAccessDate(currentTime); latestEntry.setAccessInstant(currentTime);
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
return streamHistoryTable.insert(latestEntry); return streamHistoryTable.insert(latestEntry);
} else { } else {
@ -194,13 +193,13 @@ public class HistoryRecordManager {
return Maybe.empty(); return Maybe.empty();
} }
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC); final var currentTime = Instant.now();
final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search);
return Maybe.fromCallable(() -> database.runInTransaction(() -> { return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); final SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry();
if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) { if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) {
latestEntry.setCreationDate(currentTime); latestEntry.setCreationInstant(currentTime);
return (long) searchHistoryTable.update(latestEntry); return (long) searchHistoryTable.update(latestEntry);
} else { } else {
return searchHistoryTable.insert(newEntry); return searchHistoryTable.insert(newEntry);

View file

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

View file

@ -10,8 +10,6 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder { public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder {
private final View itemHandleView; private final View itemHandleView;
@ -28,16 +26,14 @@ public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder {
@Override @Override
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager) {
final DateTimeFormatter dateTimeFormatter) { if (!(localItem instanceof PlaylistMetadataEntry item)) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
return; return;
} }
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemHandleView.setOnTouchListener(getOnTouchListener(item)); itemHandleView.setOnTouchListener(getOnTouchListener(item));
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager);
} }
private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) { private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) {

View file

@ -9,8 +9,6 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
/* /*
* Created by Christian Schabesberger on 12.02.17. * Created by Christian Schabesberger on 12.02.17.
* *
@ -40,8 +38,7 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
this.itemBuilder = itemBuilder; this.itemBuilder = itemBuilder;
} }
public abstract void updateFromItem(LocalItem item, HistoryRecordManager historyRecordManager, public abstract void updateFromItem(LocalItem item, HistoryRecordManager historyRecordManager);
DateTimeFormatter dateTimeFormatter);
public void updateState(final LocalItem localItem, public void updateState(final LocalItem localItem,
final HistoryRecordManager historyRecordManager) { } final HistoryRecordManager historyRecordManager) { }

View file

@ -8,10 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.time.format.DateTimeFormatter;
public class LocalPlaylistItemHolder extends PlaylistItemHolder { public class LocalPlaylistItemHolder extends PlaylistItemHolder {
@ -28,12 +26,10 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
@Override @Override
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager) {
final DateTimeFormatter dateTimeFormatter) { if (!(localItem instanceof PlaylistMetadataEntry item)) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
return; return;
} }
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemTitleView.setText(item.name); itemTitleView.setText(item.name);
itemStreamCountView.setText(Localization.localizeStreamCountMini( itemStreamCountView.setText(Localization.localizeStreamCountMini(
@ -49,6 +45,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemView.setAlpha(1.0f); itemView.setAlpha(1.0f);
} }
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager);
} }
} }

View file

@ -16,11 +16,10 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
@ -50,12 +49,10 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
@Override @Override
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager) {
final DateTimeFormatter dateTimeFormatter) { if (!(localItem instanceof PlaylistStreamEntry item)) {
if (!(localItem instanceof PlaylistStreamEntry)) {
return; return;
} }
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
itemVideoTitleView.setText(item.getStreamEntity().getTitle()); itemVideoTitleView.setText(item.getStreamEntity().getTitle());
itemAdditionalDetailsView.setText(Localization itemAdditionalDetailsView.setText(Localization

View file

@ -1,12 +1,16 @@
package org.schabi.newpipe.local.holder; package org.schabi.newpipe.local.holder;
import static org.schabi.newpipe.util.ServiceHelper.getNameOfServiceById;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.i18n.MessageFormat;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
@ -17,10 +21,11 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter; import java.time.ZoneId;
import java.util.GregorianCalendar;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/* /*
@ -69,24 +74,24 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
itemProgressView = itemView.findViewById(R.id.itemProgressView); itemProgressView = itemView.findViewById(R.id.itemProgressView);
} }
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, @NonNull
final DateTimeFormatter dateTimeFormatter) { private String getStreamInfoDetailLine(@NonNull final StreamStatisticsEntry entry) {
return Localization.concatenateStrings( final var context = itemBuilder.getContext();
// watchCount final var zdt = entry.getLatestAccessInstant().atZone(ZoneId.systemDefault());
Localization.shortViewCount(itemBuilder.getContext(), entry.getWatchCount()), final Map<String, Object> args = Map.of(
dateTimeFormatter.format(entry.getLatestAccessDate()), "formatted_views", Localization.shortViewCount(context, entry.getWatchCount()),
// serviceName "last_viewed_date", GregorianCalendar.from(zdt),
ServiceHelper.getNameOfServiceById(entry.getStreamEntity().getServiceId())); "service_name", getNameOfServiceById(entry.getStreamEntity().getServiceId())
);
return MessageFormat.format(context, R.string.history_detail_line, args);
} }
@Override @Override
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager) {
final DateTimeFormatter dateTimeFormatter) { if (!(localItem instanceof StreamStatisticsEntry item)) {
if (!(localItem instanceof StreamStatisticsEntry)) {
return; return;
} }
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
itemVideoTitleView.setText(item.getStreamEntity().getTitle()); itemVideoTitleView.setText(item.getStreamEntity().getTitle());
itemUploaderView.setText(item.getStreamEntity().getUploader()); itemUploaderView.setText(item.getStreamEntity().getUploader());
@ -113,7 +118,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
} }
if (itemAdditionalDetails != null) { if (itemAdditionalDetails != null) {
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateTimeFormatter)); itemAdditionalDetails.setText(getStreamInfoDetailLine(item));
} }
// Default thumbnail is shown on error, while loading and if the url is empty // Default thumbnail is shown on error, while loading and if the url is empty

View file

@ -9,8 +9,6 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
public abstract class PlaylistItemHolder extends LocalItemHolder { public abstract class PlaylistItemHolder extends LocalItemHolder {
public final ImageView itemThumbnailView; public final ImageView itemThumbnailView;
final TextView itemStreamCountView; final TextView itemStreamCountView;
@ -33,8 +31,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
@Override @Override
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager) {
final DateTimeFormatter dateTimeFormatter) {
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) { if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().selected(localItem); itemBuilder.getOnItemSelectedListener().selected(localItem);

View file

@ -10,8 +10,6 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder { public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder {
private final View itemHandleView; private final View itemHandleView;
@ -28,16 +26,14 @@ public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder {
@Override @Override
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager) {
final DateTimeFormatter dateTimeFormatter) { if (!(localItem instanceof PlaylistRemoteEntity item)) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
return; return;
} }
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemHandleView.setOnTouchListener(getOnTouchListener(item)); itemHandleView.setOnTouchListener(getOnTouchListener(item));
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager);
} }
private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) { private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) {

View file

@ -8,10 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.time.format.DateTimeFormatter;
public class RemotePlaylistItemHolder extends PlaylistItemHolder { public class RemotePlaylistItemHolder extends PlaylistItemHolder {
@ -27,12 +25,10 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
@Override @Override
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager) {
final DateTimeFormatter dateTimeFormatter) { if (!(localItem instanceof PlaylistRemoteEntity item)) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
return; return;
} }
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemTitleView.setText(item.getName()); itemTitleView.setText(item.getName());
itemStreamCountView.setText(Localization.localizeStreamCountMini( itemStreamCountView.setText(Localization.localizeStreamCountMini(
@ -47,6 +43,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager);
} }
} }

View file

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

View file

@ -882,4 +882,7 @@
<string name="youtube_player_http_403">HTTP error 403 received from server while playing, likely caused by an IP ban or streaming URL deobfuscation issues</string> <string name="youtube_player_http_403">HTTP error 403 received from server while playing, likely caused by an IP ban or streaming URL deobfuscation issues</string>
<string name="sign_in_confirm_not_bot_error">%1$s refused to provide data, asking for a login to confirm the requester is not a bot.\n\nYour IP might have been temporarily banned by %1$s, you can wait some time or switch to a different IP (for example by turning on/off a VPN, or by switching from WiFi to mobile data).</string> <string name="sign_in_confirm_not_bot_error">%1$s refused to provide data, asking for a login to confirm the requester is not a bot.\n\nYour IP might have been temporarily banned by %1$s, you can wait some time or switch to a different IP (for example by turning on/off a VPN, or by switching from WiFi to mobile data).</string>
<string name="unsupported_content_in_country">This content is not available for the currently selected content country.\n\nChange your selection from \"Settings > Content > Default content country\".</string> <string name="unsupported_content_in_country">This content is not available for the currently selected content country.\n\nChange your selection from \"Settings > Content > Default content country\".</string>
<string name="history_detail_line">
{formatted_views} • {last_viewed_date, date, short} • {service_name}
</string>
</resources> </resources>

View file

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