diff --git a/backend/app.js b/backend/app.js index 33e1647..745016d 100644 --- a/backend/app.js +++ b/backend/app.js @@ -11,6 +11,7 @@ var archiver = require('archiver'); var unzipper = require('unzipper'); var mergeFiles = require('merge-files'); const low = require('lowdb') +var ProgressBar = require('progress'); var md5 = require('md5'); const NodeID3 = require('node-id3') const downloader = require('youtube-dl/lib/downloader') @@ -61,12 +62,23 @@ var archivePath = path.join(__dirname, 'appdata', 'archives'); // other needed values var options = null; // encryption options var url_domain = null; +var updaterStatus = null; // check if debug mode let debugMode = process.env.YTDL_MODE === 'debug'; if (debugMode) console.log('YTDL-Material in debug mode!'); +// check if just updated +const just_restarted = fs.existsSync('restart.json'); +if (just_restarted) { + updaterStatus = { + updating: false, + details: 'Update complete! You are now on ' + CONSTS['CURRENT_VERSION'] + } + fs.unlinkSync('restart.json'); +} + // updates & starts youtubedl startYoutubeDL(); @@ -153,42 +165,63 @@ async function restartServer() { fs.writeFileSync('restart.json', 'internal use only'); } -async function updateServer() { - const new_version_available = await isNewVersionAvailable(); - if (!new_version_available) { - console.log('ERROR: Failed to update - no update is available.'); - return false; +async function updateServer(tag) { + // no tag provided means update to the latest version + if (!tag) { + const new_version_available = await isNewVersionAvailable(); + if (!new_version_available) { + console.log('ERROR: Failed to update - no update is available.'); + return false; + } } + return new Promise(async resolve => { // backup current dir + updaterStatus = { + updating: true, + 'details': 'Backing up key server files...' + } let backup_succeeded = await backupServerLite(); if (!backup_succeeded) { resolve(false); return false; } + updaterStatus = { + updating: true, + 'details': 'Downloading requested release...' + } // grab new package.json and public folder - await downloadUpdateFiles(); + // await downloadReleaseFiles(tag); + updaterStatus = { + updating: true, + 'details': 'Installing new dependencies...' + } // run npm install await installDependencies(); + updaterStatus = { + updating: true, + 'details': 'Update complete! Restarting server...' + } restartServer(); + }, err => { + updaterStatus = { + updating: false, + error: true, + 'details': 'Update failed. Check error logs for more info.' + } }); } -async function downloadUpdateFiles() { - let tag = await getLatestVersion(); +async function downloadReleaseFiles(tag) { + tag = tag ? tag : await getLatestVersion(); return new Promise(async resolve => { console.log('Downloading new files...') - var options = { - owner: 'tzahi12345', - repo: 'YoutubeDL-Material', - branch: tag - }; // downloads the latest release zip file - await downloadLatestRelease(tag); + await downloadReleaseZip(tag); // deletes contents of public dir fs.removeSync(path.join(__dirname, 'public')); @@ -199,7 +232,7 @@ async function downloadUpdateFiles() { console.log(`Installing update ${tag}...`) // downloads new package.json and adds new public dir files from the downloaded zip - fs.createReadStream(path.join(__dirname, 'youtubedl-material-latest-release.zip')).pipe(unzipper.Parse()) + fs.createReadStream(path.join(__dirname, `youtubedl-material-latest-release-${tag}.zip`)).pipe(unzipper.Parse()) .on('entry', function (entry) { var fileName = entry.path; var type = entry.type; // 'Directory' or 'File' @@ -229,17 +262,46 @@ async function downloadUpdateFiles() { }); } -async function downloadLatestRelease(tag) { +// helper function to download file using fetch +async function fetchFile(url, path, file_label) { + var len = null; + const res = await fetch(url); + + len = parseInt(res.headers.get("Content-Length"), 10); + + var bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, { + complete: '=', + incomplete: ' ', + width: 20, + total: len + }); + const fileStream = fs.createWriteStream(path); + await new Promise((resolve, reject) => { + res.body.pipe(fileStream); + res.body.on("error", (err) => { + reject(err); + }); + res.body.on('data', function (chunk) { + bar.tick(chunk.length); + }); + fileStream.on("finish", function() { + resolve(); + }); + }); + } + +async function downloadReleaseZip(tag) { + console.log('downloading'); return new Promise(async resolve => { - // get name of latest zip file, which depends on the version - const latest_release_link = 'https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/'; + // get name of zip file, which depends on the version + const latest_release_link = `https://github.com/Tzahi12345/YoutubeDL-Material/releases/download/${tag}/`; const tag_without_v = tag.substring(1, tag.length); const zip_file_name = `youtubedl-material-${tag_without_v}.zip` const latest_zip_link = latest_release_link + zip_file_name; - let output_path = path.join(__dirname, `youtubedl-material-latest-release.zip`); + let output_path = path.join(__dirname, `youtubedl-material-release-${tag}.zip`); // download zip from release - await fetchFile(latest_zip_link, output_path); + await fetchFile(latest_zip_link, output_path, 'update ' + tag); resolve(true); }); @@ -254,21 +316,6 @@ async function installDependencies() { } -// helper function to download file using fetch -const fetchFile = (async (url, path) => { - const res = await fetch(url); - const fileStream = fs.createWriteStream(path); - await new Promise((resolve, reject) => { - res.body.pipe(fileStream); - res.body.on("error", (err) => { - reject(err); - }); - fileStream.on("finish", function() { - resolve(); - }); - }); - }); - async function backupServerLite() { return new Promise(async resolve => { let output_path = `backup-${Date.now()}.zip`; @@ -1695,6 +1742,32 @@ app.post('/api/downloadArchive', async (req, res) => { }); +// Updater API calls + +app.get('/api/updaterStatus', async (req, res) => { + let status = updaterStatus; + + if (status) { + res.send(updaterStatus); + } else { + res.sendStatus(404); + } + +}); + +app.post('/api/updateServer', async (req, res) => { + let tag = req.body.tag; + + updateServer(tag); + + res.send({ + success: true + }); + +}); + +// Pin API calls + app.post('/api/isPinSet', async (req, res) => { let stored_pin = db.get('pin_md5').value(); let is_set = false; diff --git a/backend/package.json b/backend/package.json index c3a814d..40cff74 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,6 +41,7 @@ "node-fetch": "^2.6.0", "node-id3": "^0.1.14", "nodemon": "^2.0.2", + "progress": "^2.0.3", "shortid": "^2.2.15", "unzipper": "^0.10.10", "uuidv4": "^6.0.6", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e7191a7..90c1ac8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -52,6 +52,8 @@ import es from '@angular/common/locales/es'; import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component'; import { VideoInfoDialogComponent } from './dialogs/video-info-dialog/video-info-dialog.component'; import { ArgModifierDialogComponent, HighlightPipe } from './dialogs/arg-modifier-dialog/arg-modifier-dialog.component'; +import { UpdaterComponent } from './updater/updater.component'; +import { UpdateProgressDialogComponent } from './dialogs/update-progress-dialog/update-progress-dialog.component'; registerLocaleData(es, 'es'); export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps) { @@ -77,7 +79,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible AboutDialogComponent, VideoInfoDialogComponent, ArgModifierDialogComponent, - HighlightPipe + HighlightPipe, + UpdaterComponent, + UpdateProgressDialogComponent ], imports: [ CommonModule, diff --git a/src/app/consts.ts b/src/app/consts.ts new file mode 100644 index 0000000..5b1ec6c --- /dev/null +++ b/src/app/consts.ts @@ -0,0 +1 @@ +export const CURRENT_VERSION = 'v3.5'; diff --git a/src/app/dialogs/about-dialog/about-dialog.component.html b/src/app/dialogs/about-dialog/about-dialog.component.html index e46d4ca..9c898fe 100644 --- a/src/app/dialogs/about-dialog/about-dialog.component.html +++ b/src/app/dialogs/about-dialog/about-dialog.component.html @@ -18,7 +18,7 @@
Installation details:

