diff --git a/backend/app.js b/backend/app.js index 5c6c5e4..745016d 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,6 +1,6 @@ var async = require('async'); const { uuid } = require('uuidv4'); -var fs = require('fs'); +var fs = require('fs-extra'); var path = require('path'); var youtubedl = require('youtube-dl'); var compression = require('compression'); @@ -8,8 +8,10 @@ var https = require('https'); var express = require("express"); var bodyParser = require("body-parser"); 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') @@ -19,6 +21,8 @@ const shortid = require('shortid') const url_api = require('url'); var config_api = require('./config.js'); var subscriptions_api = require('./subscriptions') +const CONSTS = require('./consts') +const { spawn } = require('child_process') var app = express(); @@ -26,6 +30,8 @@ const FileSync = require('lowdb/adapters/FileSync') const adapter = new FileSync('./appdata/db.json'); const db = low(adapter) +// var GithubContent = require('github-content'); + // Set some defaults db.defaults( { @@ -56,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(); @@ -122,15 +139,248 @@ async function startServer() { if (usingEncryption) { https.createServer(options, app).listen(backendPort, function() { - console.log('HTTPS: Started on PORT ' + backendPort); + console.log(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on port ${backendPort} - using SSL`); }); } else { app.listen(backendPort,function(){ - console.log("HTTP: Started on PORT " + backendPort); + console.log(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on PORT ${backendPort}`); }); } + +} + +async function restartServer() { + const restartProcess = () => { + spawn('node', ['app.js'], { + detached: true, + stdio: 'inherit' + }).unref() + process.exit() + } + console.log('Update complete! Restarting server...'); + + // the following line restarts the server through nodemon + fs.writeFileSync('restart.json', 'internal use only'); +} + +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 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 downloadReleaseFiles(tag) { + tag = tag ? tag : await getLatestVersion(); + return new Promise(async resolve => { + console.log('Downloading new files...') + + // downloads the latest release zip file + await downloadReleaseZip(tag); + + // deletes contents of public dir + fs.removeSync(path.join(__dirname, 'public')); + fs.mkdirSync(path.join(__dirname, 'public')); + + let replace_ignore_list = ['youtubedl-material/appdata/default.json', + 'youtubedl-material/appdata/db.json'] + 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-${tag}.zip`)).pipe(unzipper.Parse()) + .on('entry', function (entry) { + var fileName = entry.path; + var type = entry.type; // 'Directory' or 'File' + var size = entry.size; + var is_dir = fileName.substring(fileName.length-1, fileName.length) === '/' + if (!is_dir && fileName.includes('youtubedl-material/public/')) { + // get public folder files + var actualFileName = fileName.replace('youtubedl-material/public/', ''); + if (actualFileName.length !== 0 && actualFileName.substring(actualFileName.length-1, actualFileName.length) !== '/') { + fs.ensureDirSync(path.join(__dirname, 'public', path.dirname(actualFileName))); + entry.pipe(fs.createWriteStream(path.join(__dirname, 'public', actualFileName))); + } else { + entry.autodrain(); + } + } else if (!is_dir && !replace_ignore_list.includes(fileName)) { + // get package.json + var actualFileName = fileName.replace('youtubedl-material/', ''); + if (debugMode) console.log('Downloading file ' + actualFileName); + entry.pipe(fs.createWriteStream(path.join(__dirname, actualFileName))); + } else { + entry.autodrain(); + } + }) + .on('close', function () { + resolve(true); + }); + }); +} + +// 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 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-release-${tag}.zip`); + + // download zip from release + await fetchFile(latest_zip_link, output_path, 'update ' + tag); + resolve(true); + }); + +} + +async function installDependencies() { + return new Promise(resolve => { + var child_process = require('child_process'); + child_process.execSync('npm install',{stdio:[0,1,2]}); + resolve(true); + }); + +} + +async function backupServerLite() { + return new Promise(async resolve => { + let output_path = `backup-${Date.now()}.zip`; + console.log(`Backing up your non-video/audio files to ${output_path}. This may take up to a few seconds/minutes.`); + let output = fs.createWriteStream(path.join(__dirname, output_path)); + var archive = archiver('zip', { + gzip: true, + zlib: { level: 9 } // Sets the compression level. + }); + + archive.on('error', function(err) { + console.log(err); + resolve(false); + }); + + // pipe archive data to the output file + archive.pipe(output); + + // ignore certain directories (ones with video or audio files) + const files_to_ignore = [path.join(config_api.getConfigItem('ytdl_subscriptions_base_path'), '**'), + path.join(config_api.getConfigItem('ytdl_audio_folder_path'), '**'), + path.join(config_api.getConfigItem('ytdl_video_folder_path'), '**'), + 'backup-*.zip']; + + archive.glob('**/*', { + ignore: files_to_ignore + }); + + await archive.finalize(); + + // wait a tiny bit for the zip to reload in fs + setTimeout(function() { + resolve(true); + }, 100); + }); +} + +async function isNewVersionAvailable() { + return new Promise(async resolve => { + // gets tag of the latest version of youtubedl-material, compare to current version + const latest_tag = await getLatestVersion(); + const current_tag = CONSTS['CURRENT_VERSION']; + if (latest_tag > current_tag) { + resolve(true); + } else { + resolve(false); + } + }); +} + +async function getLatestVersion() { + return new Promise(resolve => { + fetch('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest', {method: 'Get'}) + .then(async res => res.json()) + .then(async (json) => { + if (json['message']) { + // means there's an error in getting latest version + console.log(`ERROR: Received the following message from GitHub's API:`); + console.log(json['message']); + if (json['documentation_url']) console.log(`Associated URL: ${json['documentation_url']}`) + } + resolve(json['tag_name']); + return; + }); + }); } async function setPortItemFromENV() { @@ -143,7 +393,6 @@ async function setPortItemFromENV() { async function setAndLoadConfig() { await setConfigFromEnv(); await loadConfig(); - // console.log(backendUrl); } async function setConfigFromEnv() { @@ -162,9 +411,6 @@ async function setConfigFromEnv() { async function loadConfig() { return new Promise(resolve => { - // get config library - // config = require('config'); - url = !debugMode ? config_api.getConfigItem('ytdl_url') : 'http://localhost:4200'; backendPort = config_api.getConfigItem('ytdl_port'); usingEncryption = config_api.getConfigItem('ytdl_use_encryption'); @@ -657,7 +903,7 @@ async function autoUpdateYoutubeDL() { let current_app_details_path = 'node_modules/youtube-dl/bin/details'; let current_app_details_exists = fs.existsSync(current_app_details_path); if (!current_app_details_exists) { - console.log(`Failed to get youtube-dl binary details at location: ${current_app_details_path}. Cancelling update check.`); + console.log(`ERROR: Failed to get youtube-dl binary details at location '${current_app_details_path}'. Cancelling update check.`); resolve(false); return; } @@ -665,9 +911,17 @@ async function autoUpdateYoutubeDL() { let current_version = current_app_details['version']; let stored_binary_path = current_app_details['path']; if (!stored_binary_path || typeof stored_binary_path !== 'string') { - console.log(`Failed to get youtube-dl binary path at location: ${current_app_details_path}. Cancelling update check.`); - resolve(false); - return; + // console.log(`INFO: Failed to get youtube-dl binary path at location: ${current_app_details_path}, attempting to guess actual path...`); + const guessed_base_path = 'node_modules/youtube-dl/bin/'; + const guessed_file_path = guessed_base_path + 'youtube-dl' + (process.platform === 'win32' ? '.exe' : ''); + if (fs.existsSync(guessed_file_path)) { + stored_binary_path = guessed_file_path; + // console.log('INFO: Guess successful! Update process continuing...') + } else { + console.log(`ERROR: Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`); + resolve(false); + return; + } } // got version, now let's check the latest version from the youtube-dl API @@ -676,7 +930,11 @@ async function autoUpdateYoutubeDL() { .then(async res => res.json()) .then(async (json) => { // check if the versions are different - const latest_update_version = json[0]['name']; + if (!json || !json[0]) { + resolve(false); + return false; + } + const latest_update_version = json[0]['name']; if (current_version !== latest_update_version) { let binary_path = 'node_modules/youtube-dl/bin'; // versions different, download new update @@ -729,6 +987,21 @@ async function checkExistsWithTimeout(filePath, timeout) { }); } +// https://stackoverflow.com/a/32197381/8088021 +const deleteFolderRecursive = function(folder_to_delete) { + if (fs.existsSync(folder_to_delete)) { + fs.readdirSync(folder_to_delete).forEach((file, index) => { + const curPath = path.join(folder_to_delete, file); + if (fs.lstatSync(curPath).isDirectory()) { // recurse + deleteFolderRecursive(curPath); + } else { // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(folder_to_delete); + } +}; + app.use(function(req, res, next) { res.header("Access-Control-Allow-Origin", getOrigin()); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); @@ -1469,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/config.js b/backend/config.js index 14d9b12..33d425d 100644 --- a/backend/config.js +++ b/backend/config.js @@ -68,10 +68,16 @@ function setConfigFile(config) { function getConfigItem(key) { let config_json = getConfigFile(); if (!CONFIG_ITEMS[key]) { - console.log('cannot find config with key ' + key); + console.log(`ERROR: Config item with key '${key}' is not recognized.`); return null; } let path = CONFIG_ITEMS[key]['path']; + const val = Object.byString(config_json, path); + if (val === undefined && Object.byString(DEFAULT_CONFIG, path)) { + console.log(`WARNING: Cannot find config with key '${key}'. Creating one with the default value...`); + setConfigItem(key, Object.byString(DEFAULT_CONFIG, path)); + return Object.byString(DEFAULT_CONFIG, path); + } return Object.byString(config_json, path); }; diff --git a/backend/consts.js b/backend/consts.js index 0483521..0cc0f44 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -124,4 +124,7 @@ let CONFIG_ITEMS = { }, }; -module.exports.CONFIG_ITEMS = CONFIG_ITEMS; \ No newline at end of file +module.exports = { + CONFIG_ITEMS: CONFIG_ITEMS, + CURRENT_VERSION: 'v3.5.1' +} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 80426a5..40cff74 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,17 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node app.js" + "start": "nodemon -q app.js" + }, + "nodemonConfig": { + "ignore": [ + "*.js", + "appdata/*", + "public/*" + ], + "watch": [ + "restart.json" + ] }, "repository": { "type": "git", @@ -24,12 +34,16 @@ "config": "^3.2.3", "exe": "^1.0.2", "express": "^4.17.1", + "fs-extra": "^9.0.0", "lowdb": "^1.0.0", "md5": "^2.2.1", - "node-id3": "^0.1.14", "merge-files": "^0.1.2", "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", "youtube-dl": "^3.0.2" } diff --git a/docker-compose.yml b/docker-compose.yml index 0e4673c..fd48e61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,4 +13,4 @@ services: - ./subscriptions:/app/subscriptions ports: - "8998:17442" - image: tzahi12345/youtubedl-material:experimental \ No newline at end of file + image: tzahi12345/youtubedl-material:latest \ No newline at end of file diff --git a/package.json b/package.json index 3f80dd9..dffd0c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "youtube-dl-material", - "version": "3.5.0", + "version": "3.5.1", "license": "MIT", "scripts": { "ng": "ng", @@ -19,25 +19,21 @@ "private": true, "dependencies": { "@angular-devkit/core": "^9.0.6", - "@angular/animations": "^9.0.6", - "@angular/cdk": "^9.1.2", - "@angular/common": "^9.0.6", - "@angular/compiler": "^9.0.6", - "@angular/core": "^9.0.6", - "@angular/forms": "^9.0.6", - "@angular/http": "^7.2.15", - "@angular/localize": "^9.0.6", - "@angular/material": "^9.1.2", - "@angular/platform-browser": "^9.0.6", - "@angular/platform-browser-dynamic": "^9.0.6", - "@angular/router": "^9.0.6", - "@locl/core": "0.0.1-beta.2", + "@angular/animations": "^9.1.0", + "@angular/cdk": "^9.2.0", + "@angular/common": "^9.1.0", + "@angular/compiler": "^9.1.0", + "@angular/core": "^9.0.7", + "@angular/forms": "^9.1.0", + "@angular/localize": "^9.1.0", + "@angular/material": "^9.2.0", + "@angular/platform-browser": "^9.1.0", + "@angular/platform-browser-dynamic": "^9.1.0", + "@angular/router": "^9.1.0", "core-js": "^2.4.1", "file-saver": "^2.0.2", "filesize": "^6.1.0", "ng-lazyload-image": "^7.0.1", - "ng4-configure": "^0.1.7", - "ngx-content-loading": "^0.1.3", "ngx-videogular": "^9.0.1", "rxjs": "^6.5.3", "rxjs-compat": "^6.0.0-rc.0", @@ -47,11 +43,10 @@ "zone.js": "~0.10.2" }, "devDependencies": { - "@angular-devkit/build-angular": "~0.900.6", - "@angular/cli": "^9.0.6", - "@angular/compiler-cli": "^9.0.6", - "@angular/language-service": "^9.0.6", - "@locl/cli": "0.0.1-beta.6", + "@angular-devkit/build-angular": "^0.901.0", + "@angular/cli": "^9.0.7", + "@angular/compiler-cli": "^9.0.7", + "@angular/language-service": "^9.0.7", "@types/core-js": "^2.5.2", "@types/file-saver": "^2.0.1", "@types/jasmine": "2.5.45", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2d2bd77..90c1ac8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,7 @@ import { BrowserModule } from '@angular/platform-browser'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { NgModule, LOCALE_ID } from '@angular/core'; -import { registerLocaleData } from '@angular/common'; +import { registerLocaleData, CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCardModule } from '@angular/material/card'; @@ -27,7 +27,6 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import {DragDropModule} from '@angular/cdk/drag-drop'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import { AppComponent } from './app.component'; -import { HttpModule } from '@angular/http'; import { HttpClientModule, HttpClient } from '@angular/common/http'; import { PostsService } from 'app/posts.services'; import { FileCardComponent } from './file-card/file-card.component'; @@ -38,7 +37,6 @@ import { PlayerComponent } from './player/player.component'; import {VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule} from 'ngx-videogular'; import { InputDialogComponent } from './input-dialog/input-dialog.component'; import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image'; -import { NgxContentLoadingModule } from 'ngx-content-loading'; import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component'; import { CreatePlaylistComponent } from './create-playlist/create-playlist.component'; import { DownloadItemComponent } from './download-item/download-item.component'; @@ -54,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) { @@ -79,9 +79,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible AboutDialogComponent, VideoInfoDialogComponent, ArgModifierDialogComponent, - HighlightPipe + HighlightPipe, + UpdaterComponent, + UpdateProgressDialogComponent ], imports: [ + CommonModule, BrowserModule, BrowserAnimationsModule, MatNativeDateModule, @@ -90,7 +93,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible MatInputModule, MatSelectModule, ReactiveFormsModule, - HttpModule, HttpClientModule, MatToolbarModule, MatCardModule, @@ -118,7 +120,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible VgOverlayPlayModule, VgBufferingModule, LazyLoadImageModule.forRoot({ isVisible }), - NgxContentLoadingModule, RouterModule, AppRoutingModule, ], 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/file-card/file-card.component.html b/src/app/file-card/file-card.component.html index ef79cf6..92116dc 100644 --- a/src/app/file-card/file-card.component.html +++ b/src/app/file-card/file-card.component.html @@ -10,9 +10,7 @@
Thumbnail - - - +
diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 54e464d..752cc9b 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -94,6 +94,10 @@ export class PostsService { } } + loadAsset(name) { + return this.http.get(`./assets/${name}`); + } + setConfig(config) { return this.http.post(this.path + 'setConfig', {new_config_file: config}); } @@ -180,10 +184,24 @@ export class PostsService { return this.http.post(this.path + 'getAllSubscriptions', {}); } + // updates the server to the latest version + 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' + }); + } + +} diff --git a/src/main.ts b/src/main.ts index 00f55c9..3617189 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,6 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { environment } from './environments/environment'; import { loadTranslations } from '@angular/localize'; -import { getTranslations, ParsedTranslationBundle } from '@locl/core'; if (environment.production) { enableProdMode(); @@ -17,8 +16,7 @@ if (!locale) { localStorage.setItem('locale', 'en'); } if (locale && locale !== 'en') { - getTranslations(`./assets/i18n/messages.${locale}.json`).then( - (data: ParsedTranslationBundle) => { + fetch(`./assets/i18n/messages.${locale}.json`).then(res => res.json()).then((data) => { loadTranslations(data as any); import('./app/app.module').then(module => { platformBrowserDynamic()