Fix handling of video published date in video lists (#4752)

* Fix handling of video published date in video lists

* Use same date format on the history page as before

* Switch to months at 30 days instead of 32 and correct thresholds

* Add support for formatting as weeks

* According to Invidious everything on the popular tab is a short???
This commit is contained in:
absidue 2024-03-13 07:26:12 +01:00 committed by GitHub
parent dc3f73e5bc
commit 98aded9701
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 172 additions and 153 deletions

View File

@ -7,7 +7,6 @@ import {
formatNumber,
openExternalLink,
showToast,
toLocalePublicationString,
toDistractionFreeTitle,
deepCopy
} from '../../helpers/utils'
@ -98,7 +97,7 @@ export default defineComponent({
duration: '',
description: '',
watchProgress: 0,
publishedText: '',
published: undefined,
isLive: false,
isUpcoming: false,
isPremium: false,
@ -657,60 +656,60 @@ export default defineComponent({
if (typeof premiereDate === 'string') {
premiereDate = new Date(premiereDate)
}
this.publishedText = premiereDate.toLocaleString()
this.uploadedTime = premiereDate.toLocaleString([this.currentLocale, 'en'])
this.published = premiereDate.getTime()
} else if (typeof (this.data.premiereTimestamp) !== 'undefined') {
this.publishedText = new Date(this.data.premiereTimestamp * 1000).toLocaleString()
} else {
this.publishedText = this.data.publishedText
}
this.uploadedTime = new Date(this.data.premiereTimestamp * 1000).toLocaleString([this.currentLocale, 'en'])
this.published = this.data.premiereTimestamp * 1000
} else if (typeof this.data.published === 'number' && !this.isLive) {
this.published = this.data.published
if (this.data.isRSS && this.data.publishedDate != null && !this.isLive) {
const now = new Date()
// Convert from ms to second
// For easier code interpretation the value is made to be positive
// `publishedDate` is sometimes a string, e.g. when switched back from another view
const publishedDate = Date.parse(this.data.publishedDate)
let timeDiffFromNow = ((now - publishedDate) / 1000)
let timeUnit = 'second'
if (this.inHistory) {
this.uploadedTime = new Date(this.data.published).toLocaleDateString([this.currentLocale, 'en'])
} else {
const now = new Date().getTime()
// Convert from ms to second
// For easier code interpretation the value is made to be positive
let timeDiffFromNow = ((now - this.data.published) / 1000)
let timeUnit = 'second'
if (timeDiffFromNow > 60) {
timeDiffFromNow /= 60
timeUnit = 'minute'
if (timeDiffFromNow >= 60) {
timeDiffFromNow /= 60
timeUnit = 'minute'
}
if (timeUnit === 'minute' && timeDiffFromNow >= 60) {
timeDiffFromNow /= 60
timeUnit = 'hour'
}
if (timeUnit === 'hour' && timeDiffFromNow >= 24) {
timeDiffFromNow /= 24
timeUnit = 'day'
}
const timeDiffFromNowDays = timeDiffFromNow
if (timeUnit === 'day' && timeDiffFromNow >= 7) {
timeDiffFromNow /= 7
timeUnit = 'week'
}
// Use 30 days per month, just like calculatePublishedDate
if (timeUnit === 'week' && timeDiffFromNowDays >= 30) {
timeDiffFromNow = timeDiffFromNowDays / 30
timeUnit = 'month'
}
if (timeUnit === 'month' && timeDiffFromNow >= 12) {
timeDiffFromNow /= 12
timeUnit = 'year'
}
// Using `Math.ceil` so that -1.x days ago displayed as 1 day ago
// Notice that the value is turned to negative to be displayed as "ago"
this.uploadedTime = new Intl.RelativeTimeFormat([this.currentLocale, 'en']).format(Math.ceil(-timeDiffFromNow), timeUnit)
}
if (timeUnit === 'minute' && timeDiffFromNow > 60) {
timeDiffFromNow /= 60
timeUnit = 'hour'
}
if (timeUnit === 'hour' && timeDiffFromNow > 24) {
timeDiffFromNow /= 24
timeUnit = 'day'
}
// Diff month might have diff no. of days
// To ensure the display is fine we use 31
if (timeUnit === 'day' && timeDiffFromNow > 31) {
timeDiffFromNow /= 24
timeUnit = 'month'
}
if (timeUnit === 'month' && timeDiffFromNow > 12) {
timeDiffFromNow /= 12
timeUnit = 'year'
}
// Using `Math.ceil` so that -1.x days ago displayed as 1 day ago
// Notice that the value is turned to negative to be displayed as "ago"
this.uploadedTime = new Intl.RelativeTimeFormat(this.currentLocale).format(Math.ceil(-timeDiffFromNow), timeUnit)
} else if (this.publishedText && !this.isLive) {
// produces a string according to the template in the locales string
this.uploadedTime = toLocalePublicationString({
publishText: this.publishedText,
isLive: this.isLive,
isUpcoming: this.isUpcoming,
isRSS: this.data.isRSS
})
}
if (this.hideVideoViews) {
@ -732,14 +731,6 @@ export default defineComponent({
// For UX consistency, no progress reading if writing disabled
this.watchProgress = historyEntry.watchProgress
}
if (historyEntry.published !== '') {
const videoPublished = historyEntry.published
const videoPublishedDate = new Date(videoPublished)
this.publishedText = videoPublishedDate.toLocaleDateString()
} else {
this.publishedText = ''
}
} else {
this.watchProgress = 0
}
@ -751,7 +742,7 @@ export default defineComponent({
title: this.title,
author: this.channelName,
authorId: this.channelId,
published: this.publishedText ? this.publishedText.split(',')[0] : this.publishedText,
published: this.published,
description: this.description,
viewCount: this.viewCount,
lengthSeconds: this.data.lengthSeconds,

View File

@ -137,13 +137,9 @@
{{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
</span>
<span
v-if="uploadedTime !== '' && !isLive && !inHistory"
v-if="uploadedTime !== '' && !isLive"
class="uploadedTime"
> {{ uploadedTime }}</span>
<span
v-if="inHistory"
class="uploadedTime"
> {{ publishedText }}</span>
<span
v-if="isLive && !hideViews"
class="viewCount"

View File

@ -2,10 +2,10 @@ import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { setPublishedTimestampsInvidious, copyToClipboard, showToast } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { getLocalChannelLiveStreams } from '../../helpers/api/local'
import { addPublishedDatesInvidious, addPublishedDatesLocal, parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
export default defineComponent({
name: 'SubscriptionsLive',
@ -198,8 +198,6 @@ export default defineComponent({
}
}
addPublishedDatesLocal(result.videos)
return result
} catch (err) {
console.error(err)
@ -294,7 +292,7 @@ export default defineComponent({
invidiousAPICall(subscriptionsPayload).then((result) => {
const videos = result.videos.filter(e => e.type === 'video')
addPublishedDatesInvidious(videos)
setPublishedTimestampsInvidious(videos)
let name

View File

@ -2,10 +2,10 @@ import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { setPublishedTimestampsInvidious, copyToClipboard, showToast } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { getLocalChannelVideos } from '../../helpers/api/local'
import { addPublishedDatesInvidious, addPublishedDatesLocal, parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
export default defineComponent({
name: 'SubscriptionsVideos',
@ -198,8 +198,6 @@ export default defineComponent({
}
}
addPublishedDatesLocal(result.videos)
return result
} catch (err) {
console.error(err)
@ -291,7 +289,7 @@ export default defineComponent({
}
invidiousAPICall(subscriptionsPayload).then((result) => {
addPublishedDatesInvidious(result.videos)
setPublishedTimestampsInvidious(result.videos)
let name

View File

@ -3,7 +3,7 @@ import { mapMutations } from 'vuex'
import FtLoader from '../ft-loader/ft-loader.vue'
import FtCard from '../ft-card/ft-card.vue'
import FtListVideoNumbered from '../ft-list-video-numbered/ft-list-video-numbered.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { copyToClipboard, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
import {
getLocalPlaylist,
parseLocalPlaylistVideo,
@ -451,6 +451,8 @@ export default defineComponent({
this.playlistTitle = result.title
this.channelName = result.author
this.channelId = result.authorId
setPublishedTimestampsInvidious(result.videos)
this.playlistItems = this.playlistItems.concat(result.videos)
this.isLoading = false

View File

@ -5,6 +5,7 @@ import { join } from 'path'
import { PlayerCache } from './PlayerCache'
import {
CHANNEL_HANDLE_REGEX,
calculatePublishedDate,
escapeHTML,
extractNumberFromString,
getUserDataPath,
@ -681,8 +682,7 @@ export function parseLocalPlaylistVideo(video) {
}
}
let publishedText = null
let publishedText
// normal videos have 3 text runs with the last one containing the published date
// live videos have 2 text runs with the number of people watching
// upcoming either videos don't have any info text or the number of people waiting,
@ -691,13 +691,20 @@ export function parseLocalPlaylistVideo(video) {
publishedText = video_.video_info.runs[2].text
}
const published = calculatePublishedDate(
publishedText,
video_.is_live,
video_.is_upcoming,
video_.upcoming
)
return {
videoId: video_.id,
title: video_.title.text,
author: video_.author.name,
authorId: video_.author.id,
viewCount,
publishedText,
published,
lengthSeconds: isNaN(video_.duration.seconds) ? '' : video_.duration.seconds,
liveNow: video_.is_live,
isUpcoming: video_.is_upcoming,
@ -728,6 +735,20 @@ export function parseLocalListVideo(item) {
} else {
/** @type {import('youtubei.js').YTNodes.Video} */
const video = item
let publishedText
if (!video.published?.isEmpty()) {
publishedText = video.published.text
}
const published = calculatePublishedDate(
publishedText,
video.is_live,
video.is_upcoming || video.is_premiere,
video.upcoming
)
return {
type: 'video',
videoId: video.id,
@ -736,7 +757,7 @@ export function parseLocalListVideo(item) {
authorId: video.author.id,
description: video.description,
viewCount: video.view_count == null ? null : extractNumberFromString(video.view_count.text),
publishedText: (video.published == null || video.published.isEmpty()) ? null : video.published.text,
published,
lengthSeconds: isNaN(video.duration.seconds) ? '' : video.duration.seconds,
liveNow: video.is_live,
isUpcoming: video.is_upcoming || video.is_premiere,
@ -811,6 +832,14 @@ function parseListItem(item) {
* @param {import('youtubei.js').YTNodes.CompactVideo} video
*/
export function parseLocalWatchNextVideo(video) {
let publishedText
if (!video.published?.isEmpty()) {
publishedText = video.published.text
}
const published = calculatePublishedDate(publishedText, video.is_live, video.is_premiere)
return {
type: 'video',
videoId: video.id,
@ -818,7 +847,7 @@ export function parseLocalWatchNextVideo(video) {
author: video.author.name,
authorId: video.author.id,
viewCount: video.view_count == null ? null : extractNumberFromString(video.view_count.text),
publishedText: (video.published == null || video.published.isEmpty()) ? null : video.published.text,
published,
lengthSeconds: isNaN(video.duration.seconds) ? '' : video.duration.seconds,
liveNow: video.is_live,
isUpcoming: video.is_premiere

View File

@ -1,5 +1,4 @@
import store from '../store/index'
import { calculatePublishedDate } from './utils'
/**
* Filtering and sort based on user preferences
@ -57,7 +56,7 @@ export function updateVideoListAfterProcessing(videos) {
}
videoList.sort((a, b) => {
return b.publishedDate - a.publishedDate
return b.published - a.published
})
return videoList
@ -106,57 +105,10 @@ async function parseRSSEntry(entry, channelId, channelName) {
// querySelector doesn't support xml namespaces so we have to use getElementsByTagName here
videoId: entry.getElementsByTagName('yt:videoId')[0].textContent,
title: entry.querySelector('title').textContent,
publishedDate: published,
publishedText: published.toLocaleString(),
published: published.getTime(),
viewCount: entry.getElementsByTagName('media:statistics')[0]?.getAttribute('views') || null,
type: 'video',
lengthSeconds: '0:00',
isRSS: true
}
}
/**
* @param {{
* liveNow: boolean,
* isUpcoming: boolean,
* premiereDate: Date,
* publishedText: string,
* publishedDate: number
* }[]} videos publishedDate is added by this function,
* but adding it to the type definition stops vscode warning that the property doesn't exist
*/
export function addPublishedDatesLocal(videos) {
videos.forEach(video => {
if (video.liveNow) {
video.publishedDate = new Date().getTime()
} else if (video.isUpcoming) {
video.publishedDate = video.premiereDate
} else {
video.publishedDate = calculatePublishedDate(video.publishedText)
}
return video
})
}
/**
* @param {{
* liveNow: boolean,
* isUpcoming: boolean,
* premiereTimestamp: number,
* published: number,
* publishedDate: number
* }[]} videos publishedDate is added by this function,
* but adding it to the type definition stops vscode warning that the property doesn't exist
*/
export function addPublishedDatesInvidious(videos) {
videos.forEach(video => {
if (video.liveNow) {
video.publishedDate = new Date().getTime()
} else if (video.isUpcoming) {
video.publishedDate = new Date(video.premiereTimestamp * 1000)
} else {
video.publishedDate = new Date(video.published * 1000)
}
return video
})
}

View File

@ -12,11 +12,27 @@ export const CHANNEL_HANDLE_REGEX = /^@[\w.-]{3,30}$/
const PUBLISHED_TEXT_REGEX = /(\d+)\s?([a-z]+)/i
/**
* @param {string} publishedText
* @param {boolean} isLive
* @param {boolean} isUpcoming
* @param {Date|undefined} premiereDate
*/
export function calculatePublishedDate(publishedText) {
export function calculatePublishedDate(publishedText, isLive = false, isUpcoming = false, premiereDate = undefined) {
const date = new Date()
if (publishedText === 'Live') {
return publishedText
if (isLive) {
return date.getTime()
} else if (isUpcoming) {
if (premiereDate) {
return premiereDate.getTime()
} else {
// should never happen but just to be sure that we always return a number
return date.getTime()
}
}
if (!publishedText) {
console.error("publishedText is missing but the video isn't live or upcoming")
return undefined
}
const match = publishedText.match(PUBLISHED_TEXT_REGEX)
@ -44,6 +60,26 @@ export function calculatePublishedDate(publishedText) {
return date.getTime() - timeSpan
}
/**
* @param {{
* liveNow: boolean,
* isUpcoming: boolean,
* premiereTimestamp: number,
* published: number
* }[]} videos
*/
export function setPublishedTimestampsInvidious(videos) {
videos.forEach(video => {
if (video.liveNow) {
video.published = new Date().getTime()
} else if (video.isUpcoming) {
video.published = video.premiereTimestamp * 1000
} else if (typeof video.published === 'number') {
video.published *= 1000
}
})
}
export function toLocalePublicationString ({ publishText, isLive = false, isUpcoming = false, isRSS = false }) {
if (isLive) {
return i18n.tc('Global.Counts.Watching Count', 0, { count: 0 })

View File

@ -12,7 +12,13 @@ import FtSubscribeButton from '../../components/ft-subscribe-button/ft-subscribe
import ChannelAbout from '../../components/channel-about/channel-about.vue'
import autolinker from 'autolinker'
import { copyToClipboard, extractNumberFromString, formatNumber, showToast } from '../../helpers/utils'
import {
setPublishedTimestampsInvidious,
copyToClipboard,
extractNumberFromString,
formatNumber,
showToast
} from '../../helpers/utils'
import { isNullOrEmpty } from '../../helpers/strings'
import packageDetails from '../../../../package.json'
import {
@ -33,10 +39,6 @@ import {
parseLocalListVideo,
parseLocalSubscriberCount
} from '../../helpers/api/local'
import {
addPublishedDatesInvidious,
addPublishedDatesLocal
} from '../../helpers/subscriptions'
export default defineComponent({
name: 'Channel',
@ -780,7 +782,6 @@ export default defineComponent({
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && this.latestVideos.length > 0 && this.videoSortBy === 'newest') {
addPublishedDatesLocal(this.latestVideos)
this.updateSubscriptionVideosCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
@ -917,7 +918,6 @@ export default defineComponent({
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && this.latestLive.length > 0 && this.liveSortBy === 'newest') {
addPublishedDatesLocal(this.latestLive)
this.updateSubscriptionLiveCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
@ -992,7 +992,6 @@ export default defineComponent({
thumbnailUrl: youtubeImageUrlToInvidious(thumbnailUrl, this.currentInvidiousInstance)
}
})
this.latestVideos = response.latestVideos
if (response.authorBanners instanceof Array && response.authorBanners.length > 0) {
this.bannerUrl = youtubeImageUrlToInvidious(response.authorBanners[0].url, this.currentInvidiousInstance)
@ -1088,6 +1087,8 @@ export default defineComponent({
}
invidiousAPICall(payload).then((response) => {
setPublishedTimestampsInvidious(response.videos)
if (more) {
this.latestVideos = this.latestVideos.concat(response.videos)
} else {
@ -1097,7 +1098,6 @@ export default defineComponent({
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && !more && this.latestVideos.length > 0 && this.videoSortBy === 'newest') {
addPublishedDatesInvidious(this.latestVideos)
this.updateSubscriptionVideosCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
@ -1143,7 +1143,7 @@ export default defineComponent({
// https://github.com/iv-org/invidious/issues/3801
response.videos.forEach(video => {
video.isUpcoming = false
delete video.publishedText
delete video.published
delete video.premiereTimestamp
})
@ -1199,6 +1199,8 @@ export default defineComponent({
}
invidiousAPICall(payload).then((response) => {
setPublishedTimestampsInvidious(response.videos)
if (more) {
this.latestLive.push(...response.videos)
} else {
@ -1208,7 +1210,6 @@ export default defineComponent({
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && !more && this.latestLive.length > 0 && this.liveSortBy === 'newest') {
addPublishedDatesInvidious(this.latestLive)
this.updateSubscriptionLiveCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page

View File

@ -5,7 +5,7 @@ import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import packageDetails from '../../../../package.json'
import { getHashtagLocal, parseLocalListVideo } from '../../helpers/api/local'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { copyToClipboard, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
import { isNullOrEmpty } from '../../helpers/strings'
import { getHashtagInvidious } from '../../helpers/api/invidious'
@ -73,6 +73,7 @@ export default defineComponent({
getInvidiousHashtag: async function(hashtag, page) {
try {
const videos = await getHashtagInvidious(hashtag, page)
setPublishedTimestampsInvidious(videos)
this.hashtag = '#' + hashtag
this.isLoading = false
this.apiUsed = 'invidious'

View File

@ -12,7 +12,7 @@ import {
getLocalPlaylistContinuation,
parseLocalPlaylistVideo,
} from '../../helpers/api/local'
import { extractNumberFromString, showToast } from '../../helpers/utils'
import { extractNumberFromString, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
import { invidiousGetPlaylistInfo, youtubeImageUrlToInvidious } from '../../helpers/api/invidious'
export default defineComponent({
@ -284,6 +284,8 @@ export default defineComponent({
const dateString = new Date(result.updated * 1000)
this.lastUpdated = dateString.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' })
setPublishedTimestampsInvidious(result.videos)
this.playlistItems = result.videos
this.isLoading = false

View File

@ -5,7 +5,7 @@ import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { copyToClipboard, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
export default defineComponent({
name: 'Popular',
@ -60,11 +60,15 @@ export default defineComponent({
return
}
this.shownResults = result.filter((item) => {
const items = result.filter((item) => {
return item.type === 'video' || item.type === 'shortVideo' || item.type === 'channel' || item.type === 'playlist'
})
setPublishedTimestampsInvidious(items.filter(item => item.type === 'video' || item.type === 'shortVideo'))
this.shownResults = items
this.isLoading = false
this.$store.commit('setPopularCache', this.shownResults)
this.$store.commit('setPopularCache', items)
},
/**

View File

@ -2,7 +2,12 @@ import { defineComponent } from 'vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import { copyToClipboard, searchFiltersMatch, showToast } from '../../helpers/utils'
import {
copyToClipboard,
searchFiltersMatch,
setPublishedTimestampsInvidious,
showToast
} from '../../helpers/utils'
import { getLocalSearchContinuation, getLocalSearchResults } from '../../helpers/api/local'
import { invidiousAPICall } from '../../helpers/api/invidious'
@ -214,6 +219,8 @@ export default defineComponent({
return item.type === 'video' || item.type === 'channel' || item.type === 'playlist' || item.type === 'hashtag'
})
setPublishedTimestampsInvidious(returnData.filter(item => item.type === 'video'))
if (this.searchPage !== 1) {
this.shownResults = this.shownResults.concat(returnData)
} else {

View File

@ -6,7 +6,7 @@ import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { copyToClipboard, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
import { getLocalTrending } from '../../helpers/api/local'
import { invidiousAPICall } from '../../helpers/api/invidious'
@ -146,6 +146,8 @@ export default defineComponent({
return item.type === 'video' || item.type === 'channel' || item.type === 'playlist'
})
setPublishedTimestampsInvidious(returnData.filter(item => item.type === 'video'))
this.shownResults = returnData
this.isLoading = false
this.$store.commit('setTrendingCache', { value: returnData, page: this.currentTab })