Installed version: {{current_version_tag}} -  Checking for updates... - done  Update available - {{latestGithubRelease['tag_name']}} + done  Update available - {{latestGithubRelease['tag_name']}}. You can update from the settings menu. You are up to date.

diff --git a/src/app/dialogs/about-dialog/about-dialog.component.ts b/src/app/dialogs/about-dialog/about-dialog.component.ts index ba34fcd..1637d60 100644 --- a/src/app/dialogs/about-dialog/about-dialog.component.ts +++ b/src/app/dialogs/about-dialog/about-dialog.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { PostsService } from 'app/posts.services'; +import { CURRENT_VERSION } from 'app/consts'; @Component({ selector: 'app-about-dialog', @@ -14,7 +15,7 @@ export class AboutDialogComponent implements OnInit { latestGithubRelease = null; checking_for_updates = true; - current_version_tag = 'v3.5.1'; + current_version_tag = CURRENT_VERSION; constructor(private postsService: PostsService) { } diff --git a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.html b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.html new file mode 100644 index 0000000..fca82ee --- /dev/null +++ b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.html @@ -0,0 +1,18 @@ +

Updater

+ + +
+
+
Update in progress
+
Update failed
+
Update succeeded!
+
+ + +

{{updateStatus['details']}}

+
+
+ + + + \ No newline at end of file diff --git a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.scss b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.spec.ts b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.spec.ts new file mode 100644 index 0000000..60ce881 --- /dev/null +++ b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UpdateProgressDialogComponent } from './update-progress-dialog.component'; + +describe('UpdateProgressDialogComponent', () => { + let component: UpdateProgressDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UpdateProgressDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UpdateProgressDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts new file mode 100644 index 0000000..a69bdac --- /dev/null +++ b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-update-progress-dialog', + templateUrl: './update-progress-dialog.component.html', + styleUrls: ['./update-progress-dialog.component.scss'] +}) +export class UpdateProgressDialogComponent implements OnInit { + + updateStatus = null; + updateInterval = 250; + errored = false; + + constructor(private postsService: PostsService, private snackBar: MatSnackBar) { } + + ngOnInit(): void { + this.getUpdateProgress(); + setInterval(() => { + this.getUpdateProgress(); + }, 250); + } + + getUpdateProgress() { + this.postsService.getUpdaterStatus().subscribe(res => { + this.updateStatus = res; + if (!this.updateStatus) { + // update complete? + console.log('Update complete? or not started'); + } + if (this.updateStatus && this.updateStatus['error']) { + this.openSnackBar('Update failed. Check logs for more details.'); + } + }); + } + + public openSnackBar(message: string, action: string = '') { + this.snackBar.open(message, action, { + duration: 2000, + }); + } + +} diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 9907ed9..752cc9b 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -185,14 +185,23 @@ export class PostsService { } // updates the server to the latest version - updateServer() { - return this.http.post(this.path + 'updateServer', {}); + updateServer(tag) { + return this.http.post(this.path + 'updateServer', {tag: tag}); + } + + getUpdaterStatus() { + return this.http.get(this.path + 'updaterStatus'); } // gets tag of the latest version of youtubedl-material getLatestGithubRelease() { return this.http.get('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest'); } + + getAvailableRelease() { + return this.http.get('https://api.github.com/repos/tzahi12345/youtubedl-material/releases'); + } + } diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index d36e64f..1cc4e6e 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -247,6 +247,18 @@ + + + + + Version + + +
+ +
+
+ diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 4a631f0..c2b846f 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -6,6 +6,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import {DomSanitizer} from '@angular/platform-browser'; import { MatDialog } from '@angular/material/dialog'; import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component'; +import { CURRENT_VERSION } from 'app/consts'; @Component({ selector: 'app-settings', @@ -24,6 +25,9 @@ export class SettingsComponent implements OnInit { _settingsSame = true; + latestGithubRelease = null; + CURRENT_VERSION = CURRENT_VERSION + get settingsAreTheSame() { this._settingsSame = this.settingsSame() return this._settingsSame; @@ -40,6 +44,8 @@ export class SettingsComponent implements OnInit { this.getConfig(); this.generated_bookmarklet_code = this.sanitizer.bypassSecurityTrustUrl(this.generateBookmarkletCode()); + + this.getLatestGithubRelease(); } getConfig() { @@ -129,6 +135,12 @@ export class SettingsComponent implements OnInit { }); } + getLatestGithubRelease() { + this.postsService.getLatestGithubRelease().subscribe(res => { + this.latestGithubRelease = res; + }); + } + // snackbar helper public openSnackBar(message: string, action: string = '') { this.snackBar.open(message, action, { diff --git a/src/app/updater/updater.component.html b/src/app/updater/updater.component.html new file mode 100644 index 0000000..80d02d2 --- /dev/null +++ b/src/app/updater/updater.component.html @@ -0,0 +1,18 @@ +
+
+ Select a version: +
+
+ + + + {{version['tag_name'] + (version === latestStableRelease ? ' - Latest Stable' : '') + (version['tag_name'] === CURRENT_VERSION ? ' - Current Version' : '')}} + + + +
+
+ +
+
\ No newline at end of file diff --git a/src/app/updater/updater.component.scss b/src/app/updater/updater.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/updater/updater.component.spec.ts b/src/app/updater/updater.component.spec.ts new file mode 100644 index 0000000..62ae61d --- /dev/null +++ b/src/app/updater/updater.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UpdaterComponent } from './updater.component'; + +describe('UpdaterComponent', () => { + let component: UpdaterComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UpdaterComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UpdaterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/updater/updater.component.ts b/src/app/updater/updater.component.ts new file mode 100644 index 0000000..7c63024 --- /dev/null +++ b/src/app/updater/updater.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import { CURRENT_VERSION } from 'app/consts'; +import { MatDialog } from '@angular/material/dialog'; +import { UpdateProgressDialogComponent } from 'app/dialogs/update-progress-dialog/update-progress-dialog.component'; +@Component({ + selector: 'app-updater', + templateUrl: './updater.component.html', + styleUrls: ['./updater.component.scss'] +}) +export class UpdaterComponent implements OnInit { + + availableVersions = null; + availableVersionsFiltered = []; + versionsShowLimit = 5; + latestStableRelease = null; + selectedVersion = null; + CURRENT_VERSION = CURRENT_VERSION; + + constructor(private postsService: PostsService, private dialog: MatDialog) { } + + ngOnInit(): void { + this.getAvailableVersions(); + } + + updateServer() { + this.postsService.updateServer(this.selectedVersion).subscribe(res => { + if (res['success']) { + this.openUpdateProgressDialog(); + } + }); + } + + getAvailableVersions() { + this.availableVersionsFiltered = []; + this.postsService.getAvailableRelease().subscribe(res => { + this.availableVersions = res; + for (let i = 0; i < this.availableVersions.length; i++) { + const currentVersion = this.availableVersions[i]; + // if a stable release has not been found and the version is not "rc" (meaning it's stable) then set it as the stable release + if (!this.latestStableRelease && !currentVersion.tag_name.includes('rc')) { + this.latestStableRelease = currentVersion; + this.selectedVersion = this.latestStableRelease.tag_name; + } + + if (this.latestStableRelease && i >= this.versionsShowLimit) { + break; + } + + this.availableVersionsFiltered.push(currentVersion); + } + }); + } + + openUpdateProgressDialog() { + this.dialog.open(UpdateProgressDialogComponent, { + minWidth: '300px', + minHeight: '200px' + }); + } + +}