Skip to content
This repository has been archived by the owner on Jul 6, 2022. It is now read-only.

Version 2, ridding of reliance on web-service #6

Merged
merged 16 commits into from
Dec 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 145 additions & 53 deletions app/containers/app.container.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,180 @@
import React, {Component} from 'react';
import Axios from 'axios';
import LinkInput from '../components/LinkInput';
import ProgressBar from '../components/ProgressBar';

const {ipcRenderer} = window.require('electron');
import React, {Component} from 'react';

import * as path from 'path';

const ffmpeg = window.require('fluent-ffmpeg');
const binaries = window.require('ffmpeg-binaries');
const sanitize = window.require('sanitize-filename');
const {ipcRenderer, remote} = window.require('electron');
const ytdl = window.require('ytdl-core');
const fs = window.require('fs-extra');

class AppContainer extends Component {
constructor(props) {
super(props);
this.state = {
showProgressBar: false,
progress: 0,
progressMessage: 'Sending request..',
progressMessage: '',
userDownloadsFolder: localStorage.getItem('userSelectedFolder') ? localStorage.getItem('userSelectedFolder') : remote.app.getPath('downloads'),
};

this.interval = null;
this.api_key = 'yt-mp3.com';
this.fetchVideo = this.fetchVideo.bind(this);
// Signal from main process to show prompt to change the download to folder.
ipcRenderer.on('promptForChangeDownloadFolder', () => {
// Changing the folder in renderer because we need access to both state and local storage.
this.changeOutputFolder();
});

// This property will be used to control the rate at which the progress bar is updated to prevent UI lag.
this.rateLimitTriggered = false;

this.startDownload = this.startDownload.bind(this);
this.updateProgress = this.updateProgress.bind(this);
this.retryDownload = this.retryDownload.bind(this);
this.downloadFinished = this.downloadFinished.bind(this);
this.changeOutputFolder = this.changeOutputFolder.bind(this);
}

startDownload(id) {
this.setState({
progress: 0,
showProgressBar: true,
progressMessage: 'Sending request...',
});
this.fetchVideo(id);
}
getVideoAsMp4(urlLink, userProvidedPath, title) {
// Tell the user we are starting to get the video.
this.setState({progressMessage: 'Downloading...'});
return new Promise((resolve, reject) => {
let fullPath = path.join(userProvidedPath, `tmp_${title}.mp4`);

fetchVideo(id) {
let _this = this;
Axios.get(`http://www.yt-mp3.com/fetch?v=${id}&apikey=${this.api_key}`).then(function (response) {
if (response.data.status.localeCompare('timeout') === 0) {
_this.setState({progressMessage: 'Waiting on yt-mp3 worker...'});
_this.retryDownload(id);
} else if (response.data.url) {
_this.updateProgress(100);
_this.setState({progressMessage: 'Conversion complete!'});
_this.downloadFinished(response.data.url);
} else {
_this.setState({progressMessage: 'Converting video...'});
_this.updateProgress(response.data.progress);
_this.retryDownload(id);
}
}).catch((e) => {
console.error(e);
alert('There was an error retrieving the video. Please restart the application.');
// Create a reference to the stream of the video being downloaded.
let videoObject = ytdl(urlLink, {filter: 'audioonly'});

videoObject
.on('progress', (chunkLength, downloaded, total) => {
// When the stream emits a progress event, we capture the currently downloaded amount and the total
// to download, we then divided the downloaded by the total and multiply the result to get a float of
// the percent complete, which is then passed through the Math.floor function to drop the decimals.
if (!this.rateLimitTriggered) {
let newVal = Math.floor((downloaded / total) * 100);
this.setState({progress: newVal});

// Set the rate limit trigger to true and set a timeout to set it back to false. This will prevent the UI
// from updating every few milliseconds and creating visual lag.
this.rateLimitTriggered = true;
setTimeout(() => {
this.rateLimitTriggered = false
}, 800);
}
});

// Create write-able stream for the temp file and pipe the video stream into it.
videoObject
.pipe(fs.createWriteStream(fullPath))
.on('finish', () => {
// all of the video stream has finished piping, set the progress bar to 100% and give user pause to see the
// completion of step. Then we return the path to the temp file, the output path, and the desired filename.
this.setState({progress: 100});
setTimeout(() => {
resolve({filePath: fullPath, folderPath: userProvidedPath, fileTitle: `${title}.mp3`});
}, 1000);
});
});
}

// The video has been placed in a queue, retry in 5 seconds
retryDownload(id) {
setTimeout(
() => this.fetchVideo(id),
5000
);
convertMp4ToMp3(paths) {
// Tell the user we are starting to convert the file to mp3.
this.setState({progressMessage: 'Converting...', progress: 0});

return new Promise((resolve, reject) => {
// Reset the rate limiting trigger just encase.
this.rateLimitTriggered = false;

// Pass ffmpeg the temp mp4 file. Set the path where is ffmpeg binary for the platform. Provided desired format.
ffmpeg(paths.filePath)
.setFfmpegPath(binaries.ffmpegPath())
.format('mp3')
.audioBitrate(160)
.on('progress', (progress) => {
// Use same rate limiting as above in function "getVideoAsMp4()" to prevent UI lag.
if (!this.rateLimitTriggered) {
this.setState({progress: Math.floor(progress.percent)});
this.rateLimitTriggered = true;
setTimeout(() => {
this.rateLimitTriggered = false
}, 800);
}
})
.output(fs.createWriteStream(path.join(paths.folderPath, sanitize(paths.fileTitle))))
.on('end', () => {
// After the mp3 is wrote to the disk we set the progress to 99% the last 1% is the removal of the temp file.
this.setState({progress: 99});
resolve();
})
.run();
});
}

downloadFinished(url) {
async startDownload(id) {
// Reset state for each download/conversion
this.setState({
progress: 100
progress: 0,
showProgressBar: true,
progressMessage: '...',
});

// Prompt the user to save the file
ipcRenderer.send('download-file', url);
try {
// Tell the user we are getting the video info, and call the function to do so.
this.setState({progressMessage: 'Fetching video info...'});
let info = await ytdl.getInfo(id);

// Given the id of the video, the path in which to store the output, and the video title
// download the video as an audio only mp4 and write it to a temp file then return
// the full path for the tmp file, the path in which its stored, and the title of the desired output.
let paths = await this.getVideoAsMp4(id, this.state.userDownloadsFolder, info.title);

// Clear the progress bar incrementation
clearInterval(this.interval);
// Pass the returned paths and info into the function which will convert the mp4 tmp file into
// the desired output mp3 file.
await this.convertMp4ToMp3(paths);

// Remove the temp mp4 file.
fs.unlinkSync(paths.filePath);

// Set the bar to 100% and give the OS about one second to get rid of the temp file.
await (() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
this.setState({progress: 100});
resolve();
}, 900);
});
});

// Signal that the download and conversion have completed and we need to tell the user about it and then reset.
this.downloadFinished();
} catch (e) {
console.error(e);
}
}

downloadFinished() {
// Make sure progress bar is at 100% and tell the user we have completed the task successfully.
this.setState({
progress: 100,
progressMessage: 'Conversion successful!'
});

// Reset the progress bar to the LinkInput
setTimeout(() => this.setState({
showProgressBar: false
}), 1000);
}), 2000);
}

