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