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

Commit

Permalink
Merge pull request #6 from IT-MikeS/master
Browse files Browse the repository at this point in the history
Version 2, ridding of reliance on web-service
  • Loading branch information
leerob committed Dec 12, 2017
2 parents 11bf033 + 2f80178 commit a866db1
Show file tree
Hide file tree
Showing 7 changed files with 16,376 additions and 18,049 deletions.
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

0 comments on commit a866db1

Please sign in to comment.