updateProgress(progress) {
if (this.state.progress <= 100) {
this.setState({
progress: Math.floor(progress)
});
changeOutputFolder() {
// Create an electron open dialog for selecting folders, this will take into account platform.
let fileSelector = remote.dialog.showOpenDialog({defaultPath: `${this.state.userDownloadsFolder}`, properties: ['openDirectory'], title: 'Select folder to store files.'});

// If a folder was selected and not just closed, set the localStorage value to that path and adjust the state.
if (fileSelector) {
let pathToStore = fileSelector[0];
localStorage.setItem('userSelectedFolder', pathToStore);
this.setState({userDownloadsFolder: pathToStore});
console.log(`New current path ${localStorage.getItem('userSelectedFolder')}`)
}
}

Expand Down
20 changes: 13 additions & 7 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {app, BrowserWindow, ipcMain, Menu} = require('electron');
const {app, BrowserWindow, Menu} = require('electron');
const isDevMode = require('electron-is-dev');
const path = require('path');

Expand Down Expand Up @@ -40,9 +40,18 @@ function createWindow() {
{label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:"},
{label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:"}
]
}];
}, {
label: "Preferences",
submenu: [
{label: "Download Folder", click: () => {
mainWindow.webContents.send('promptForChangeDownloadFolder');
}}
]
}
];

// If developing add dev menu option to menu bar
if (isDevMode)
if (isDevMode) {
template.push({
label: 'Dev Options',
submenu: [
Expand All @@ -53,6 +62,7 @@ function createWindow() {
}
]
});
}

Menu.setApplicationMenu(Menu.buildFromTemplate(template));

Expand All @@ -74,7 +84,3 @@ app.on('activate', function () {
createWindow();
}
});

ipcMain.on('download-file', function (e, url) {
mainWindow.loadURL('http:' + url);
});
Loading