Refactor video download logic
This commit is contained in:
		
							parent
							
								
									e9239ec1b4
								
							
						
					
					
						commit
						36dfb7849d
					
				|  | @ -32,6 +32,10 @@ export default Vue.extend({ | |||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     returnIndex: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     dropdownPositionX: { | ||||
|       type: String, | ||||
|       default: 'center' | ||||
|  | @ -47,11 +51,6 @@ export default Vue.extend({ | |||
|     dropdownValues: { | ||||
|       type: Array, | ||||
|       default: () => { return [] } | ||||
|     }, | ||||
|     relatedVideoTitle: { | ||||
|       type: String, | ||||
|       default: () => { return '' }, | ||||
|       require: false | ||||
|     } | ||||
|   }, | ||||
|   data: function () { | ||||
|  | @ -60,18 +59,6 @@ export default Vue.extend({ | |||
|       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 () { | ||||
|     this.id = `iconButton${this._uid}` | ||||
|   }, | ||||
|  | @ -128,16 +115,12 @@ export default Vue.extend({ | |||
|     }, | ||||
| 
 | ||||
|     handleDropdownClick: function (index) { | ||||
|       if (this.relatedVideoTitle !== '') { | ||||
|         this.$emit('click', { | ||||
|           url: this.dropdownValues[index], | ||||
|           title: this.relatedVideoTitle, | ||||
|           extension: this.filesExtensions[index], | ||||
|           folderPath: this.$store.getters.getDownloadFolderPath | ||||
|         }) | ||||
|       if (this.returnIndex) { | ||||
|         this.$emit('click', index) | ||||
|       } else { | ||||
|         this.$emit('click', this.dropdownValues[index]) | ||||
|       } | ||||
| 
 | ||||
|       this.focusOut() | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -25,13 +25,7 @@ export default Vue.extend({ | |||
| 
 | ||||
|       toast.isOpen = false | ||||
|     }, | ||||
|     open: function (message, action, time, translate = false, formatArgs = []) { | ||||
|       if (translate) { | ||||
|         message = this.$t(message) | ||||
|         for (const arg of formatArgs) { | ||||
|           message = message.replace('$', arg) | ||||
|         } | ||||
|       } | ||||
|     open: function (message, action, time) { | ||||
|       const toast = { message: message, action: action || (() => { }), isOpen: false, timeout: null } | ||||
|       toast.timeout = setTimeout(this.close, time || 3000, toast) | ||||
|       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 () { | ||||
|       const videoData = { | ||||
|         videoId: this.id, | ||||
|  |  | |||
|  | @ -99,10 +99,10 @@ | |||
|           class="option" | ||||
|           theme="secondary" | ||||
|           icon="download" | ||||
|           :return-index="true" | ||||
|           :dropdown-names="downloadLinkNames" | ||||
|           :dropdown-values="downloadLinkValues" | ||||
|           :related-video-title="title" | ||||
|           @click="downloadMedia" | ||||
|           @click="handleDownload" | ||||
|         /> | ||||
|         <ft-icon-button | ||||
|           v-if="!isUpcoming" | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import IsEqual from 'lodash.isequal' | ||||
| import FtToastEvents from '../../components/ft-toast/ft-toast-events' | ||||
| import fs from 'fs' | ||||
| import i18n from '../../i18n/index' | ||||
| 
 | ||||
| import { IpcChannels } from '../../../constants' | ||||
| 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 askFolderPath = folderPath === '' | ||||
|     let filePathSelected | ||||
|     const successMsg = 'Downloading has completed' | ||||
|     const locale = i18n._vm.locale | ||||
|     const translations = i18n._vm.messages[locale] | ||||
|     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) { | ||||
|       const resp = await ipcRenderer.invoke( | ||||
|         IpcChannels.SHOW_SAVE_DIALOG, | ||||
|         { defaultPath: `${title}.${extension}` } | ||||
|       ) | ||||
|       filePathSelected = resp.filePath | ||||
|     if (!usingElectron) { | ||||
|       // Add logic here in the future
 | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     if (fallingBackPath !== undefined) { | ||||
|       dispatch('showToast', { | ||||
|         message: 'Download folder does not exist', | ||||
|         translate: true, | ||||
|         formatArgs: [fallingBackPath] | ||||
|       }) | ||||
|     if (folderPath === '') { | ||||
|       const options = { | ||||
|         defaultPath: fileName, | ||||
|         filters: [ | ||||
|           { | ||||
|             extensions: [extension] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|       const response = await dispatch('showSaveDialog', options) | ||||
| 
 | ||||
|       if (response.canceled || response.filePath === '') { | ||||
|         // User canceled the save dialog
 | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       folderPath = response.filePath | ||||
|     } | ||||
| 
 | ||||
|     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 contentLength = response.headers.get('Content-Length') | ||||
| 
 | ||||
|     let receivedLength = 0 | ||||
|     const chunks = [] | ||||
|     // manage frequency notifications to the user
 | ||||
|     const intervalPercentageNotification = 0.2 | ||||
|     let lastPercentageNotification = 0 | ||||
| 
 | ||||
|     while (true) { | ||||
|       const { done, value } = await reader.read() | ||||
|     const handleError = (err) => { | ||||
|       console.log(err) | ||||
|       dispatch('showToast', { | ||||
|         message: errorMessage | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     const processText = async ({ done, value }) => { | ||||
|       if (done) { | ||||
|         break | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       chunks.push(value) | ||||
|       receivedLength += value.length | ||||
|       // Can be used in the future to determine download percentage
 | ||||
|       const percentage = receivedLength / contentLength | ||||
|       if (percentage > (lastPercentageNotification + intervalPercentageNotification)) { | ||||
|         // mechanism kept for an upcoming download page
 | ||||
|         lastPercentageNotification = percentage | ||||
|       } | ||||
|       await reader.read().then(processText).catch(handleError) | ||||
|     } | ||||
| 
 | ||||
|     const chunksAll = new Uint8Array(receivedLength) | ||||
|     let position = 0 | ||||
|     for (const chunk of chunks) { | ||||
|       chunksAll.set(chunk, position) | ||||
|       position += chunk.length | ||||
|     } | ||||
|     await reader.read().then(processText).catch(handleError) | ||||
| 
 | ||||
|     // 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 buffer = await blobFile.arrayBuffer() | ||||
| 
 | ||||
|     if (usingElectron && !askFolderPath) { | ||||
|       fs.writeFile(`${folderPath}/${title}.${extension}`, new DataView(buffer), (err) => { | ||||
|         if (err) { | ||||
|           console.error(err) | ||||
|           dispatch('updateDownloadFolderPath', '') | ||||
|           dispatch('downloadMedia', { url: url, title: title, extension: extension, folderPath: '', fallingBackPath: folderPath }) | ||||
|         } else { | ||||
|           dispatch('showToast', { | ||||
|             message: successMsg, translate: true, formatArgs: [title] | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     } 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
 | ||||
|     } | ||||
|     fs.writeFile(folderPath, new DataView(buffer), (err) => { | ||||
|       if (err) { | ||||
|         console.error(err) | ||||
|         dispatch('showToast', { | ||||
|           message: errorMessage | ||||
|         }) | ||||
|       } else { | ||||
|         dispatch('showToast', { | ||||
|           message: completedMessage | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
|   }, | ||||
| 
 | ||||
|   async getSystemLocale (context) { | ||||
|  | @ -790,9 +770,7 @@ const actions = { | |||
|   }, | ||||
| 
 | ||||
|   showToast (_, payload) { | ||||
|     const formatArgs = 'formatArgs' in payload ? payload.formatArgs : [] | ||||
|     const translate = 'translate' in payload ? payload.translate : false | ||||
|     FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time, translate, formatArgs) | ||||
|     FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time) | ||||
|   }, | ||||
| 
 | ||||
|   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 | ||||
|   loop to continue playing' | ||||
| 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' | ||||
| Starting download: 'Downloading "$" has started' | ||||
| Downloading failed: 'Unable to download "$", return http request status code $' | ||||
| 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. | ||||
| Downloading has completed: '"$" has finished downloading' | ||||
| Starting download: 'Starting download of "$"' | ||||
| Downloading failed: 'There was an issue downloading "$"' | ||||
| 
 | ||||
| Yes: Yes | ||||
| No: No | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue