diff --git a/backend/app.js b/backend/app.js index d8a4c0d..5787999 100644 --- a/backend/app.js +++ b/backend/app.js @@ -77,6 +77,7 @@ db.defaults( video: [] }, configWriteFlag: false, + downloads: {}, subscriptions: [], pin_md5: '', files_to_db_migration_complete: false @@ -101,6 +102,10 @@ var archivePath = path.join(__dirname, 'appdata', 'archives'); var options = null; // encryption options var url_domain = null; var updaterStatus = null; +var last_downloads_check = null; +var downloads_check_interval = 1000; + +var timestamp_server_start = Date.now(); if (debugMode) logger.info('YTDL-Material in debug mode!'); @@ -234,7 +239,6 @@ async function startServer() { logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on PORT ${backendPort}`); }); } - } async function restartServer() { @@ -547,6 +551,9 @@ async function loadConfig() { // check migrations await checkMigrations(); + // load in previous downloads + downloads = db.get('downloads').value(); + // start the server here startServer(); @@ -999,6 +1006,7 @@ function registerFileDB(full_file_path, type) { } file_object['uid'] = uuid(); + file_object['registered'] = Date.now(); path_object = path.parse(file_object['path']); file_object['path'] = path.format(path_object); db.get(`files.${type}`) @@ -1081,6 +1089,304 @@ function getVideoInfos(fileNames) { return result; } +// downloads + +async function downloadFileByURL_exec(url, type, options, sessionID = null) { + return new Promise(async resolve => { + var date = Date.now(); + + const downloadConfig = await generateArgs(url, type, options); + + // adds download to download helper + const download_uid = uuid(); + const session = sessionID ? sessionID : 'undeclared'; + if (!downloads[session]) downloads[session] = {}; + downloads[session][download_uid] = { + uid: download_uid, + downloading: true, + complete: false, + url: url, + type: type, + percent_complete: 0, + is_playlist: url.includes('playlist'), + timestamp_start: Date.now() + }; + const download = downloads[session][download_uid]; + updateDownloads(); + + await new Promise(resolve => { + youtubedl.exec(url, [...downloadConfig, '--dump-json'], {}, function(err, output) { + if (output) { + let json = JSON.parse(output[0]); + const output_no_ext = removeFileExtension(json['_filename']); + download['expected_path'] = output_no_ext + '.mp4'; + download['expected_json_path'] = output_no_ext + '.info.json'; + resolve(true); + } else if (err) { + logger.error(err.stderr); + } else { + logger.error(`Video info retrieval failed. Download progress will be unavailable for URL ${url}`); + } + + }); + }); + youtubedl.exec(url, downloadConfig, {}, function(err, output) { + download['downloading'] = false; + download['timestamp_end'] = Date.now(); + var file_uid = null; + let new_date = Date.now(); + let difference = (new_date - date)/1000; + logger.debug(`Video download delay: ${difference} seconds.`); + if (err) { + logger.error(err.stderr); + + download['error'] = err.stderr; + updateDownloads(); + resolve(false); + throw err; + } else if (output) { + if (output.length === 0 || output[0].length === 0) { + download['error'] = 'No output. Check if video already exists in your archive.'; + updateDownloads(); + + resolve(false); + return; + } + var file_names = []; + for (let i = 0; i < output.length; i++) { + let output_json = null; + try { + output_json = JSON.parse(output[i]); + } catch(e) { + output_json = null; + } + var modified_file_name = output_json ? output_json['title'] : null; + if (!output_json) { + continue; + } + + // get filepath with no extension + const filepath_no_extension = removeFileExtension(output_json['_filename']); + + var full_file_path = filepath_no_extension + '.mp4'; + var file_name = filepath_no_extension.substring(audioFolderPath.length, filepath_no_extension.length); + + // renames file if necessary due to bug + if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) { + try { + fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']); + logger.info('Renamed ' + file_name + '.webm to ' + file_name); + } catch(e) { + } + } + + // registers file in DB + file_uid = registerFileDB(full_file_path.substring(videoFolderPath.length, full_file_path.length), 'video'); + + if (file_name) file_names.push(file_name); + } + + let is_playlist = file_names.length > 1; + + if (options.merged_string !== null) { + let current_merged_archive = fs.readFileSync(videoFolderPath + 'merged.txt', 'utf8'); + let diff = current_merged_archive.replace(options.merged_string, ''); + const archive_path = path.join(archivePath, 'archive_video.txt'); + fs.appendFileSync(archive_path, diff); + } + + download['complete'] = true; + updateDownloads(); + + var videopathEncoded = encodeURIComponent(file_names[0]); + + resolve({ + videopathEncoded: videopathEncoded, + file_names: is_playlist ? file_names : null, + uid: file_uid + }); + } + }); + }); +} + +async function downloadFileByURL_normal(url, type, options, sessionID = null) { + return new Promise(async resolve => { + var date = Date.now(); + var file_uid = null; + + const downloadConfig = await generateArgs(url, type, options); + + // adds download to download helper + const download_uid = uuid(); + const session = sessionID ? sessionID : 'undeclared'; + if (!downloads[session]) downloads[session] = {}; + downloads[session][download_uid] = { + uid: download_uid, + downloading: true, + complete: false, + url: url, + type: type, + percent_complete: 0, + is_playlist: url.includes('playlist'), + timestamp_start: Date.now() + }; + const download = downloads[session][download_uid]; + updateDownloads(); + + const video = youtubedl(url, + // Optional arguments passed to youtube-dl. + downloadConfig, + // Additional options can be given for calling `child_process.execFile()`. + { cwd: __dirname }); + + let video_info = null; + let file_size = 0; + + // Will be called when the download starts. + video.on('info', function(info) { + video_info = info; + file_size = video_info.size; + console.log('Download started') + fs.writeJSONSync(removeFileExtension(video_info._filename) + '.info.json', video_info); + video.pipe(fs.createWriteStream(video_info._filename, { flags: 'w' })) + }); + // Will be called if download was already completed and there is nothing more to download. + video.on('complete', function complete(info) { + 'use strict' + console.log('filename: ' + info._filename + ' already downloaded.') + }) + + let download_pos = 0; + video.on('data', function data(chunk) { + download_pos += chunk.length + // `size` should not be 0 here. + if (file_size) { + let percent = (download_pos / file_size * 100).toFixed(2) + download['percent_complete'] = percent; + } + }); + + video.on('end', function() { + console.log('finished downloading!') + + let new_date = Date.now(); + let difference = (new_date - date)/1000; + logger.debug(`Video download delay: ${difference} seconds.`); + + download['complete'] = true; + updateDownloads(); + + // registers file in DB + const base_file_name = video_info._filename.substring(videoFolderPath.length, video_info._filename.length); + file_uid = registerFileDB(base_file_name, type); + + if (options.merged_string) { + let current_merged_archive = fs.readFileSync(videoFolderPath + 'merged.txt', 'utf8'); + let diff = current_merged_archive.replace(options.merged_string, ''); + const archive_path = path.join(archivePath, 'archive_video.txt'); + fs.appendFileSync(archive_path, diff); + } + + videopathEncoded = encodeURIComponent(removeFileExtension(base_file_name)); + + resolve({ + videopathEncoded: videopathEncoded, + file_names: /*is_playlist ? file_names :*/ null, // playlist support is not ready + uid: file_uid + }); + }); + + video.on('error', function error(err) { + logger.error(err); + + download[error] = err; + updateDownloads(); + + resolve(false); + }); + }); + +} + +async function generateArgs(url, type, options) { + return new Promise(async resolve => { + var videopath = '%(title)s'; + var globalArgs = config_api.getConfigItem('ytdl_custom_args'); + + var customArgs = options.customArgs; + var customOutput = options.customOutput; + + var selectedHeight = options.selectedHeight; + var customQualityConfiguration = options.customQualityConfiguration; + var youtubeUsername = options.youtubeUsername; + var youtubePassword = options.youtubePassword; + + let downloadConfig = null; + let qualityPath = 'best[ext=mp4]'; + + if (url.includes('tiktok') || url.includes('pscp.tv')) { + // tiktok videos fail when using the default format + qualityPath = 'best'; + } + + if (customArgs) { + downloadConfig = customArgs.split(' '); + } else { + if (customQualityConfiguration) { + qualityPath = customQualityConfiguration; + } else if (selectedHeight && selectedHeight !== '') { + qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`; + } + + if (customOutput) { + downloadConfig = ['-o', videoFolderPath + customOutput + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json']; + } else { + downloadConfig = ['-o', videoFolderPath + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json']; + } + + if (youtubeUsername && youtubePassword) { + downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword); + } + + if (!useDefaultDownloadingAgent && customDownloadingAgent) { + downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent); + } + + let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + if (useYoutubeDLArchive) { + const archive_path = path.join(archivePath, 'archive_video.txt'); + // create archive file if it doesn't exist + if (!fs.existsSync(archive_path)) { + fs.closeSync(fs.openSync(archive_path, 'w')); + } + + let blacklist_path = path.join(archivePath, 'blacklist_video.txt'); + // create blacklist file if it doesn't exist + if (!fs.existsSync(blacklist_path)) { + fs.closeSync(fs.openSync(blacklist_path, 'w')); + } + + let merged_path = videoFolderPath + 'merged.txt'; + // merges blacklist and regular archive + let inputPathList = [archive_path, blacklist_path]; + let status = await mergeFiles(inputPathList, merged_path); + + options.merged_string = fs.readFileSync(merged_path, "utf8"); + + downloadConfig.push('--download-archive', merged_path); + } + + if (globalArgs && globalArgs !== '') { + // adds global args + downloadConfig = downloadConfig.concat(globalArgs.split(' ')); + } + + } + resolve(downloadConfig); + }); +} + // currently only works for single urls async function getUrlInfos(urls) { let startDate = Date.now(); @@ -1117,6 +1423,57 @@ function writeToBlacklist(type, line) { fs.appendFileSync(blacklistPath, line); } +// download management functions + +function updateDownloads() { + db.assign({downloads: downloads}).write(); +} + +/* +function checkDownloads() { + for (let [session_id, session_downloads] of Object.entries(downloads)) { + for (let [download_uid, download_obj] of Object.entries(session_downloads)) { + if (download_obj && !download_obj['complete'] && !download_obj['error'] + && download_obj.timestamp_start > timestamp_server_start) { + // download is still running (presumably) + download_obj.percent_complete = getDownloadPercent(download_obj); + } + } + } +} +*/ + +function getDownloadPercent(download_obj) { + if (!download_obj.final_size) { + if (fs.existsSync(download_obj.expected_json_path)) { + const file_json = JSON.parse(fs.readFileSync(download_obj.expected_json_path, 'utf8')); + let calculated_filesize = null; + if (file_json['format_id']) { + calculated_filesize = 0; + const formats_used = file_json['format_id'].split('+'); + for (let i = 0; i < file_json['formats'].length; i++) { + if (formats_used.includes(file_json['formats'][i]['format_id'])) { + calculated_filesize += file_json['formats'][i]['filesize']; + } + } + } + download_obj.final_size = calculated_filesize; + } else { + console.log('could not find json file'); + } + } + if (fs.existsSync(download_obj.expected_path)) { + const stats = fs.statSync(download_obj.expected_path); + const size = stats.size; + return (size / download_obj.final_size)*100; + } else { + console.log('could not find file'); + return 0; + } +} + +// youtube-dl functions + async function startYoutubeDL() { // auto update youtube-dl await autoUpdateYoutubeDL(); @@ -1366,28 +1723,39 @@ app.post('/api/tomp3', async function(req, res) { // adds download to download helper const download_uid = uuid(); - downloads[download_uid] = { + const session = req.query.sessionID ? req.query.sessionID : 'undeclared'; + if (!downloads[session]) downloads[session] = {}; + downloads[session][download_uid] = { uid: download_uid, downloading: true, complete: false, url: url, - type: 'audio' + type: 'audio', + percent_complete: 0, + is_playlist: url.includes('playlist'), + timestamp_start: Date.now() }; - + updateDownloads(); youtubedl.exec(url, downloadConfig, {}, function(err, output) { - downloads[download_uid]['downloading'] = false; + downloads[session][download_uid]['downloading'] = false; var uid = null; let new_date = Date.now(); let difference = (new_date - date)/1000; logger.debug(`Audio download delay: ${difference} seconds.`); if (err) { - audiopath = "-1"; logger.error(err.stderr); + + downloads[session][download_uid]['error'] = err.stderr; + updateDownloads(); + res.sendStatus(500); throw err; } else if (output) { var file_names = []; if (output.length === 0 || output[0].length === 0) { + downloads[session][download_uid]['error'] = 'No output. Check if video already exists in your archive.'; + updateDownloads(); + res.sendStatus(500); return; } @@ -1435,7 +1803,8 @@ app.post('/api/tomp3', async function(req, res) { fs.unlinkSync(merged_path) } - downloads[download_uid]['complete'] = true; + downloads[session][download_uid]['complete'] = true; + updateDownloads(); var audiopathEncoded = encodeURIComponent(file_names[0]); res.send({ @@ -1449,164 +1818,23 @@ app.post('/api/tomp3', async function(req, res) { app.post('/api/tomp4', async function(req, res) { var url = req.body.url; - var date = Date.now(); - var videopath = '%(title)s'; - var globalArgs = config_api.getConfigItem('ytdl_custom_args'); - var customArgs = req.body.customArgs; - var customOutput = req.body.customOutput; - - var selectedHeight = req.body.selectedHeight; - var customQualityConfiguration = req.body.customQualityConfiguration; - var youtubeUsername = req.body.youtubeUsername; - var youtubePassword = req.body.youtubePassword; - - let merged_string = null; - - let downloadConfig = null; - let qualityPath = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'; - - if (url.includes('tiktok') || url.includes('pscp.tv')) { - // tiktok videos fail when using the default format - qualityPath = 'best'; + var options = { + customArgs: req.body.customArgs, + customOutput: req.body.customOutput, + selectedHeight: req.body.selectedHeight, + customQualityConfiguration: req.body.customQualityConfiguration, + youtubeUsername: req.body.youtubeUsername, + youtubePassword: req.body.youtubePassword } - if (customArgs) { - downloadConfig = customArgs.split(' '); + const result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID) + if (result_obj) { + res.send(result_obj); } else { - if (customQualityConfiguration) { - qualityPath = customQualityConfiguration; - } else if (selectedHeight && selectedHeight !== '') { - qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`; - } - - if (customOutput) { - downloadConfig = ['-o', videoFolderPath + customOutput + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json']; - } else { - downloadConfig = ['-o', videoFolderPath + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json']; - } - - if (youtubeUsername && youtubePassword) { - downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword); - } - - if (!useDefaultDownloadingAgent && customDownloadingAgent) { - downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent); - } - - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, 'archive_video.txt'); - // create archive file if it doesn't exist - if (!fs.existsSync(archive_path)) { - fs.closeSync(fs.openSync(archive_path, 'w')); - } - - let blacklist_path = path.join(archivePath, 'blacklist_video.txt'); - // create blacklist file if it doesn't exist - if (!fs.existsSync(blacklist_path)) { - fs.closeSync(fs.openSync(blacklist_path, 'w')); - } - - let merged_path = videoFolderPath + 'merged.txt'; - // merges blacklist and regular archive - let inputPathList = [archive_path, blacklist_path]; - let status = await mergeFiles(inputPathList, merged_path); - - merged_string = fs.readFileSync(merged_path, "utf8"); - - downloadConfig.push('--download-archive', merged_path); - } - - if (globalArgs && globalArgs !== '') { - // adds global args - downloadConfig = downloadConfig.concat(globalArgs.split(' ')); - } - + res.sendStatus(500); } - - // adds download to download helper - const download_uid = uuid(); - downloads[download_uid] = { - uid: download_uid, - downloading: true, - complete: false, - url: url, - type: 'video', - percent_complete: 0, - is_playlist: url.includes('playlist') - }; - - youtubedl.exec(url, downloadConfig, {}, function(err, output) { - downloads[download_uid]['downloading'] = false; - var uid = null; - let new_date = Date.now(); - let difference = (new_date - date)/1000; - logger.debug(`Video download delay: ${difference} seconds.`); - if (err) { - videopath = "-1"; - logger.error(err.stderr); - res.sendStatus(500); - throw err; - } else if (output) { - if (output.length === 0 || output[0].length === 0) { - res.sendStatus(500); - return; - } - var file_names = []; - for (let i = 0; i < output.length; i++) { - let output_json = null; - try { - output_json = JSON.parse(output[i]); - } catch(e) { - output_json = null; - } - var modified_file_name = output_json ? output_json['title'] : null; - if (!output_json) { - continue; - } - - // get filepath with no extension - const filepath_no_extension = removeFileExtension(output_json['_filename']); - - var full_file_path = filepath_no_extension + '.mp4'; - var file_name = filepath_no_extension.substring(audioFolderPath.length, filepath_no_extension.length); - - // renames file if necessary due to bug - if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) { - try { - fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']); - logger.info('Renamed ' + file_name + '.webm to ' + file_name); - } catch(e) { - } - } - - // registers file in DB - uid = registerFileDB(full_file_path.substring(videoFolderPath.length, full_file_path.length), 'video'); - - if (file_name) file_names.push(file_name); - } - - let is_playlist = file_names.length > 1; - if (!is_playlist) audiopath = file_names[0]; - - if (merged_string !== null) { - let current_merged_archive = fs.readFileSync(videoFolderPath + 'merged.txt', 'utf8'); - let diff = current_merged_archive.replace(merged_string, ''); - const archive_path = path.join(archivePath, 'archive_video.txt'); - fs.appendFileSync(archive_path, diff); - } - - downloads[download_uid]['complete'] = true; - - var videopathEncoded = encodeURIComponent(file_names[0]); - res.send({ - videopathEncoded: videopathEncoded, - file_names: is_playlist ? file_names : null, - uid: uid - }); - res.end("yes"); - } - }); + + res.end("yes"); }); // gets the status of the mp3 file that's being downloaded @@ -2314,9 +2542,47 @@ app.get('/api/audio/:id', function(req , res){ // Downloads management app.get('/api/downloads', async (req, res) => { + /* + if (!last_downloads_check || Date.now() - last_downloads_check > downloads_check_interval) { + last_downloads_check = Date.now(); + updateDownloads(); + } + */ res.send({downloads: downloads}); }); + app.post('/api/clearDownloads', async (req, res) => { + let success = false; + var delete_all = req.body.delete_all; + if (!req.body.session_id) req.body.session_id = 'undeclared'; + var session_id = req.body.session_id; + var download_id = req.body.download_id; + if (delete_all) { + // delete all downloads + downloads = {}; + success = true; + } else if (download_id) { + // delete just 1 download + if (downloads[session_id][download_id]) { + delete downloads[session_id][download_id]; + success = true; + } else if (!downloads[session_id]) { + logger.error(`Session ${session_id} has no downloads.`) + } else if (!downloads[session_id][download_id]) { + logger.error(`Download '${download_id}' for session '${session_id}' could not be found`); + } + } else if (session_id) { + // delete a session's downloads + if (downloads[session_id]) { + delete downloads[session_id]; + success = true; + } else { + logger.error(`Session ${session_id} has no downloads.`) + } + } + updateDownloads(); + res.send({success: success, downloads: downloads}); + }); app.post('/api/getVideoInfos', async (req, res) => { let fileNames = req.body.fileNames; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index c4cce48..e1b3dcd 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,11 +4,13 @@ import { MainComponent } from './main/main.component'; import { PlayerComponent } from './player/player.component'; import { SubscriptionsComponent } from './subscriptions/subscriptions.component'; import { SubscriptionComponent } from './subscription/subscription/subscription.component'; +import { DownloadsComponent } from './components/downloads/downloads.component'; const routes: Routes = [ { path: 'home', component: MainComponent }, { path: 'player', component: PlayerComponent}, { path: 'subscriptions', component: SubscriptionsComponent }, { path: 'subscription', component: SubscriptionComponent }, + { path: 'downloads', component: DownloadsComponent }, { path: '', redirectTo: '/home', pathMatch: 'full' }, ]; diff --git a/src/app/app.component.html b/src/app/app.component.html index 2a0463f..0e03fe9 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -3,7 +3,7 @@
- +
@@ -37,7 +37,8 @@ Home - Subscriptions + Subscriptions + Downloads diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6f03650..beff3cc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -57,6 +57,7 @@ import { ArgModifierDialogComponent, HighlightPipe } from './dialogs/arg-modifie import { UpdaterComponent } from './updater/updater.component'; import { UpdateProgressDialogComponent } from './dialogs/update-progress-dialog/update-progress-dialog.component'; import { ShareMediaDialogComponent } from './dialogs/share-media-dialog/share-media-dialog.component'; +import { DownloadsComponent } from './components/downloads/downloads.component'; registerLocaleData(es, 'es'); export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps) { @@ -85,7 +86,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible HighlightPipe, UpdaterComponent, UpdateProgressDialogComponent, - ShareMediaDialogComponent + ShareMediaDialogComponent, + DownloadsComponent ], imports: [ CommonModule, diff --git a/src/app/components/downloads/downloads.component.html b/src/app/components/downloads/downloads.component.html new file mode 100644 index 0000000..251ca9b --- /dev/null +++ b/src/app/components/downloads/downloads.component.html @@ -0,0 +1,24 @@ +
+
+ + +

