Refactor video download logic
This commit is contained in:
parent
e9239ec1b4
commit
36dfb7849d
|
@ -32,6 +32,10 @@ export default Vue.extend({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
returnIndex: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
dropdownPositionX: {
|
dropdownPositionX: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'center'
|
default: 'center'
|
||||||
|
@ -47,11 +51,6 @@ export default Vue.extend({
|
||||||
dropdownValues: {
|
dropdownValues: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => { return [] }
|
default: () => { return [] }
|
||||||
},
|
|
||||||
relatedVideoTitle: {
|
|
||||||
type: String,
|
|
||||||
default: () => { return '' },
|
|
||||||
require: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -60,18 +59,6 @@ export default Vue.extend({
|
||||||
id: ''
|
id: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
filesExtensions: function() {
|
|
||||||
const regex = /\/(\w*)/i
|
|
||||||
return this.dropdownNames.slice().map((el) => {
|
|
||||||
const group = el.match(regex)
|
|
||||||
if (group === null || group.length === 0) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return group[1]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.id = `iconButton${this._uid}`
|
this.id = `iconButton${this._uid}`
|
||||||
},
|
},
|
||||||
|
@ -128,16 +115,12 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDropdownClick: function (index) {
|
handleDropdownClick: function (index) {
|
||||||
if (this.relatedVideoTitle !== '') {
|
if (this.returnIndex) {
|
||||||
this.$emit('click', {
|
this.$emit('click', index)
|
||||||
url: this.dropdownValues[index],
|
|
||||||
title: this.relatedVideoTitle,
|
|
||||||
extension: this.filesExtensions[index],
|
|
||||||
folderPath: this.$store.getters.getDownloadFolderPath
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
this.$emit('click', this.dropdownValues[index])
|
this.$emit('click', this.dropdownValues[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusOut()
|
this.focusOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
toast.isOpen = false
|
toast.isOpen = false
|
||||||
},
|
},
|
||||||
open: function (message, action, time, translate = false, formatArgs = []) {
|
open: function (message, action, time) {
|
||||||
if (translate) {
|
|
||||||
message = this.$t(message)
|
|
||||||
for (const arg of formatArgs) {
|
|
||||||
message = message.replace('$', arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const toast = { message: message, action: action || (() => { }), isOpen: false, timeout: null }
|
const toast = { message: message, action: action || (() => { }), isOpen: false, timeout: null }
|
||||||
toast.timeout = setTimeout(this.close, time || 3000, toast)
|
toast.timeout = setTimeout(this.close, time || 3000, toast)
|
||||||
setImmediate(() => { toast.isOpen = true })
|
setImmediate(() => { toast.isOpen = true })
|
||||||
|
|
|
@ -408,6 +408,27 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleDownload: function (index) {
|
||||||
|
const url = this.downloadLinkValues[index]
|
||||||
|
const linkName = this.downloadLinkNames[index]
|
||||||
|
const extension = this.grabExtensionFromUrl(linkName)
|
||||||
|
|
||||||
|
this.downloadMedia({
|
||||||
|
url: url,
|
||||||
|
title: this.title,
|
||||||
|
extension: extension
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
grabExtensionFromUrl: function (url) {
|
||||||
|
const regex = /\/(\w*)/i
|
||||||
|
const group = url.match(regex)
|
||||||
|
if (group.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return group[1]
|
||||||
|
},
|
||||||
|
|
||||||
addToPlaylist: function () {
|
addToPlaylist: function () {
|
||||||
const videoData = {
|
const videoData = {
|
||||||
videoId: this.id,
|
videoId: this.id,
|
||||||
|
|
|
@ -99,10 +99,10 @@
|
||||||
class="option"
|
class="option"
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
icon="download"
|
icon="download"
|
||||||
|
:return-index="true"
|
||||||
:dropdown-names="downloadLinkNames"
|
:dropdown-names="downloadLinkNames"
|
||||||
:dropdown-values="downloadLinkValues"
|
:dropdown-values="downloadLinkValues"
|
||||||
:related-video-title="title"
|
@click="handleDownload"
|
||||||
@click="downloadMedia"
|
|
||||||
/>
|
/>
|
||||||
<ft-icon-button
|
<ft-icon-button
|
||||||
v-if="!isUpcoming"
|
v-if="!isUpcoming"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import IsEqual from 'lodash.isequal'
|
import IsEqual from 'lodash.isequal'
|
||||||
import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import i18n from '../../i18n/index'
|
||||||
|
|
||||||
import { IpcChannels } from '../../../constants'
|
import { IpcChannels } from '../../../constants'
|
||||||
import { ipcRenderer } from 'electron'
|
import { ipcRenderer } from 'electron'
|
||||||
|
@ -176,113 +177,92 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async downloadMedia({ rootState, dispatch }, { url, title, extension, folderPath, fallingBackPath }) {
|
async downloadMedia({ rootState, dispatch }, { url, title, extension, fallingBackPath }) {
|
||||||
|
const fileName = `${title}.${extension}`
|
||||||
const usingElectron = rootState.settings.usingElectron
|
const usingElectron = rootState.settings.usingElectron
|
||||||
const askFolderPath = folderPath === ''
|
const locale = i18n._vm.locale
|
||||||
let filePathSelected
|
const translations = i18n._vm.messages[locale]
|
||||||
const successMsg = 'Downloading has completed'
|
const startMessage = translations['Starting download'].replace('$', title)
|
||||||
|
const completedMessage = translations['Downloading has completed'].replace('$', title)
|
||||||
|
const errorMessage = translations['Downloading failed'].replace('$', title)
|
||||||
|
let folderPath = rootState.settings.downloadFolderPath
|
||||||
|
|
||||||
if (askFolderPath && usingElectron) {
|
if (!usingElectron) {
|
||||||
const resp = await ipcRenderer.invoke(
|
// Add logic here in the future
|
||||||
IpcChannels.SHOW_SAVE_DIALOG,
|
return
|
||||||
{ defaultPath: `${title}.${extension}` }
|
|
||||||
)
|
|
||||||
filePathSelected = resp.filePath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fallingBackPath !== undefined) {
|
if (folderPath === '') {
|
||||||
dispatch('showToast', {
|
const options = {
|
||||||
message: 'Download folder does not exist',
|
defaultPath: fileName,
|
||||||
translate: true,
|
filters: [
|
||||||
formatArgs: [fallingBackPath]
|
{
|
||||||
})
|
extensions: [extension]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const response = await dispatch('showSaveDialog', options)
|
||||||
|
|
||||||
|
if (response.canceled || response.filePath === '') {
|
||||||
|
// User canceled the save dialog
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
folderPath = response.filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch('showToast', {
|
dispatch('showToast', {
|
||||||
message: 'Starting download', translate: true, formatArgs: [title]
|
message: startMessage
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(url).catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: errorMessage
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await fetch(url)
|
|
||||||
// mechanism to show the download progress reference https://javascript.info/fetch-progress
|
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
|
|
||||||
const contentLength = response.headers.get('Content-Length')
|
const contentLength = response.headers.get('Content-Length')
|
||||||
|
|
||||||
let receivedLength = 0
|
let receivedLength = 0
|
||||||
const chunks = []
|
const chunks = []
|
||||||
// manage frequency notifications to the user
|
|
||||||
const intervalPercentageNotification = 0.2
|
|
||||||
let lastPercentageNotification = 0
|
|
||||||
|
|
||||||
while (true) {
|
const handleError = (err) => {
|
||||||
const { done, value } = await reader.read()
|
console.log(err)
|
||||||
|
dispatch('showToast', {
|
||||||
|
message: errorMessage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const processText = async ({ done, value }) => {
|
||||||
if (done) {
|
if (done) {
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks.push(value)
|
chunks.push(value)
|
||||||
receivedLength += value.length
|
receivedLength += value.length
|
||||||
|
// Can be used in the future to determine download percentage
|
||||||
const percentage = receivedLength / contentLength
|
const percentage = receivedLength / contentLength
|
||||||
if (percentage > (lastPercentageNotification + intervalPercentageNotification)) {
|
await reader.read().then(processText).catch(handleError)
|
||||||
// mechanism kept for an upcoming download page
|
|
||||||
lastPercentageNotification = percentage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunksAll = new Uint8Array(receivedLength)
|
await reader.read().then(processText).catch(handleError)
|
||||||
let position = 0
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
chunksAll.set(chunk, position)
|
|
||||||
position += chunk.length
|
|
||||||
}
|
|
||||||
|
|
||||||
// write the file into the hardrive
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`"Unable to download ${title}, return status code ${response.status}`)
|
|
||||||
dispatch('showToast', {
|
|
||||||
message: 'Downloading failed', translate: true, formatArgs: [title, response.status]
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const blobFile = new Blob(chunks)
|
const blobFile = new Blob(chunks)
|
||||||
const buffer = await blobFile.arrayBuffer()
|
const buffer = await blobFile.arrayBuffer()
|
||||||
|
|
||||||
if (usingElectron && !askFolderPath) {
|
fs.writeFile(folderPath, new DataView(buffer), (err) => {
|
||||||
fs.writeFile(`${folderPath}/${title}.${extension}`, new DataView(buffer), (err) => {
|
if (err) {
|
||||||
if (err) {
|
console.error(err)
|
||||||
console.error(err)
|
dispatch('showToast', {
|
||||||
dispatch('updateDownloadFolderPath', '')
|
message: errorMessage
|
||||||
dispatch('downloadMedia', { url: url, title: title, extension: extension, folderPath: '', fallingBackPath: folderPath })
|
})
|
||||||
} else {
|
} else {
|
||||||
dispatch('showToast', {
|
dispatch('showToast', {
|
||||||
message: successMsg, translate: true, formatArgs: [title]
|
message: completedMessage
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (usingElectron) {
|
|
||||||
fs.writeFile(filePathSelected, new DataView(buffer), (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err)
|
|
||||||
if (filePathSelected === '') {
|
|
||||||
dispatch('showToast', {
|
|
||||||
message: 'Downloading canceled',
|
|
||||||
translate: true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
dispatch('showToast', {
|
|
||||||
message: err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispatch('showToast', {
|
|
||||||
message: successMsg, translate: true, formatArgs: [title]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Web placeholder
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getSystemLocale (context) {
|
async getSystemLocale (context) {
|
||||||
|
@ -790,9 +770,7 @@ const actions = {
|
||||||
},
|
},
|
||||||
|
|
||||||
showToast (_, payload) {
|
showToast (_, payload) {
|
||||||
const formatArgs = 'formatArgs' in payload ? payload.formatArgs : []
|
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time)
|
||||||
const translate = 'translate' in payload ? payload.translate : false
|
|
||||||
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time, translate, formatArgs)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showExternalPlayerUnsupportedActionToast: function ({ dispatch }, payload) {
|
showExternalPlayerUnsupportedActionToast: function ({ dispatch }, payload) {
|
||||||
|
|
|
@ -713,11 +713,9 @@ Default Invidious instance has been cleared: Default Invidious instance has been
|
||||||
'The playlist has ended. Enable loop to continue playing': 'The playlist has ended. Enable
|
'The playlist has ended. Enable loop to continue playing': 'The playlist has ended. Enable
|
||||||
loop to continue playing'
|
loop to continue playing'
|
||||||
External link opening has been disabled in the general settings: 'External link opening has been disabled in the general settings'
|
External link opening has been disabled in the general settings: 'External link opening has been disabled in the general settings'
|
||||||
Downloading has completed: 'Downloading "$" has completed'
|
Downloading has completed: '"$" has finished downloading'
|
||||||
Starting download: 'Downloading "$" has started'
|
Starting download: 'Starting download of "$"'
|
||||||
Downloading failed: 'Unable to download "$", return http request status code $'
|
Downloading failed: 'There was an issue downloading "$"'
|
||||||
Downloading canceled: The dowload is canceled by the user
|
|
||||||
Download folder does not exist: The download directory "$" doesn't exist. Falling back to "ask folder" mode.
|
|
||||||
|
|
||||||
Yes: Yes
|
Yes: Yes
|
||||||
No: No
|
No: No
|
||||||
|
|
Loading…
Reference in New Issue