diff --git a/src/renderer/helpers/playlists.js b/src/renderer/helpers/playlists.js index 5f53972f4b43..0a458c1b7fe5 100644 --- a/src/renderer/helpers/playlists.js +++ b/src/renderer/helpers/playlists.js @@ -5,6 +5,8 @@ export const SORT_BY_VALUES = { AuthorDescending: 'author_descending', VideoTitleAscending: 'video_title_ascending', VideoTitleDescending: 'video_title_descending', + VideoDurationAscending: 'video_duration_ascending', + VideoDurationDescending: 'video_duration_descending', Custom: 'custom' } @@ -19,7 +21,9 @@ export function getSortedPlaylistItems(playlistItems, sortOrder, locale, reverse sortOrder === SORT_BY_VALUES.VideoTitleAscending || sortOrder === SORT_BY_VALUES.VideoTitleDescending || sortOrder === SORT_BY_VALUES.AuthorAscending || - sortOrder === SORT_BY_VALUES.AuthorDescending + sortOrder === SORT_BY_VALUES.AuthorDescending || + sortOrder === SORT_BY_VALUES.VideoDurationAscending || + sortOrder === SORT_BY_VALUES.VideoDurationDescending ) { collator = new Intl.Collator([locale, 'en']) } @@ -31,6 +35,19 @@ export function getSortedPlaylistItems(playlistItems, sortOrder, locale, reverse }) } +export function videoDurationPresent(v) { + if (typeof v.lengthSeconds !== 'number') { return false } + + return !(isNaN(v.lengthSeconds) || v.lengthSeconds === 0) +} + +function videoDurationWithFallback(v) { + if (videoDurationPresent(v)) { return v.lengthSeconds } + + // Fallback + return 0 +} + function compareTwoPlaylistItems(a, b, sortOrder, collator) { switch (sortOrder) { case SORT_BY_VALUES.DateAddedNewest: @@ -45,6 +62,12 @@ function compareTwoPlaylistItems(a, b, sortOrder, collator) { return collator.compare(a.author, b.author) case SORT_BY_VALUES.AuthorDescending: return collator.compare(b.author, a.author) + case SORT_BY_VALUES.VideoDurationAscending: { + return videoDurationWithFallback(a) - videoDurationWithFallback(b) + } + case SORT_BY_VALUES.VideoDurationDescending: { + return videoDurationWithFallback(b) - videoDurationWithFallback(a) + } default: console.error(`Unknown sortOrder: ${sortOrder}`) return 0 diff --git a/src/renderer/views/Playlist/Playlist.js b/src/renderer/views/Playlist/Playlist.js index 8121a626f09b..fc6ef516984a 100644 --- a/src/renderer/views/Playlist/Playlist.js +++ b/src/renderer/views/Playlist/Playlist.js @@ -20,9 +20,10 @@ import { getIconForSortPreference, setPublishedTimestampsInvidious, showToast, + deepCopy, } from '../../helpers/utils' import { invidiousGetPlaylistInfo, youtubeImageUrlToInvidious } from '../../helpers/api/invidious' -import { getSortedPlaylistItems, SORT_BY_VALUES } from '../../helpers/playlists' +import { getSortedPlaylistItems, videoDurationPresent, SORT_BY_VALUES } from '../../helpers/playlists' import packageDetails from '../../../../package.json' import { MOBILE_WIDTH_THRESHOLD, PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD } from '../../../constants' @@ -74,6 +75,7 @@ export default defineComponent({ getPlaylistInfoDebounce: function() {}, playlistInEditMode: false, forceListView: false, + alreadyShownNotice: false, videoSearchQuery: '', @@ -180,6 +182,14 @@ export default defineComponent({ return this.sortOrder === SORT_BY_VALUES.Custom }, sortedPlaylistItems: function () { + if ( + this.sortOrder === SORT_BY_VALUES.VideoDurationAscending || + this.sortOrder === SORT_BY_VALUES.VideoDurationDescending + ) { + const playlistItems = this.getDurationPlaylistItems() + this.showNoticesSometimes(playlistItems) + return getSortedPlaylistItems(playlistItems, this.sortOrder, this.currentLocale) + } return getSortedPlaylistItems(this.playlistItems, this.sortOrder, this.currentLocale) }, visiblePlaylistItems: function () { @@ -214,6 +224,10 @@ export default defineComponent({ return this.$t('Playlist.Sort By.AuthorAscending') case SORT_BY_VALUES.AuthorDescending: return this.$t('Playlist.Sort By.AuthorDescending') + case SORT_BY_VALUES.VideoDurationAscending: + return this.$t('Playlist.Sort By.VideoDurationAscending') + case SORT_BY_VALUES.VideoDurationDescending: + return this.$t('Playlist.Sort By.VideoDurationDescending') default: console.error(`Unknown sort: ${k}`) return k @@ -420,6 +434,26 @@ export default defineComponent({ showToast(this.$t('User Playlists.SinglePlaylistView.Toast.This playlist does not exist')) }, + getDurationPlaylistItems: function () { + const modifiedPlaylistItems = deepCopy(this.playlistItems) + modifiedPlaylistItems.forEach(video => { + if (!videoDurationPresent(video)) { + const videoHistory = this.$store.getters.getHistoryCacheById[video.videoId] + if (typeof videoHistory !== 'undefined') video.lengthSeconds = videoHistory.lengthSeconds + } + }) + return modifiedPlaylistItems + }, + + showNoticesSometimes: function (playlistItems) { + if (this.alreadyShownNotice) return + const anyVideoMissingDuration = playlistItems.some(v => !videoDurationPresent(v)) + if (anyVideoMissingDuration) { + showToast(this.$t('User Playlists.SinglePlaylistView.Toast.This playlist has a video with a duration error'), 5000) + this.alreadyShownNotice = true + } + }, + getNextPage: function () { switch (this.infoSource) { case 'local': diff --git a/static/locales/en-GB.yaml b/static/locales/en-GB.yaml index 995766999782..b9269c166f5e 100644 --- a/static/locales/en-GB.yaml +++ b/static/locales/en-GB.yaml @@ -174,6 +174,8 @@ User Playlists: This playlist is protected and cannot be removed.: This playlist is protected and cannot be removed. This playlist does not exist: This playlist does not exist + + This playlist has a video with a duration error: This playlist contains at least one video that doesn't have a duration, it will be sorted as if their duration is zero. Playlist {playlistName} has been deleted.: Playlist {playlistName} has been deleted. Video has been removed: Video has been removed @@ -1006,6 +1008,8 @@ Playlist: AuthorAscending: Author (A-Z) AuthorDescending: Author (Z-A) VideoTitleAscending: Title (A-Z) + VideoDurationAscending: Duration (Shortest first) + VideoDurationDescending: Duration (Longest first) Toggle Theatre Mode: 'Toggle Theatre Mode' Change Format: Change Media Formats: 'Change Media Formats' diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index f4366ef53ebd..430a73dece2b 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -236,6 +236,8 @@ User Playlists: Playlist {playlistName} is the new quick bookmark playlist.: Playlist {playlistName} is the new quick bookmark playlist. This playlist does not exist: This playlist does not exist + + This playlist has a video with a duration error: This playlist contains at least one video that doesn't have a duration, it will be sorted as if their duration is zero. AddVideoPrompt: Select a playlist to add your N videos to: 'Select a playlist to add your video to | Select a playlist to add your {videoCount} videos to' N playlists selected: '{playlistCount} Selected' @@ -928,6 +930,8 @@ Playlist: AuthorDescending: Author (Z-A) VideoTitleAscending: Title (A-Z) VideoTitleDescending: Title (Z-A) + VideoDurationAscending: Duration (Shortest first) + VideoDurationDescending: Duration (Longest first) Custom: Custom # On Video Watch Page