Session ID: {{session_downloads.key}} +  (current) +

+
+
+
+ + + +
+
+
+
+
+
+ +
+

No downloads available!

+
+
\ No newline at end of file diff --git a/src/app/components/downloads/downloads.component.scss b/src/app/components/downloads/downloads.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/downloads/downloads.component.spec.ts b/src/app/components/downloads/downloads.component.spec.ts new file mode 100644 index 0000000..e7a1fa6 --- /dev/null +++ b/src/app/components/downloads/downloads.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DownloadsComponent } from './downloads.component'; + +describe('DownloadsComponent', () => { + let component: DownloadsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DownloadsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DownloadsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/downloads/downloads.component.ts b/src/app/components/downloads/downloads.component.ts new file mode 100644 index 0000000..f07d8d7 --- /dev/null +++ b/src/app/components/downloads/downloads.component.ts @@ -0,0 +1,106 @@ +import { Component, OnInit, ViewChildren, QueryList, ElementRef } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations'; + +@Component({ + selector: 'app-downloads', + templateUrl: './downloads.component.html', + styleUrls: ['./downloads.component.scss'], + animations: [ + // nice stagger effect when showing existing elements + trigger('list', [ + transition(':enter', [ + // child animation selector + stagger + query('@items', + stagger(100, animateChild()), { optional: true } + ) + ]), + ]), + trigger('items', [ + // cubic-bezier for a tiny bouncing feel + transition(':enter', [ + style({ transform: 'scale(0.5)', opacity: 0 }), + animate('500ms cubic-bezier(.8,-0.6,0.2,1.5)', + style({ transform: 'scale(1)', opacity: 1 })) + ]), + transition(':leave', [ + style({ transform: 'scale(1)', opacity: 1, height: '*' }), + animate('1s cubic-bezier(.8,-0.6,0.2,1.5)', + style({ transform: 'scale(0.5)', opacity: 0, height: '0px', margin: '0px' })) + ]), + ]) + ], +}) +export class DownloadsComponent implements OnInit { + + downloads_check_interval = 500; + downloads = null; + + keys = Object.keys; + + valid_sessions_length = 0; + + constructor(public postsService: PostsService) { } + + ngOnInit(): void { + this.getCurrentDownloads(); + setInterval(() => { + this.getCurrentDownloads(); + }, this.downloads_check_interval); + } + + getCurrentDownloads() { + this.postsService.getCurrentDownloads().subscribe(res => { + if (res['downloads']) { + if (JSON.stringify(this.downloads) !== JSON.stringify(res['downloads'])) { + // if they're not the same, then replace + this.downloads = res['downloads']; + } + } else { + // failed to get downloads + } + }); + } + + clearDownload(session_id, download_uid) { + this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => { + if (res['success']) { + this.downloads = res['downloads']; + } else { + } + }); + } + + clearDownloads(session_id) { + this.postsService.clearDownloads(false, session_id).subscribe(res => { + if (res['success']) { + this.downloads = res['downloads']; + } else { + } + }); + } + + clearAllDownloads() { + this.postsService.clearDownloads(true).subscribe(res => { + if (res['success']) { + this.downloads = res['downloads']; + } else { + } + }); + } + + downloadsValid() { + let valid = false; + const keys = this.keys(this.downloads); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = this.downloads[key]; + if (this.keys(value).length > 0) { + valid = true; + break; + } + } + return valid; + } + +} diff --git a/src/app/download-item/download-item.component.html b/src/app/download-item/download-item.component.html index 5c5a02f..cc112c8 100644 --- a/src/app/download-item/download-item.component.html +++ b/src/app/download-item/download-item.component.html @@ -1,16 +1,21 @@
- -
{{queueNumber}}.
-
- -
ID: {{url_id}}
+ +
ID: {{url_id ? url_id : download.uid}}
- + + done + error - - + +
+ + + Error + + {{download.error}} +
\ No newline at end of file diff --git a/src/app/download-item/download-item.component.scss b/src/app/download-item/download-item.component.scss index e69de29..f28a49f 100644 --- a/src/app/download-item/download-item.component.scss +++ b/src/app/download-item/download-item.component.scss @@ -0,0 +1,16 @@ +.shorten { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + display: block; +} + +.mat-expansion-panel:not([class*='mat-elevation-z']) { + box-shadow: none; +} + +.ignore-margin { + margin-left: -15px; + margin-right: -15px; + margin-bottom: -15px; +} diff --git a/src/app/download-item/download-item.component.ts b/src/app/download-item/download-item.component.ts index 6795d11..5520ace 100644 --- a/src/app/download-item/download-item.component.ts +++ b/src/app/download-item/download-item.component.ts @@ -13,9 +13,11 @@ export class DownloadItemComponent implements OnInit { uid: null, type: 'audio', percent_complete: 0, + complete: false, url: 'http://youtube.com/watch?v=17848rufj', downloading: true, - is_playlist: false + is_playlist: false, + error: false }; @Output() cancelDownload = new EventEmitter(); diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 568b1ce..69d4813 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -34,7 +34,9 @@ export interface Download { percent_complete: number; downloading: boolean; is_playlist: boolean; + error: boolean | string; fileNames?: string[]; + complete?: boolean; } @Component({ @@ -207,7 +209,8 @@ export class MainComponent implements OnInit { percent_complete: 0, url: 'http://youtube.com/watch?v=17848rufj', downloading: true, - is_playlist: false + is_playlist: false, + error: false }; simulatedOutput = ''; @@ -571,7 +574,8 @@ export class MainComponent implements OnInit { percent_complete: 0, url: this.url, downloading: true, - is_playlist: this.url.includes('playlist') + is_playlist: this.url.includes('playlist'), + error: false }; this.downloads.push(new_download); if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; @@ -613,7 +617,8 @@ export class MainComponent implements OnInit { percent_complete: 0, url: this.url, downloading: true, - is_playlist: this.url.includes('playlist') + is_playlist: this.url.includes('playlist'), + error: false }; this.downloads.push(new_download); if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 6112e75..a34de62 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -8,6 +8,7 @@ import { THEMES_CONFIG } from '../themes'; import { Router } from '@angular/router'; import { DOCUMENT } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; +import { v4 as uuid } from 'uuid'; @Injectable() export class PostsService { @@ -21,7 +22,9 @@ export class PostsService { theme; settings_changed = new BehaviorSubject(false); auth_token = '4241b401-7236-493e-92b5-b72696b9d853'; + session_id = null; httpOptions = null; + http_params: string = null; debugMode = false; constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document) { @@ -29,15 +32,17 @@ export class PostsService { // this.startPath = window.location.href + '/api/'; // this.startPathSSL = window.location.href + '/api/'; this.path = this.document.location.origin + '/api/'; - + this.session_id = uuid(); if (isDevMode()) { this.debugMode = true; this.path = 'http://localhost:17442/api/'; } + this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}` + this.httpOptions = { params: new HttpParams({ - fromString: `apiKey=${this.auth_token}` + fromString: this.http_params }), }; } @@ -220,6 +225,13 @@ export class PostsService { return this.http.get(this.path + 'downloads', this.httpOptions); } + // clear downloads. download_id is optional, if it exists only 1 download will be cleared + clearDownloads(delete_all = false, session_id = null, download_id = null) { + return this.http.post(this.path + 'clearDownloads', {delete_all: delete_all, + download_id: download_id, + session_id: session_id ? session_id : this.session_id}, this.httpOptions); + } + // updates the server to the latest version updateServer(tag) { return this.http.post(this.path + 'updateServer', {tag: tag}, this.httpOptions);