FreeTube/src/renderer/store/modules/playlists.js

460 lines
14 KiB
JavaScript

import { DBPlaylistHandlers } from '../../../datastores/handlers/index'
function generateRandomPlaylistId() {
return `ft-playlist--${generateRandomUniqueId()}`
}
function generateRandomPlaylistName() {
return `Playlist ${new Date().toISOString()}-${Math.floor(Math.random() * 10000)}`
}
function generateRandomUniqueId() {
// To avoid importing `crypto` from NodeJS
return crypto.randomUUID ? crypto.randomUUID() : `id-${Date.now()}-${Math.floor(Math.random() * 10000)}`
}
const state = {
// Playlist loading takes time on app load (new windows)
// This is necessary to let components to know when to start data loading
// which depends on playlist data being ready
playlistsReady: false,
playlists: [],
defaultPlaylists: [
{
playlistName: 'Favorites',
protected: false,
description: 'Your favorite videos',
videos: [],
_id: 'favorites',
},
{
playlistName: 'Watch Later',
protected: false,
description: 'Videos to watch later',
videos: [],
_id: 'watchLater',
},
],
}
const getters = {
getPlaylistsReady: () => state.playlistsReady,
getAllPlaylists: () => state.playlists,
getPlaylist: (state) => (playlistId) => {
return state.playlists.find(playlist => playlist._id === playlistId)
},
}
const actions = {
async addPlaylist({ commit }, payload) {
// In case internal id is forgotten, generate one (instead of relying on caller and have a chance to cause data corruption)
if (payload._id == null) {
// {Time now in unix time}-{0-9999}
payload._id = generateRandomPlaylistId()
}
// Ensure playlist name trimmed
if (typeof payload.playlistName === 'string') {
payload.playlistName = payload.playlistName.trim()
}
// Ensure playlist description trimmed
if (typeof payload.description === 'string') {
payload.description = payload.description.trim()
}
payload.createdAt = Date.now()
payload.lastUpdatedAt = Date.now()
// Ensure all videos has required attributes
const currentTime = new Date().getTime()
if (Array.isArray(payload.videos)) {
payload.videos.forEach(videoData => {
if (videoData.timeAdded == null) {
videoData.timeAdded = currentTime
}
if (videoData.playlistItemId == null) {
videoData.playlistItemId = generateRandomUniqueId()
}
})
}
try {
await DBPlaylistHandlers.create([payload])
commit('addPlaylist', payload)
} catch (errMessage) {
console.error(errMessage)
}
},
async addPlaylists({ commit }, payload) {
try {
await DBPlaylistHandlers.create(payload)
commit('addPlaylists', payload)
} catch (errMessage) {
console.error(errMessage)
}
},
async updatePlaylist({ commit }, playlist) {
// Ensure playlist name trimmed
if (typeof playlist.playlistName === 'string') {
playlist.playlistName = playlist.playlistName.trim()
}
// Ensure playlist description trimmed
if (typeof playlist.description === 'string') {
playlist.description = playlist.description.trim()
}
// Caller no need to assign last updated time
playlist.lastUpdatedAt = Date.now()
try {
await DBPlaylistHandlers.upsert(playlist)
commit('upsertPlaylistToList', playlist)
} catch (errMessage) {
console.error(errMessage)
}
},
async updatePlaylistLastPlayedAt({ commit }, playlist) {
// This action does NOT update `lastUpdatedAt` on purpose
// Only `lastPlayedAt` should be updated
playlist.lastPlayedAt = Date.now()
try {
await DBPlaylistHandlers.upsert(playlist)
commit('upsertPlaylistToList', playlist)
} catch (errMessage) {
console.error(errMessage)
}
},
async addVideo({ commit }, payload) {
try {
const { _id, videoData } = payload
if (videoData.timeAdded == null) {
videoData.timeAdded = new Date().getTime()
}
if (videoData.playlistItemId == null) {
videoData.playlistItemId = generateRandomUniqueId()
}
// For backward compatibility
if (videoData.type == null) {
videoData.type = 'video'
}
await DBPlaylistHandlers.upsertVideoByPlaylistId(_id, videoData)
commit('addVideo', payload)
} catch (errMessage) {
console.error(errMessage)
}
},
async addVideos({ commit }, payload) {
// Assumes videos are added NOT from export
// Since this action will ensure uniqueness of `playlistItemId` of added video entries
try {
const { _id, videos } = payload
const currentTime = new Date().getTime()
const newVideoObjects = videos.map((video) => {
// Create a new object to prevent changing existing values outside
const videoData = Object.assign({}, video)
if (videoData.timeAdded == null) {
videoData.timeAdded = currentTime
}
videoData.playlistItemId = generateRandomUniqueId()
// For backward compatibility
if (videoData.type == null) {
videoData.type = 'video'
}
// Undesired attributes, even with `null` values
[
'description',
'viewCount',
].forEach(attrName => {
if (typeof videoData[attrName] !== 'undefined') {
delete videoData[attrName]
}
})
return videoData
})
await DBPlaylistHandlers.upsertVideosByPlaylistId(_id, newVideoObjects)
commit('addVideos', { _id, videos: newVideoObjects })
} catch (errMessage) {
console.error(errMessage)
}
},
async grabAllPlaylists({ commit, dispatch, state }) {
try {
const payload = (await DBPlaylistHandlers.find()).filter((e) => e != null)
if (payload.length === 0) {
// Not using `addPlaylists` to ensure required attributes with dynamic values added
state.defaultPlaylists.forEach(playlist => {
dispatch('addPlaylist', playlist)
})
} else {
const dateNow = Date.now()
const currentTime = new Date().getTime()
payload.forEach((playlist) => {
let anythingUpdated = false
// Assign generated playlist ID in case DB data corrupted
if (playlist._id == null) {
// {Time now in unix time}-{0-9999}
playlist._id = generateRandomPlaylistId()
anythingUpdated = true
}
// Ensure all videos has `playlistName` property
if (playlist.playlistName == null) {
// Time now in unix time, in ms
playlist.playlistName = generateRandomPlaylistName()
anythingUpdated = true
}
// Assign current time as created time in case DB data corrupted
if (playlist.createdAt == null) {
// Time now in unix time, in ms
playlist.createdAt = dateNow
anythingUpdated = true
}
// Assign current time as last updated time in case DB data corrupted
if (playlist.lastUpdatedAt == null) {
// Time now in unix time, in ms
playlist.lastUpdatedAt = dateNow
anythingUpdated = true
}
playlist.videos.forEach((v) => {
// Ensure all videos has `timeAdded` property
if (v.timeAdded == null) {
v.timeAdded = currentTime
anythingUpdated = true
}
// Ensure all videos has `playlistItemId` property
if (v.playlistItemId == null) {
v.playlistItemId = generateRandomUniqueId()
anythingUpdated = true
}
// For backward compatibility
if (v.type == null) {
v.type = 'video'
anythingUpdated = true
}
// Undesired attributes, even with `null` values
[
'description',
'viewCount',
].forEach(attrName => {
if (typeof v[attrName] !== 'undefined') {
delete v[attrName]
anythingUpdated = true
}
})
})
// Save updated playlist object
if (anythingUpdated) {
DBPlaylistHandlers.upsert(playlist)
}
})
const favoritesPlaylist = payload.find((playlist) => {
return playlist.playlistName === 'Favorites' || playlist._id === 'favorites'
})
const watchLaterPlaylist = payload.find((playlist) => {
return playlist.playlistName === 'Watch Later' || playlist._id === 'watchLater'
})
if (favoritesPlaylist != null) {
const defaultFavoritesPlaylist = state.defaultPlaylists.find((e) => e._id === 'favorites')
// Update existing matching playlist only if it exists
if (favoritesPlaylist._id !== defaultFavoritesPlaylist._id || favoritesPlaylist.protected !== defaultFavoritesPlaylist.protected) {
const oldId = favoritesPlaylist._id
favoritesPlaylist._id = defaultFavoritesPlaylist._id
favoritesPlaylist.protected = defaultFavoritesPlaylist.protected
if (oldId === defaultFavoritesPlaylist._id) {
// Update playlist if ID already the same
DBPlaylistHandlers.upsert(favoritesPlaylist)
} else {
dispatch('removePlaylist', oldId)
// DO NOT use dispatch('addPlaylist', ...)
// Which causes duplicate displayed playlist in window (But DB is fine)
// Due to the object is already in `payload`
DBPlaylistHandlers.create(favoritesPlaylist)
}
}
}
if (watchLaterPlaylist != null) {
const defaultWatchLaterPlaylist = state.defaultPlaylists.find((e) => e._id === 'watchLater')
// Update existing matching playlist only if it exists
if (watchLaterPlaylist._id !== defaultWatchLaterPlaylist._id || watchLaterPlaylist.protected !== defaultWatchLaterPlaylist.protected) {
const oldId = watchLaterPlaylist._id
watchLaterPlaylist._id = defaultWatchLaterPlaylist._id
watchLaterPlaylist.protected = defaultWatchLaterPlaylist.protected
if (oldId === defaultWatchLaterPlaylist._id) {
// Update playlist if ID already the same
DBPlaylistHandlers.upsert(watchLaterPlaylist)
} else {
dispatch('removePlaylist', oldId)
// DO NOT use dispatch('addPlaylist', ...)
// Which causes duplicate displayed playlist in window (But DB is fine)
// Due to the object is already in `payload`
DBPlaylistHandlers.create(watchLaterPlaylist)
}
}
}
commit('setAllPlaylists', payload)
}
commit('setPlaylistsReady', true)
} catch (errMessage) {
console.error(errMessage)
}
},
async removeAllPlaylists({ commit }) {
try {
await DBPlaylistHandlers.deleteAll()
commit('removeAllPlaylists')
} catch (errMessage) {
console.error(errMessage)
}
},
async removeAllVideos({ commit }, _id) {
try {
await DBPlaylistHandlers.deleteAllVideosByPlaylistId(_id)
commit('removeAllVideos', _id)
} catch (errMessage) {
console.error(errMessage)
}
},
async removePlaylist({ commit }, playlistId) {
try {
await DBPlaylistHandlers.delete(playlistId)
commit('removePlaylist', playlistId)
} catch (errMessage) {
console.error(errMessage)
}
},
async removePlaylists({ commit }, playlistIds) {
try {
await DBPlaylistHandlers.deleteMultiple(playlistIds)
commit('removePlaylists', playlistIds)
} catch (errMessage) {
console.error(errMessage)
}
},
async removeVideo({ commit }, payload) {
try {
const { _id, videoId, playlistItemId } = payload
await DBPlaylistHandlers.deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId })
commit('removeVideo', payload)
} catch (errMessage) {
console.error(errMessage)
}
},
async removeVideos({ commit }, payload) {
try {
const { _id, videoIds } = payload
await DBPlaylistHandlers.deleteVideoIdsByPlaylistId(_id, videoIds)
commit('removeVideos', payload)
} catch (errMessage) {
console.error(errMessage)
}
},
}
const mutations = {
addPlaylist(state, payload) {
state.playlists.push(payload)
},
addPlaylists(state, payload) {
state.playlists = state.playlists.concat(payload)
},
upsertPlaylistToList(state, updatedPlaylist) {
const i = state.playlists.findIndex((p) => {
return p._id === updatedPlaylist._id
})
if (i === -1) {
state.playlists.push(updatedPlaylist)
} else {
const foundPlaylist = state.playlists[i]
state.playlists.splice(i, 1, Object.assign(foundPlaylist, updatedPlaylist))
}
},
addVideo(state, payload) {
const playlist = state.playlists.find(playlist => playlist._id === payload._id)
if (playlist) {
playlist.videos.push(payload.videoData)
}
},
addVideos(state, payload) {
const playlist = state.playlists.find(playlist => playlist._id === payload._id)
if (playlist) {
playlist.videos = [].concat(playlist.videos, payload.videos)
}
},
removeAllPlaylists(state) {
state.playlists = []
},
removeAllVideos(state, playlistId) {
const playlist = state.playlists.find(playlist => playlist._id === playlistId)
if (playlist) {
playlist.videos = []
}
},
removeVideo(state, { _id, videoId, playlistItemId }) {
const playlist = state.playlists.find(playlist => playlist._id === _id)
if (playlist) {
if (playlistItemId != null) {
playlist.videos = playlist.videos.filter(video => video.playlistItemId !== playlistItemId)
} else if (videoId != null) {
playlist.videos = playlist.videos.filter(video => video.videoId !== videoId)
}
}
},
removeVideos(state, { _id, videoId }) {
const playlist = state.playlists.find(playlist => playlist._id === _id)
if (playlist) {
playlist.videos = playlist.videos.filter(video => videoId.indexOf(video) === -1)
}
},
removePlaylist(state, playlistId) {
state.playlists = state.playlists.filter(playlist => playlist._id !== playlistId || playlist.protected)
},
setAllPlaylists(state, payload) {
state.playlists = payload
},
setPlaylistsReady(state, payload) {
state.playlistsReady = payload
},
}
export default {
state,
getters,
actions,
mutations
}