From b3744e616d548738c999c26a9510cb85420873ba Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 22:52:46 -0600 Subject: [PATCH 01/20] Users can now stream videos concurrently with other users with the new concurrent stream component --- src/app/app.module.ts | 4 +- .../concurrent-stream.component.html | 6 + .../concurrent-stream.component.scss | 7 + .../concurrent-stream.component.spec.ts | 25 ++++ .../concurrent-stream.component.ts | 140 ++++++++++++++++++ 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.html create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.scss create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.spec.ts create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0ae868f..ec638bd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -86,6 +86,7 @@ import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component'; import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component'; import { H401Interceptor } from './http.interceptor'; +import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component'; registerLocaleData(es, 'es'); @@ -134,7 +135,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible CustomPlaylistsComponent, EditCategoryDialogComponent, TwitchChatComponent, - SeeMoreComponent + SeeMoreComponent, + ConcurrentStreamComponent ], imports: [ CommonModule, diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.html b/src/app/components/concurrent-stream/concurrent-stream.component.html new file mode 100644 index 0000000..414c4ac --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.html @@ -0,0 +1,6 @@ +
+ + + + +
\ No newline at end of file diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.scss b/src/app/components/concurrent-stream/concurrent-stream.component.scss new file mode 100644 index 0000000..d3b74be --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.scss @@ -0,0 +1,7 @@ +.buttons-container { + display: flex; + align-items: center; + justify-content: center; + margin-top: 15px; + margin-bottom: 15px; +} \ No newline at end of file diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts b/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts new file mode 100644 index 0000000..a881ec8 --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConcurrentStreamComponent } from './concurrent-stream.component'; + +describe('ConcurrentStreamComponent', () => { + let component: ConcurrentStreamComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConcurrentStreamComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConcurrentStreamComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.ts b/src/app/components/concurrent-stream/concurrent-stream.component.ts new file mode 100644 index 0000000..6c2cc67 --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.ts @@ -0,0 +1,140 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { PostsService } from 'app/posts.services'; + +@Component({ + selector: 'app-concurrent-stream', + templateUrl: './concurrent-stream.component.html', + styleUrls: ['./concurrent-stream.component.scss'] +}) +export class ConcurrentStreamComponent implements OnInit { + + @Input() server_mode = false; + @Input() playback_timestamp; + @Input() playing; + @Input() uid; + + @Output() setPlaybackTimestamp = new EventEmitter(); + @Output() togglePlayback = new EventEmitter(); + @Output() setPlaybackRate = new EventEmitter(); + + started = false; + server_started = false; + watch_together_clicked = false; + + server_already_exists = null; + + check_timeout: any; + update_timeout: any; + + PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION = 0.5; + PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP = 2; + + PLAYBACK_MODIFIER = 0.1; + + playback_rate_modified = false; + + constructor(private postsService: PostsService) { } + + // flow: click start watching -> check for available stream to enable join button and if user, display "start stream" + // users who join a stream will send continuous requests for info on playback + + ngOnInit(): void { + + } + + startServer() { + this.started = true; + this.server_started = true; + this.update_timeout = setInterval(() => { + this.updateStream(); + }, 1000); + } + + updateStream() { + this.postsService.updateConcurrentStream(this.uid, this.playback_timestamp, Date.now()/1000, this.playing).subscribe(res => { + }); + } + + startClient() { + this.started = true; + } + + checkStream() { + if (this.server_started) { return; } + const current_playback_timestamp = this.playback_timestamp; + const current_unix_timestamp = Date.now()/1000; + this.postsService.checkConcurrentStream(this.uid).subscribe(res => { + const stream = res['stream']; + + if (!stream) { + this.server_already_exists = false; + return; + } + + this.server_already_exists = true; + + // check whether client has joined the stream + if (!this.started) { return; } + + if (!stream['playing'] && this.playing) { + // tell client to pause and set the timestamp to sync + this.togglePlayback.emit(false); + this.setPlaybackTimestamp.emit(stream['playback_timestamp']); + } else if (stream['playing']) { + // sync unpause state + if (!this.playing) { this.togglePlayback.emit(true); } + + // sync time + const zeroed_local_unix_timestamp = current_unix_timestamp - current_playback_timestamp; + const zeroed_server_unix_timestamp = stream['unix_timestamp'] - stream['playback_timestamp']; + + const seconds_behind_locally = zeroed_local_unix_timestamp - zeroed_server_unix_timestamp; + + if (Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP) { + // skip to playback timestamp because the difference is too high + this.setPlaybackTimestamp.emit(this.playback_timestamp + seconds_behind_locally + 0.3); + this.playback_rate_modified = false; + } else if (!this.playback_rate_modified && Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION) { + // increase playback speed to avoid skipping + let seconds_to_wait = (Math.abs(seconds_behind_locally)/this.PLAYBACK_MODIFIER); + seconds_to_wait += 0.3/this.PLAYBACK_MODIFIER; + + this.playback_rate_modified = true; + + if (seconds_behind_locally > 0) { + // increase speed + this.setPlaybackRate.emit(1 + this.PLAYBACK_MODIFIER); + setTimeout(() => { + this.setPlaybackRate.emit(1); + this.playback_rate_modified = false; + }, seconds_to_wait * 1000); + } else { + // decrease speed + this.setPlaybackRate.emit(1 - this.PLAYBACK_MODIFIER); + setTimeout(() => { + this.setPlaybackRate.emit(1); + this.playback_rate_modified = false; + }, seconds_to_wait * 1000); + } + } + } + }); + } + + startWatching() { + this.watch_together_clicked = true; + this.check_timeout = setInterval(() => { + this.checkStream(); + }, 1000); + } + + stop() { + if (this.check_timeout) { clearInterval(this.check_timeout); } + if (this.update_timeout) { clearInterval(this.update_timeout); } + this.started = false; + this.server_started = false; + this.watch_together_clicked = false; + } + + +} From 46f85794391d981123b30ce7efd885af344f55f6 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 22:56:16 -0600 Subject: [PATCH 02/20] Refactored player component to utilize uids instead of fileNames to improve maintainability, consistency, and reliability Playlists now use uids instead of fileNames Added generic getPlaylist and updatePlaylist functions --- backend/app.js | 164 +++++++------ backend/db.js | 86 ++++--- .../custom-playlists.component.ts | 16 +- .../recent-videos/recent-videos.component.ts | 5 +- .../create-playlist.component.html | 2 +- .../share-media-dialog.component.ts | 2 +- src/app/main/main.component.ts | 34 +-- src/app/player/player.component.html | 8 +- src/app/player/player.component.ts | 225 ++++++------------ src/app/posts.services.ts | 45 ++-- .../subscription-file-card.component.ts | 2 +- .../subscription/subscription.component.ts | 14 +- 12 files changed, 288 insertions(+), 315 deletions(-) diff --git a/backend/app.js b/backend/app.js index c5e2d11..24b5664 100644 --- a/backend/app.js +++ b/backend/app.js @@ -139,6 +139,8 @@ var updaterStatus = null; var timestamp_server_start = Date.now(); +const concurrentStreams = {}; + if (debugMode) logger.info('YTDL-Material in debug mode!'); // check if just updated @@ -1849,14 +1851,14 @@ const optionalJwt = function (req, res, next) { const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') || req.path.includes('/api/stream') || + req.path.includes('/api/getPlaylist') || req.path.includes('/api/downloadFile'))) { // check if shared video const using_body = req.body && req.body.uuid; const uuid = using_body ? req.body.uuid : req.query.uuid; const uid = using_body ? req.body.uid : req.query.uid; - const type = using_body ? req.body.type : req.query.type; - const playlist_id = using_body ? req.body.id : req.query.id; - const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, type, true, req.body) : auth_api.getUserPlaylist(uuid, playlist_id, null, false); + const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id; + const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : db_api.getPlaylist(playlist_id, uuid, true); if (file) { req.can_watch = true; return next(); @@ -2118,6 +2120,34 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) { }); }); +app.post('/api/checkConcurrentStream', async (req, res) => { + const uid = req.body.uid; + + const DEAD_SERVER_THRESHOLD = 10; + + if (concurrentStreams[uid] && Date.now()/1000 - concurrentStreams[uid]['unix_timestamp'] > DEAD_SERVER_THRESHOLD) { + logger.verbose( `Killing dead stream on ${uid}`); + delete concurrentStreams[uid]; + } + + res.send({stream: concurrentStreams[uid]}) +}); + +app.post('/api/updateConcurrentStream', optionalJwt, async (req, res) => { + const uid = req.body.uid; + const playback_timestamp = req.body.playback_timestamp; + const unix_timestamp = req.body.unix_timestamp; + const playing = req.body.playing; + + concurrentStreams[uid] = { + playback_timestamp: playback_timestamp, + unix_timestamp: unix_timestamp, + playing: playing + } + + res.send({stream: concurrentStreams[uid]}) +}); + app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => { var id = req.body.id; var type = req.body.type; @@ -2174,7 +2204,7 @@ app.post('/api/enableSharing', optionalJwt, function(req, res) { // single-user mode try { success = true; - if (!is_playlist && type !== 'subscription') { + if (!is_playlist) { db.get(`files`) .find({uid: uid}) .assign({sharingEnabled: true}) @@ -2184,7 +2214,7 @@ app.post('/api/enableSharing', optionalJwt, function(req, res) { .find({id: uid}) .assign({sharingEnabled: true}) .write(); - } else if (type === 'subscription') { + } else if (false) { // TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every // time they are requested from the subscription directory. } else { @@ -2193,6 +2223,7 @@ app.post('/api/enableSharing', optionalJwt, function(req, res) { } } catch(err) { + logger.error(err); success = false; } @@ -2525,14 +2556,14 @@ app.post('/api/getSubscriptions', optionalJwt, async (req, res) => { app.post('/api/createPlaylist', optionalJwt, async (req, res) => { let playlistName = req.body.playlistName; - let fileNames = req.body.fileNames; + let uids = req.body.uids; let type = req.body.type; let thumbnailURL = req.body.thumbnailURL; let duration = req.body.duration; let new_playlist = { name: playlistName, - fileNames: fileNames, + uids: uids, id: shortid.generate(), thumbnailURL: thumbnailURL, type: type, @@ -2556,15 +2587,19 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => { }); app.post('/api/getPlaylist', optionalJwt, async (req, res) => { - let playlistID = req.body.playlistID; - let uuid = req.body.uuid; + let playlist_id = req.body.playlist_id; + let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null); + let include_file_metadata = req.body.include_file_metadata; - let playlist = null; + const playlist = await db_api.getPlaylist(playlist_id, uuid); - if (req.isAuthenticated()) { - playlist = auth_api.getUserPlaylist(uuid ? uuid : req.user.uid, playlistID); - } else { - playlist = db.get(`playlists`).find({id: playlistID}).value(); + if (playlist && include_file_metadata) { + playlist['file_objs'] = []; + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const file_obj = await db_api.getVideo(uid, uuid); + playlist['file_objs'].push(file_obj); + } } res.send({ @@ -2576,16 +2611,16 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => { app.post('/api/updatePlaylistFiles', optionalJwt, async (req, res) => { let playlistID = req.body.playlistID; - let fileNames = req.body.fileNames; + let uids = req.body.uids; let success = false; try { if (req.isAuthenticated()) { - auth_api.updatePlaylistFiles(req.user.uid, playlistID, fileNames); + auth_api.updatePlaylistFiles(req.user.uid, playlistID, uids); } else { db.get(`playlists`) .find({id: playlistID}) - .assign({fileNames: fileNames}) + .assign({uids: uids}) .write(); } @@ -2664,51 +2699,36 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { }); app.post('/api/downloadFile', optionalJwt, async (req, res) => { - let fileNames = req.body.fileNames; - let zip_mode = req.body.zip_mode; - let type = req.body.type; - let outputName = req.body.outputName; - let fullPathProvided = req.body.fullPathProvided; - let subscriptionName = req.body.subscriptionName; - let subscriptionPlaylist = req.body.subPlaylist; - let file = null; - if (!zip_mode) { - fileNames = decodeURIComponent(fileNames); - const is_audio = type === 'audio'; - const fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; - const ext = is_audio ? '.mp3' : '.mp4'; + let uid = req.body.uid; + let is_playlist = req.body.is_playlist; + let uuid = req.body.uuid; - let base_path = fileFolderPath; - let usersFileFolder = null; - const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); - if (multiUserMode && (req.body.uuid || req.user.uid)) { - usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - base_path = path.join(usersFileFolder, req.body.uuid ? req.body.uuid : req.user.uid, type); - } - if (!subscriptionName) { - file = path.join(__dirname, base_path, fileNames + ext); - } else { - let basePath = null; - if (usersFileFolder) - basePath = path.join(usersFileFolder, req.user.uid, 'subscriptions'); - else - basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); + let file_path_to_download = null; - file = path.join(__dirname, basePath, (subscriptionPlaylist === true || subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext); + if (!uuid && req.user) uuid = req.user.uid; + if (is_playlist) { + const playlist_files_to_download = []; + const playlist = db_api.getPlaylist(uid, uuid); + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const file_obj = await db_api.getVideo(uid, uuid); + playlist_files_to_download.push(file_obj.path); } + + // generate zip + file_path_to_download = await createPlaylistZipFile(playlist_files_to_download, playlist.type, playlist.name); } else { - for (let i = 0; i < fileNames.length; i++) { - fileNames[i] = decodeURIComponent(fileNames[i]); - } - file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided, req.body.uuid || req.user.uid); - if (!path.isAbsolute(file)) file = path.join(__dirname, file); + const file_obj = await db_api.getVideo(uid, uuid) + file_path_to_download = file_obj.path; } - res.sendFile(file, function (err) { + if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download); + res.sendFile(file_path_to_download, function (err) { if (err) { logger.error(err); - } else if (fullPathProvided) { + } else if (is_playlist) { try { - fs.unlinkSync(file); + // delete generated zip file + fs.unlinkSync(file_path_to_download); } catch(e) { logger.error("Failed to remove file", file); } @@ -2783,31 +2803,21 @@ app.post('/api/generateNewAPIKey', function (req, res) { // Streaming API calls -app.get('/api/stream/:id', optionalJwt, (req, res) => { +app.get('/api/stream', optionalJwt, async (req, res) => { const type = req.query.type; const ext = type === 'audio' ? '.mp3' : '.mp4'; const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4'; var head; let optionalParams = url_api.parse(req.url,true).query; - let id = decodeURIComponent(req.params.id); - let file_path = req.query.file_path ? decodeURIComponent(req.query.file_path.split('?')[0]) : null; - if (!file_path && (req.isAuthenticated() || req.can_watch)) { - let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - if (optionalParams['subName']) { - const isPlaylist = optionalParams['subPlaylist']; - file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + ext) - } else { - file_path = path.join(usersFileFolder, req.query.uuid ? req.query.uuid : req.user.uid, type, id + ext); - } - } else if (!file_path && optionalParams['subName']) { - let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); - const isPlaylist = optionalParams['subPlaylist']; - basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/'); - file_path = basePath + optionalParams['subName'] + '/' + id + ext; - } + let uid = decodeURIComponent(req.query.uid); - if (!file_path) { - file_path = path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext); + let file_path = null; + + const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); + if (!multiUserMode || req.isAuthenticated() || req.can_watch) { + const file_obj = await db_api.getVideo(uid, req.query.uuid ? req.query.uuid : (req.user ? req.user.uid : null), req.query.sub_id); + if (file_obj) file_path = file_obj['path']; + else file_path = null; } const stat = fs.statSync(file_path) @@ -2821,11 +2831,11 @@ app.get('/api/stream/:id', optionalJwt, (req, res) => { : fileSize-1 const chunksize = (end-start)+1 const file = fs.createReadStream(file_path, {start, end}) - if (config_api.descriptors[id]) config_api.descriptors[id].push(file); - else config_api.descriptors[id] = [file]; + if (config_api.descriptors[uid]) config_api.descriptors[uid].push(file); + else config_api.descriptors[uid] = [file]; file.on('close', function() { - let index = config_api.descriptors[id].indexOf(file); - config_api.descriptors[id].splice(index, 1); + let index = config_api.descriptors[uid].indexOf(file); + config_api.descriptors[uid].splice(index, 1); logger.debug('Successfully closed stream and removed file reference.'); }); head = { diff --git a/backend/db.js b/backend/db.js index c8ef8fb..061280b 100644 --- a/backend/db.js +++ b/backend/db.js @@ -10,12 +10,12 @@ var users_db = null; function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db } function setLogger(input_logger) { logger = input_logger; } -function initialize(input_db, input_users_db, input_logger) { +exports.initialize = (input_db, input_users_db, input_logger) => { setDB(input_db, input_users_db); setLogger(input_logger); } -function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null) { +exports.registerFileDB = (file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null) => { let db_path = null; const file_id = utils.removeFileExtension(file_path); const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub); @@ -107,23 +107,11 @@ function generateFileObject(id, type, customPath = null, sub = null) { return file_obj; } -function updatePlaylist(playlist, user_uid) { - let playlistID = playlist.id; - let db_loc = null; - if (user_uid) { - db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}); - } else { - db_loc = db.get(`playlists`).find({id: playlistID}); - } - db_loc.assign(playlist).write(); - return true; -} - function getAppendedBasePathSub(sub, base_path) { return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name); } -function getFileDirectoriesAndDBs() { +exports.getFileDirectoriesAndDBs = () => { let dirs_to_check = []; let subscriptions_to_check = []; const subscriptions_base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); // only for single-user mode @@ -192,8 +180,8 @@ function getFileDirectoriesAndDBs() { return dirs_to_check; } -async function importUnregisteredFiles() { - const dirs_to_check = getFileDirectoriesAndDBs(); +exports.importUnregisteredFiles = async () => { + const dirs_to_check = exports.getFileDirectoriesAndDBs(); // run through check list and check each file to see if it's missing from the db for (const dir_to_check of dirs_to_check) { @@ -213,7 +201,7 @@ async function importUnregisteredFiles() { } -async function preimportUnregisteredSubscriptionFile(sub, appendedBasePath) { +exports.preimportUnregisteredSubscriptionFile = async (sub, appendedBasePath) => { const preimported_file_paths = []; let dbPath = null; @@ -236,13 +224,60 @@ async function preimportUnregisteredSubscriptionFile(sub, appendedBasePath) { return preimported_file_paths; } -async function getVideo(file_uid, uuid, sub_id) { +exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => { + let playlist = null + if (user_uid) { + playlist = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlist_id}).value(); + + // prevent unauthorized users from accessing the file info + if (require_sharing && !playlist['sharingEnabled']) return null; + } else { + playlist = db.get(`playlists`).find({id: playlist_id}).value(); + } + + // converts playlists to new UID-based schema + if (playlist && playlist['fileNames'] && !playlist['uids']) { + playlist['uids'] = []; + logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`); + for (let i = 0; i < playlist['fileNames'].length; i++) { + const fileName = playlist['fileNames'][i]; + const uid = exports.getVideoUIDByID(fileName, user_uid); + if (uid) playlist['uids'].push(uid); + else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`); + } + delete playlist['fileNames']; + exports.updatePlaylist(playlist, user_uid); + } + + return playlist; +} + +exports.updatePlaylist = (playlist, user_uid = null) => { + let playlistID = playlist.id; + let db_loc = null; + if (user_uid) { + db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}); + } else { + db_loc = db.get(`playlists`).find({id: playlistID}); + } + db_loc.assign(playlist).write(); + return true; +} + +// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that +exports.getVideoUIDByID = (file_id, uuid = null) => { + const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; + const file_obj = base_db_path.get('files').find({id: file_id}).value(); + return file_obj ? file_obj['uid'] : null; +} + +exports.getVideo = async (file_uid, uuid, sub_id) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); return sub_db_path.find({uid: file_uid}).value(); } -async function setVideoProperty(file_uid, assignment_obj, uuid, sub_id) { +exports.setVideoProperty = async (file_uid, assignment_obj, uuid, sub_id) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); const file_db_path = sub_db_path.find({uid: file_uid}); @@ -251,14 +286,3 @@ async function setVideoProperty(file_uid, assignment_obj, uuid, sub_id) { } sub_db_path.find({uid: file_uid}).assign(assignment_obj).write(); } - -module.exports = { - initialize: initialize, - registerFileDB: registerFileDB, - updatePlaylist: updatePlaylist, - getFileDirectoriesAndDBs: getFileDirectoriesAndDBs, - importUnregisteredFiles: importUnregisteredFiles, - preimportUnregisteredSubscriptionFile: preimportUnregisteredSubscriptionFile, - getVideo: getVideo, - setVideoProperty: setVideoProperty -} diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 73e3036..8870a8e 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -57,12 +57,11 @@ export class CustomPlaylistsComponent implements OnInit { if (playlist) { if (this.postsService.config['Extra']['download_only_mode']) { - this.downloading_content[type][playlistID] = true; - this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID); + this.downloadPlaylist(playlist.id, playlist.name); } else { localStorage.setItem('player_navigator', this.router.url); const fileNames = playlist.fileNames; - this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID, auto: playlist.auto}]); + this.router.navigate(['/player', {playlist_id: playlistID, auto: playlist.auto}]); } } else { // playlist not found @@ -70,11 +69,12 @@ export class CustomPlaylistsComponent implements OnInit { } } - downloadPlaylist(fileNames, type, zipName = null, playlistID = null) { - this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => { - if (playlistID) { this.downloading_content[type][playlistID] = false }; - const blob: Blob = res; - saveAs(blob, zipName + '.zip'); + downloadPlaylist(playlist_id, playlist_name) { + this.downloading_content[playlist_id] = true; + this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => { + this.downloading_content[playlist_id] = false; + const blob: any = res; + saveAs(blob, playlist_name + '.zip'); }); } diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 6aec7f2..2b8fe94 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -201,8 +201,7 @@ export class RecentVideosComponent implements OnInit { const type = file.isAudio ? 'audio' : 'video'; const ext = type === 'audio' ? '.mp3' : '.mp4' const sub = this.postsService.getSubscriptionByID(file.sub_id); - this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist, - this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => { + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { const blob: Blob = res; saveAs(blob, file.id + ext); }, err => { @@ -215,7 +214,7 @@ export class RecentVideosComponent implements OnInit { const ext = type === 'audio' ? '.mp3' : '.mp4' const name = file.id; this.downloading_content[type][name] = true; - this.postsService.downloadFileFromServer(name, type).subscribe(res => { + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { this.downloading_content[type][name] = false; const blob: Blob = res; saveAs(blob, decodeURIComponent(name) + ext); diff --git a/src/app/create-playlist/create-playlist.component.html b/src/app/create-playlist/create-playlist.component.html index 8027983..d9f108a 100644 --- a/src/app/create-playlist/create-playlist.component.html +++ b/src/app/create-playlist/create-playlist.component.html @@ -19,7 +19,7 @@ Audio files Videos - {{file.id}} + {{file.id}} {{file.id}} {{file.id}} diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts index 137e8fc..9b687ff 100644 --- a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts @@ -33,7 +33,7 @@ export class ShareMediaDialogComponent implements OnInit { this.is_playlist = this.data.is_playlist; this.current_timestamp = (this.data.current_timestamp / 1000).toFixed(2); - const arg = (this.is_playlist ? ';id=' : ';uid='); + const arg = (this.is_playlist ? ';playlist_id=' : ';uid='); this.default_share_url = window.location.href.split(';')[0] + arg + this.uid; if (this.uuid) { this.default_share_url += ';uuid=' + this.uuid; diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 51a90ce..e37d041 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -355,7 +355,7 @@ export class MainComponent implements OnInit { if (playlist) { if (this.downloadOnlyMode) { this.downloading_content[type][playlistID] = true; - this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID); + this.downloadPlaylist(playlist); } else { localStorage.setItem('player_navigator', this.router.url); const fileNames = playlist.fileNames; @@ -626,41 +626,41 @@ export class MainComponent implements OnInit { } } - downloadAudioFile(name) { - this.downloading_content['audio'][name] = true; - this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => { - this.downloading_content['audio'][name] = false; + downloadAudioFile(file) { + this.downloading_content['audio'][file.id] = true; + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { + this.downloading_content['audio'][file.id] = false; const blob: Blob = res; - saveAs(blob, decodeURIComponent(name) + '.mp3'); + saveAs(blob, decodeURIComponent(file.id) + '.mp3'); if (!this.fileManagerEnabled) { // tell server to delete the file once downloaded - this.postsService.deleteFile(name, 'video').subscribe(delRes => { + this.postsService.deleteFile(file.uid).subscribe(delRes => { }); } }); } - downloadVideoFile(name) { - this.downloading_content['video'][name] = true; - this.postsService.downloadFileFromServer(name, 'video').subscribe(res => { - this.downloading_content['video'][name] = false; + downloadVideoFile(file) { + this.downloading_content['video'][file.id] = true; + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { + this.downloading_content['video'][file.id] = false; const blob: Blob = res; - saveAs(blob, decodeURIComponent(name) + '.mp4'); + saveAs(blob, decodeURIComponent(file.id) + '.mp4'); if (!this.fileManagerEnabled) { // tell server to delete the file once downloaded - this.postsService.deleteFile(name, 'audio').subscribe(delRes => { + this.postsService.deleteFile(file.uid).subscribe(delRes => { }); } }); } - downloadPlaylist(fileNames, type, zipName = null, playlistID = null) { - this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => { - if (playlistID) { this.downloading_content[type][playlistID] = false }; + downloadPlaylist(playlist) { + this.postsService.downloadFileFromServer(playlist.id, null, true).subscribe(res => { + if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false }; const blob: Blob = res; - saveAs(blob, zipName + '.zip'); + saveAs(blob, playlist.name + '.zip'); }); } diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 7f8d928..9de791a 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -29,12 +29,11 @@
- - + - + @@ -47,6 +46,9 @@ {{playlist_item.label}}
+ + + diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index b3660a5..601271d 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -36,18 +36,16 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { api_ready = false; // params - fileNames: string[]; + uids: string[]; type: string; - id = null; // used for playlists (not subscription) + playlist_id = null; // used for playlists (not subscription) uid = null; // used for non-subscription files (audio, video, playlist) subscription = null; - subscriptionName = null; + sub_id = null; subPlaylist = null; uuid = null; // used for sharing in multi-user mode, uuid is the user that downloaded the video timestamp = null; - is_shared = false; - db_playlist = null; db_file = null; @@ -56,8 +54,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { videoFolderPath = null; subscriptionFolderPath = null; - sharingEnabled = null; - // url-mode params url = null; name = null; @@ -79,11 +75,9 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit(): void { this.innerWidth = window.innerWidth; - this.type = this.route.snapshot.paramMap.get('type'); - this.id = this.route.snapshot.paramMap.get('id'); + this.playlist_id = this.route.snapshot.paramMap.get('playlist_id'); this.uid = this.route.snapshot.paramMap.get('uid'); - this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName'); - this.subPlaylist = this.route.snapshot.paramMap.get('subPlaylist'); + this.sub_id = this.route.snapshot.paramMap.get('sub_id'); this.url = this.route.snapshot.paramMap.get('url'); this.name = this.route.snapshot.paramMap.get('name'); this.uuid = this.route.snapshot.paramMap.get('uuid'); @@ -120,19 +114,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.audioFolderPath = this.postsService.config['Downloader']['path-audio']; this.videoFolderPath = this.postsService.config['Downloader']['path-video']; this.subscriptionFolderPath = this.postsService.config['Subscriptions']['subscriptions_base_path']; - this.fileNames = this.route.snapshot.paramMap.get('fileNames') ? this.route.snapshot.paramMap.get('fileNames').split('|nvr|') : null; - if (!this.fileNames && !this.type) { - this.is_shared = true; - } - - if (this.uid && !this.id) { - this.getFile(); - } else if (this.id) { - this.getPlaylistFiles(); - } else if (this.subscriptionName) { + if (this.sub_id) { this.getSubscription(); - } + } else if (this.playlist_id) { + this.getPlaylistFiles(); + } else if (this.uid) { + this.getFile(); + } if (this.url) { // if a url is given, just stream the URL @@ -147,14 +136,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.currentItem = this.playlist[0]; this.currentIndex = 0; this.show_player = true; - } else if (this.fileNames && !this.subscriptionName) { - this.show_player = true; - this.parseFileNames(); } } getFile() { - const already_has_filenames = !!this.fileNames; this.postsService.getFile(this.uid, null, this.uuid).subscribe(res => { this.db_file = res['file']; if (!this.db_file) { @@ -165,45 +150,32 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { console.error('Failed to increment view count'); console.error(err); }); - this.sharingEnabled = this.db_file.sharingEnabled; - if (!this.fileNames) { - // means it's a shared video - if (!this.id) { - // regular video/audio file (not playlist) - this.fileNames = [this.db_file['id']]; - this.type = this.db_file['isAudio'] ? 'audio' : 'video'; - if (!already_has_filenames) { this.parseFileNames(); } - } - } - if (this.db_file['sharingEnabled'] || !this.uuid) { - this.show_player = true; - } else if (!already_has_filenames) { - this.openSnackBar('Error: Sharing has been disabled for this video!', 'Dismiss'); - } + // regular video/audio file (not playlist) + this.uids = [this.db_file['uid']]; + this.type = this.db_file['isAudio'] ? 'audio' : 'video'; + this.parseFileNames(); }); } getSubscription() { - this.postsService.getSubscription(null, this.subscriptionName).subscribe(res => { + this.postsService.getSubscription(this.sub_id).subscribe(res => { const subscription = res['subscription']; this.subscription = subscription; - if (this.fileNames) { - subscription.videos.forEach(video => { - if (video['id'] === this.fileNames[0]) { - this.db_file = video; - this.postsService.incrementViewCount(this.db_file['uid'], this.subscription['id'], this.uuid).subscribe(res => {}, err => { - console.error('Failed to increment view count'); - console.error(err); - }); - this.show_player = true; - this.parseFileNames(); - } - }); - } else { - console.log('no file name specified'); - } + this.type === this.subscription.type; + subscription.videos.forEach(video => { + if (video['uid'] === this.uid) { + this.db_file = video; + this.postsService.incrementViewCount(this.db_file['uid'], this.sub_id, this.uuid).subscribe(res => {}, err => { + console.error('Failed to increment view count'); + console.error(err); + }); + this.uids = this.db_file['uid']; + this.show_player = true; + this.parseFileNames(); + } + }); }, err => { - this.openSnackBar(`Failed to find subscription ${this.subscriptionName}`, 'Dismiss'); + this.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss'); }); } @@ -212,10 +184,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.show_player = true; return; } - this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => { + this.postsService.getPlaylist(this.playlist_id, this.uuid, true).subscribe(res => { if (res['playlist']) { this.db_playlist = res['playlist']; - this.fileNames = this.db_playlist['fileNames']; + this.uids = this.db_playlist.uids; this.type = res['type']; this.show_player = true; this.parseFileNames(); @@ -231,60 +203,43 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { let fileType = null; if (this.type === 'audio') { fileType = 'audio/mp3'; - } else if (this.type === 'video') { - fileType = 'video/mp4'; } else { - // error - console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.'); + fileType = 'video/mp4'; } this.playlist = []; - for (let i = 0; i < this.fileNames.length; i++) { - const fileName = this.fileNames[i]; - let baseLocation = null; - let fullLocation = null; + for (let i = 0; i < this.uids.length; i++) { + const uid = this.uids[i]; - // adds user token if in multi-user-mode - const uuid_str = this.uuid ? `&uuid=${this.uuid}` : ''; - const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`; - const type_str = (this.type || !this.db_file) ? `&type=${this.type}` : `&type=${this.db_file.type}` - const id_str = this.id ? `&id=${this.id}` : ''; - const file_path_str = (!this.db_file) ? '' : `&file_path=${encodeURIComponent(this.db_file.path)}`; + const file_obj = this.playlist_id ? this.db_playlist['file_objs'][i] : this.db_file; - if (!this.subscriptionName) { - baseLocation = 'stream/'; - fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + `?test=test${type_str}${file_path_str}`; - } else { - // default to video but include subscription name param - baseLocation = 'stream/'; - fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName + - '&subPlaylist=' + this.subPlaylist + `${file_path_str}${type_str}`; - } + let baseLocation = 'stream/'; + let fullLocation = this.baseStreamPath + baseLocation + `?test=test&uid=${file_obj['uid']}`; if (this.postsService.isLoggedIn) { - fullLocation += (this.subscriptionName ? '&' : '&') + `jwt=${this.postsService.token}`; - if (this.is_shared) { fullLocation += `${uuid_str}${uid_str}${type_str}${id_str}`; } - } else if (this.is_shared) { - fullLocation += (this.subscriptionName ? '&' : '?') + `test=test${uuid_str}${uid_str}${type_str}${id_str}`; + fullLocation += `&jwt=${this.postsService.token}`; } - // if it has a slash (meaning it's in a directory), only get the file name for the label - let label = null; - const decodedName = decodeURIComponent(fileName); - const hasSlash = decodedName.includes('/') || decodedName.includes('\\'); - if (hasSlash) { - label = decodedName.replace(/^.*[\\\/]/, ''); - } else { - label = decodedName; + + if (this.uuid) { + fullLocation += `&uuid=${this.uuid}`; } + + if (this.sub_id) { + fullLocation += `&sub_id=${this.sub_id}`; + } else if (this.playlist_id) { + fullLocation += `&playlist_id=${this.playlist_id}`; + } + const mediaObject: IMedia = { - title: fileName, + title: file_obj['title'], src: fullLocation, type: fileType, - label: label + label: file_obj['title'] } this.playlist.push(mediaObject); } this.currentItem = this.playlist[this.currentIndex]; this.original_playlist = JSON.stringify(this.playlist); + this.show_player = true; } onPlayerReady(api: VgApiService) { @@ -361,8 +316,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0]; this.downloading = true; - this.postsService.downloadFileFromServer(fileNames, this.type, zipName, null, null, null, null, - !this.uuid ? this.postsService.user.uid : this.uuid, this.id).subscribe(res => { + this.postsService.downloadFileFromServer(this.playlist_id, this.uuid, true).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, zipName + '.zip'); @@ -376,8 +330,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const ext = (this.type === 'audio') ? '.mp3' : '.mp4'; const filename = this.playlist[0].title; this.downloading = true; - this.postsService.downloadFileFromServer(filename, this.type, null, null, this.subscriptionName, this.subPlaylist, - this.is_shared ? this.db_file['uid'] : null, this.uuid).subscribe(res => { + this.postsService.downloadFileFromServer(this.uid, this.uuid, false).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, filename + ext); @@ -387,50 +340,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { }); } - namePlaylistDialog() { - const done = new EventEmitter(); - const dialogRef = this.dialog.open(InputDialogComponent, { - width: '300px', - data: { - inputTitle: 'Name the playlist', - inputPlaceholder: 'Name', - submitText: 'Favorite', - doneEmitter: done - } - }); - - done.subscribe(name => { - - // Eventually do additional checks on name - if (name) { - const fileNames = this.getFileNames(); - this.postsService.createPlaylist(name, fileNames, this.type, null).subscribe(res => { - if (res['success']) { - dialogRef.close(); - const new_playlist = res['new_playlist']; - this.db_playlist = new_playlist; - this.openSnackBar('Playlist \'' + name + '\' successfully created!', '') - this.playlistPostCreationHandler(new_playlist.id); - } - }); - } - }); - } - - /* - createPlaylist(name) { - this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => { - if (res['success']) { - console.log('Success!'); - } - }); - } - */ - playlistPostCreationHandler(playlistID) { // changes the route without moving from the current view or // triggering a navigation event - this.id = playlistID; + this.playlist_id = playlistID; this.router.navigateByUrl(this.router.url + ';id=' + playlistID); } @@ -445,11 +358,11 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { updatePlaylist() { const fileNames = this.getFileNames(); this.playlist_updating = true; - this.postsService.updatePlaylistFiles(this.id, fileNames, this.type).subscribe(res => { + this.postsService.updatePlaylistFiles(this.playlist_id, fileNames, this.type).subscribe(res => { this.playlist_updating = false; if (res['success']) { const fileNamesEncoded = fileNames.join('|nvr|'); - this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.id}]); + this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.playlist_id}]); this.openSnackBar('Successfully updated playlist.', ''); this.original_playlist = JSON.stringify(this.playlist); } else { @@ -461,10 +374,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { openShareDialog() { const dialogRef = this.dialog.open(ShareMediaDialogComponent, { data: { - uid: this.id ? this.id : this.uid, + uid: this.playlist_id ? this.playlist_id : this.uid, type: this.type, - sharing_enabled: this.id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled, - is_playlist: !!this.id, + sharing_enabled: this.playlist_id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled, + is_playlist: !!this.playlist_id, uuid: this.postsService.isLoggedIn ? this.postsService.user.uid : null, current_timestamp: this.api.time.current }, @@ -472,7 +385,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { }); dialogRef.afterClosed().subscribe(res => { - if (!this.id) { + if (!this.playlist_id) { this.getFile(); } else { this.getPlaylistFiles(); @@ -489,6 +402,22 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { }) } + setPlaybackTimestamp(time) { + this.api.seekTime(time); + } + + togglePlayback(to_play) { + if (to_play) { + this.api.play(); + } else { + this.api.pause(); + } + } + + setPlaybackRate(speed) { + this.api.playbackRate = speed; + } + // snackbar helper public openSnackBar(message: string, action: string) { this.snackBar.open(message, action, { diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 1fd7929..cf04447 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -219,8 +219,8 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions); } - deleteFile(uid: string, type: string, blacklistMode = false) { - return this.http.post(this.path + 'deleteFile', {uid: uid, type: type, blacklistMode: blacklistMode}, this.httpOptions); + deleteFile(uid: string, blacklistMode = false) { + return this.http.post(this.path + 'deleteFile', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions); } getMp3s() { @@ -247,22 +247,30 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid, sub: sub}, this.httpOptions); } - downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null, - uid = null, uuid = null, id = null) { - return this.http.post(this.path + 'downloadFile', {fileNames: fileName, - type: type, - zip_mode: Array.isArray(fileName), - outputName: outputName, - fullPathProvided: fullPathProvided, - subscriptionName: subscriptionName, - subPlaylist: subPlaylist, - uuid: uuid, + downloadFileFromServer(uid, uuid = null, is_playlist = false) { + return this.http.post(this.path + 'downloadFile', { uid: uid, - id: id + uuid: uuid, + is_playlist: is_playlist }, {responseType: 'blob', params: this.httpOptions.params}); } + downloadPlaylistFromServer(playlist_id, uuid = null) { + return this.http.post(this.path + 'downloadPlaylist', {playlist_id: playlist_id, uuid: uuid}); + } + + checkConcurrentStream(uid) { + return this.http.post(this.path + 'checkConcurrentStream', {uid: uid}, this.httpOptions); + } + + updateConcurrentStream(uid, playback_timestamp, unix_timestamp, playing) { + return this.http.post(this.path + 'updateConcurrentStream', {uid: uid, + playback_timestamp: playback_timestamp, + unix_timestamp: unix_timestamp, + playing: playing}, this.httpOptions); + } + uploadCookiesFile(fileFormData) { return this.http.post(this.path + 'uploadCookies', fileFormData, this.httpOptions); } @@ -299,17 +307,18 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions); } - createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) { + createPlaylist(playlistName, uids, type, thumbnailURL, duration = null) { return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName, - fileNames: fileNames, + uids: uids, type: type, thumbnailURL: thumbnailURL, duration: duration}, this.httpOptions); } - getPlaylist(playlistID, type, uuid = null) { - return this.http.post(this.path + 'getPlaylist', {playlistID: playlistID, - type: type, uuid: uuid}, this.httpOptions); + getPlaylist(playlist_id, uuid = null, include_file_metadata = false) { + return this.http.post(this.path + 'getPlaylist', {playlist_id: playlist_id, + uuid: uuid, + include_file_metadata: include_file_metadata}, this.httpOptions); } updatePlaylist(playlist) { diff --git a/src/app/subscription/subscription-file-card/subscription-file-card.component.ts b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts index e500a7d..2257fd8 100644 --- a/src/app/subscription/subscription-file-card/subscription-file-card.component.ts +++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts @@ -42,7 +42,7 @@ export class SubscriptionFileCardComponent implements OnInit { goToFile() { const emit_obj = { - name: this.file.id, + uid: this.file.uid, url: this.file.requested_formats ? this.file.requested_formats[0].url : this.file.url } this.goToFileEmit.emit(emit_obj); diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index 461dcfc..af08088 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -103,15 +103,14 @@ export class SubscriptionComponent implements OnInit, OnDestroy { } goToFile(emit_obj) { - const name = emit_obj['name']; + const uid = emit_obj['uid']; const url = emit_obj['url']; localStorage.setItem('player_navigator', this.router.url); if (this.subscription.streamingOnly) { - this.router.navigate(['/player', {name: name, url: url}]); + this.router.navigate(['/player', {uid: uid, url: url}]); } else { - this.router.navigate(['/player', {fileNames: name, - type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name, - subPlaylist: this.subscription.isPlaylist}]); + this.router.navigate(['/player', {uid: uid, + sub_id: this.subscription.id}]); } } @@ -154,14 +153,15 @@ export class SubscriptionComponent implements OnInit, OnDestroy { } this.downloading = true; - this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { + // TODO: add download subscription route + /*this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, this.subscription.name + '.zip'); }, err => { console.log(err); this.downloading = false; - }); + });*/ } editSubscription() { From 1d2ab0dc41e20215919ba31f8aae9367107c3389 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 22:56:38 -0600 Subject: [PATCH 03/20] 401 errors will now not cause redirects in the /player route --- src/app/http.interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/http.interceptor.ts b/src/app/http.interceptor.ts index edde22b..b941a34 100644 --- a/src/app/http.interceptor.ts +++ b/src/app/http.interceptor.ts @@ -14,7 +14,7 @@ export class H401Interceptor implements HttpInterceptor { return next.handle(request).pipe(catchError(err => { if (err.status === 401) { localStorage.setItem('jwt_token', null); - if (this.router.url !== '/login') { + if (this.router.url !== '/login' && !this.router.url.includes('player')) { this.router.navigate(['/login']).then(() => { this.openSnackBar('Login expired, please login again.'); }); From 297a4a3f34541c405d5d7f3c06e384e62476f256 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:53:36 -0600 Subject: [PATCH 04/20] Simplified streaming and file deletion functions --- backend/app.js | 142 ++++-------------- .../recent-videos/recent-videos.component.ts | 2 +- src/app/file-card/file-card.component.ts | 2 +- 3 files changed, 32 insertions(+), 114 deletions(-) diff --git a/backend/app.js b/backend/app.js index 24b5664..8ae57ac 100644 --- a/backend/app.js +++ b/backend/app.js @@ -888,18 +888,22 @@ async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvid return path.join(zipFolderPath,outputName + '.zip'); } -async function deleteAudioFile(name, customPath = null, blacklistMode = false) { - let filePath = customPath ? customPath : audioFolderPath; +// TODO: add to db_api and support multi-user mode +async function deleteFile(uid, uuid = null, blacklistMode = false) { + const file_obj = await db_api.getVideo(uid, uuid); + const type = file_obj.isAudio ? 'audio' : 'video'; + const folderPath = path.dirname(file_obj.path); + const ext = type === 'audio' ? 'mp3' : 'mp4'; + const name = file_obj.id; + const filePathNoExtension = utils.removeFileExtension(file_obj.path); - var jsonPath = path.join(filePath,name+'.mp3.info.json'); - var altJSONPath = path.join(filePath,name+'.info.json'); - var audioFilePath = path.join(filePath,name+'.mp3'); - var thumbnailPath = path.join(filePath,name+'.webp'); - var altThumbnailPath = path.join(filePath,name+'.jpg'); + var jsonPath = `${file_obj.path}.info.json`; + var altJSONPath = `${filePathNoExtension}.info.json`; + var thumbnailPath = `${filePathNoExtension}.webp`; + var altThumbnailPath = `${filePathNoExtension}.jpg`; jsonPath = path.join(__dirname, jsonPath); altJSONPath = path.join(__dirname, altJSONPath); - audioFilePath = path.join(__dirname, audioFilePath); let jsonExists = await fs.pathExists(jsonPath); let thumbnailExists = await fs.pathExists(thumbnailPath); @@ -918,7 +922,7 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { } } - let audioFileExists = await fs.pathExists(audioFilePath); + let fileExists = await fs.pathExists(file_obj.path); if (config_api.descriptors[name]) { try { @@ -932,18 +936,18 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, 'archive_audio.txt'); + const archive_path = path.join(archivePath, `archive_${type}.txt`); // get ID from JSON - var jsonobj = await utils.getJSONMp3(name, filePath); + var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath)); let id = null; if (jsonobj) id = jsonobj.id; // use subscriptions API to remove video from the archive file, and write it to the blacklist if (await fs.pathExists(archive_path)) { const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) await writeToBlacklist('audio', line); + if (blacklistMode && line) await writeToBlacklist(type, line); } else { logger.info('Could not find archive file for audio files. Creating...'); await fs.close(await fs.open(archive_path, 'w')); @@ -952,84 +956,9 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { if (jsonExists) await fs.unlink(jsonPath); if (thumbnailExists) await fs.unlink(thumbnailPath); - if (audioFileExists) { - await fs.unlink(audioFilePath); - if (await fs.pathExists(jsonPath) || await fs.pathExists(audioFilePath)) { - return false; - } else { - return true; - } - } else { - // TODO: tell user that the file didn't exist - return true; - } -} - -async function deleteVideoFile(name, customPath = null, blacklistMode = false) { - let filePath = customPath ? customPath : videoFolderPath; - var jsonPath = path.join(filePath,name+'.info.json'); - - var altJSONPath = path.join(filePath,name+'.mp4.info.json'); - var videoFilePath = path.join(filePath,name+'.mp4'); - var thumbnailPath = path.join(filePath,name+'.webp'); - var altThumbnailPath = path.join(filePath,name+'.jpg'); - - jsonPath = path.join(__dirname, jsonPath); - videoFilePath = path.join(__dirname, videoFilePath); - - let jsonExists = await fs.pathExists(jsonPath); - let videoFileExists = await fs.pathExists(videoFilePath); - let thumbnailExists = await fs.pathExists(thumbnailPath); - - if (!jsonExists) { - if (await fs.pathExists(altJSONPath)) { - jsonExists = true; - jsonPath = altJSONPath; - } - } - - if (!thumbnailExists) { - if (await fs.pathExists(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } - } - - if (config_api.descriptors[name]) { - try { - for (let i = 0; i < config_api.descriptors[name].length; i++) { - config_api.descriptors[name][i].destroy(); - } - } catch(e) { - - } - } - - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, 'archive_video.txt'); - - // get ID from JSON - - var jsonobj = await utils.getJSONMp4(name, filePath); - let id = null; - if (jsonobj) id = jsonobj.id; - - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (await fs.pathExists(archive_path)) { - const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) await writeToBlacklist('video', line); - } else { - logger.info('Could not find archive file for videos. Creating...'); - fs.closeSync(fs.openSync(archive_path, 'w')); - } - } - - if (jsonExists) await fs.unlink(jsonPath); - if (thumbnailExists) await fs.unlink(thumbnailPath); - if (videoFileExists) { - await fs.unlink(videoFilePath); - if (await fs.pathExists(jsonPath) || await fs.pathExists(videoFilePath)) { + if (fileExists) { + await fs.unlink(file_obj.path); + if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) { return false; } else { return true; @@ -1638,6 +1567,8 @@ async function cropFile(file_path, start, end, ext) { async function writeToBlacklist(type, line) { let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt'); // adds newline to the beginning of the line + line.replace('\n', ''); + line.replace('\r', ''); line = '\n' + line; await fs.appendFile(blacklistPath, line); } @@ -2668,9 +2599,8 @@ app.post('/api/deletePlaylist', optionalJwt, async (req, res) => { // deletes non-subscription files app.post('/api/deleteFile', optionalJwt, async (req, res) => { - var uid = req.body.uid; - var type = req.body.type; - var blacklistMode = req.body.blacklistMode; + const uid = req.body.uid; + const blacklistMode = req.body.blacklistMode; if (req.isAuthenticated()) { let success = await auth_api.deleteUserFile(req.user.uid, uid, blacklistMode); @@ -2678,24 +2608,10 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { return; } - var file_obj = db.get(`files`).find({uid: uid}).value(); - var name = file_obj.id; - var fullpath = file_obj ? file_obj.path : null; - var wasDeleted = false; - if (await fs.pathExists(fullpath)) - { - wasDeleted = type === 'audio' ? await deleteAudioFile(name, path.dirname(fullpath), blacklistMode) : await deleteVideoFile(name, path.dirname(fullpath), blacklistMode); - db.get('files').remove({uid: uid}).write(); - wasDeleted = true; - res.send(wasDeleted); - } else if (file_obj) { - db.get('files').remove({uid: uid}).write(); - wasDeleted = true; - res.send(wasDeleted); - } else { - wasDeleted = false; - res.send(wasDeleted); - } + let wasDeleted = false; + wasDeleted = await deleteFile(uid, null, blacklistMode); + db.get('files').remove({uid: uid}).write(); + res.send(wasDeleted); }); app.post('/api/downloadFile', optionalJwt, async (req, res) => { @@ -2805,6 +2721,8 @@ app.post('/api/generateNewAPIKey', function (req, res) { app.get('/api/stream', optionalJwt, async (req, res) => { const type = req.query.type; + const uuid = req.query.uuid ? req.query.uuid : (req.user ? req.user.uid : null); + const sub_id = req.query.sub_id; const ext = type === 'audio' ? '.mp3' : '.mp4'; const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4'; var head; @@ -2815,7 +2733,7 @@ app.get('/api/stream', optionalJwt, async (req, res) => { const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); if (!multiUserMode || req.isAuthenticated() || req.can_watch) { - const file_obj = await db_api.getVideo(uid, req.query.uuid ? req.query.uuid : (req.user ? req.user.uid : null), req.query.sub_id); + const file_obj = await db_api.getVideo(uid, uuid, sub_id); if (file_obj) file_path = file_obj['path']; else file_path = null; } diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 2b8fe94..31ed771 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -244,7 +244,7 @@ export class RecentVideosComponent implements OnInit { } deleteNormalFile(file, blacklistMode = false) { - this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => { + this.postsService.deleteFile(file.uid, blacklistMode).subscribe(result => { if (result) { this.postsService.openSnackBar('Delete success!', 'OK.'); this.removeFileCard(file); diff --git a/src/app/file-card/file-card.component.ts b/src/app/file-card/file-card.component.ts index 68a8453..90b906c 100644 --- a/src/app/file-card/file-card.component.ts +++ b/src/app/file-card/file-card.component.ts @@ -56,7 +56,7 @@ export class FileCardComponent implements OnInit { deleteFile(blacklistMode = false) { if (!this.playlist) { - this.postsService.deleteFile(this.uid, this.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => { + this.postsService.deleteFile(this.uid, blacklistMode).subscribe(result => { if (result) { this.openSnackBar('Delete success!', 'OK.'); this.removeFile.emit(this.name); From a11445b80db7db8a4fb3f75c7ca5a10654dfc196 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:54:15 -0600 Subject: [PATCH 05/20] Added backend tests and made authentication more testable --- backend/authentication/auth.js | 14 +- backend/package-lock.json | 1159 ++++++++++++++++++++++---------- backend/package.json | 1 + backend/test/tests.js | 94 +++ 4 files changed, 912 insertions(+), 356 deletions(-) create mode 100644 backend/test/tests.js diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index e7cf337..3af31e7 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -144,16 +144,18 @@ exports.registerUser = function(req, res) { ************************************************/ +exports.login = async (username, password) => { + const user = users_db.get('users').find({name: username}).value(); + if (!user) { logger.error(`User ${username} not found`); false } + if (user.auth_method && user.auth_method !== 'internal') { return false } + return await bcrypt.compare(password, user.passhash) ? user : false; +} + exports.passport.use(new LocalStrategy({ usernameField: 'username', passwordField: 'password'}, async function(username, password, done) { - const user = users_db.get('users').find({name: username}).value(); - if (!user) { logger.error(`User ${username} not found`); return done(null, false); } - if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); } - if (user) { - return done(null, (await bcrypt.compare(password, user.passhash)) ? user : false); - } + return done(null, await exports.login(username, password)); } )); diff --git a/backend/package-lock.json b/backend/package-lock.json index 4067dd9..74f76fa 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -4,6 +4,19 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -87,6 +100,11 @@ "@types/mime": "*" } }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -113,24 +131,56 @@ } }, "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", "requires": { - "string-width": "^2.0.0" + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + }, "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" + }, + "dependencies": { + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } } }, "any-promise": { @@ -209,6 +259,11 @@ } } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -306,9 +361,9 @@ } }, "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bl": { "version": "4.0.2", @@ -350,17 +405,18 @@ } }, "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" } }, "brace-expansion": { @@ -380,6 +436,11 @@ "fill-range": "^7.0.1" } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, "buffer": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", @@ -462,15 +523,31 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==" + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caseless": { "version": "0.12.0", @@ -486,13 +563,27 @@ } }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } } }, "charenc": { @@ -501,29 +592,62 @@ "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" }, "chokidar": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", - "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", "requires": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.1.2", + "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.3.0" + "readdirp": "~3.5.0" } }, "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" }, "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } }, "color": { "version": "3.0.0", @@ -673,16 +797,16 @@ } }, "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" } }, "connected-domain": { @@ -735,14 +859,6 @@ "readable-stream": "^3.4.0" } }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, "cross-spawn": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", @@ -759,9 +875,9 @@ "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" }, "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" }, "dashdash": { "version": "1.14.1", @@ -779,11 +895,29 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -841,12 +975,17 @@ } } }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "requires": { - "is-obj": "^1.0.0" + "is-obj": "^2.0.0" } }, "dtrace-provider": { @@ -909,6 +1048,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, "enabled": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", @@ -935,15 +1079,25 @@ "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==" }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, "etag": { "version": "1.8.1", @@ -1061,6 +1215,20 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, "fluent-ffmpeg": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", @@ -1132,9 +1300,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, "fstream": { @@ -1148,6 +1316,11 @@ "rimraf": "2" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-stream": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", @@ -1178,48 +1351,46 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "requires": { "is-glob": "^4.0.1" } }, "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", "requires": { - "ini": "^1.3.4" + "ini": "1.3.7" } }, "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", "requires": { - "create-error-class": "^3.0.0", + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" }, "dependencies": { "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } } } }, @@ -1228,6 +1399,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1247,6 +1423,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, "hashish": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz", @@ -1255,6 +1436,11 @@ "traverse": ">=0.2.4" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, "hh-mm-ss": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hh-mm-ss/-/hh-mm-ss-1.2.0.tgz", @@ -1263,6 +1449,11 @@ "zero-fill": "^2.2.3" } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -1340,9 +1531,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" }, "ipaddr.js": { "version": "1.9.1", @@ -1368,11 +1559,11 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", "requires": { - "ci-info": "^1.5.0" + "ci-info": "^2.0.0" } }, "is-extglob": { @@ -1394,18 +1585,18 @@ } }, "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" } }, "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" }, "is-number": { "version": "7.0.0", @@ -1413,33 +1604,25 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" }, "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" - }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -1450,6 +1633,11 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1465,11 +1653,24 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "js-yaml": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", + "requires": { + "argparse": "^2.0.1" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -1556,6 +1757,14 @@ "safe-buffer": "^5.0.1" } }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, "kuler": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", @@ -1565,11 +1774,11 @@ } }, "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", "requires": { - "package-json": "^4.0.0" + "package-json": "^6.3.0" } }, "lazystream": { @@ -1667,6 +1876,14 @@ "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", @@ -1727,6 +1944,38 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "logform": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", @@ -1763,21 +2012,19 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "requires": { - "pify": "^3.0.0" + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "md5": { @@ -1841,6 +2088,11 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1862,6 +2114,83 @@ "minimist": "^1.2.5" } }, + "mocha": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.0.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.20", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==" + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", @@ -1993,9 +2322,9 @@ } }, "nodemon": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz", - "integrity": "sha512-GWhYPMfde2+M0FsHnggIHXTqPDHXia32HRhh6H0d75Mt9FKUoCBvumNHr7LdrpPBTKxsWmIEOjoN+P4IU6Hcaw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", + "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", "requires": { "chokidar": "^3.2.2", "debug": "^3.2.6", @@ -2005,22 +2334,22 @@ "semver": "^5.7.1", "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.5.0" + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -2037,6 +2366,11 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -2089,20 +2423,48 @@ "mimic-fn": "^2.1.0" } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, "p-finally": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "parseurl": { @@ -2184,16 +2546,16 @@ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2230,9 +2592,9 @@ "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" }, "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, "process-nextick-args": { "version": "2.0.1", @@ -2261,20 +2623,15 @@ "table-parser": "^0.1.3" } }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==" + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, "pump": { "version": "3.0.0", @@ -2290,11 +2647,27 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "requires": { + "escape-goat": "^2.0.0" + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2348,28 +2721,27 @@ } }, "readdirp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", - "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", "requires": { - "picomatch": "^2.0.7" + "picomatch": "^2.2.1" } }, "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" + "rc": "^1.2.8" } }, "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", "requires": { - "rc": "^1.0.1" + "rc": "^1.2.8" } }, "request": { @@ -2411,6 +2783,19 @@ } } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -2441,11 +2826,18 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", "requires": { - "semver": "^5.0.3" + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "send": { @@ -2475,6 +2867,14 @@ } } }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -2578,12 +2978,38 @@ "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } } }, "string_decoder": { @@ -2595,18 +3021,13 @@ } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -2646,87 +3067,9 @@ } }, "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "requires": { - "execa": "^0.7.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - } - } + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==" }, "text-hex": { "version": "1.0.0", @@ -2749,10 +3092,10 @@ "thenify": ">= 3.1.0 < 4" } }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" }, "to-regex-range": { "version": "5.0.1", @@ -2807,6 +3150,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2821,6 +3169,14 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, "undefsafe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", @@ -2830,11 +3186,11 @@ } }, "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "requires": { - "crypto-random-string": "^1.0.0" + "crypto-random-string": "^2.0.0" } }, "universalify": { @@ -2847,11 +3203,6 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" - }, "unzipper": { "version": "0.10.10", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", @@ -2886,20 +3237,23 @@ } }, "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" } }, "uri-js": { @@ -2911,11 +3265,11 @@ } }, "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", "requires": { - "prepend-http": "^1.0.1" + "prepend-http": "^2.0.0" } }, "util-deprecate": { @@ -2987,12 +3341,44 @@ "isexe": "^2.0.0" } }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { - "string-width": "^2.1.1" + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" } }, "winston": { @@ -3051,35 +3437,108 @@ } } }, + "workerpool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" + } + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, "youtube-dl": { "version": "3.0.2", diff --git a/backend/package.json b/backend/package.json index a38315d..72607de 100644 --- a/backend/package.json +++ b/backend/package.json @@ -44,6 +44,7 @@ "lowdb": "^1.0.0", "md5": "^2.2.1", "merge-files": "^0.1.2", + "mocha": "^8.4.0", "moment": "^2.29.1", "multer": "^1.4.2", "node-fetch": "^2.6.1", diff --git a/backend/test/tests.js b/backend/test/tests.js new file mode 100644 index 0000000..c9726a0 --- /dev/null +++ b/backend/test/tests.js @@ -0,0 +1,94 @@ +var assert = require('assert'); +const low = require('lowdb') +var winston = require('winston'); + +process.chdir('./backend') + +const FileSync = require('lowdb/adapters/FileSync'); + +const adapter = new FileSync('./appdata/db.json'); +const db = low(adapter) + +const users_adapter = new FileSync('./appdata/users.json'); +const users_db = low(users_adapter); + +const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => { + return `${timestamp} ${level.toUpperCase()}: ${message}`; +}); + +let debugMode = process.env.YTDL_MODE === 'debug'; + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine(winston.format.timestamp(), defaultFormat), + defaultMeta: {}, + transports: [ + // + // - Write to all logs with level `info` and below to `combined.log` + // - Write all logs error (and below) to `error.log`. + // + new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }), + new winston.transports.File({ filename: 'appdata/logs/combined.log' }), + new winston.transports.Console({level: !debugMode ? 'info' : 'debug', name: 'console'}) + ] +}); + +var auth_api = require('../authentication/auth'); +var db_api = require('../db'); + +db_api.initialize(db, users_db, logger); +auth_api.initialize(db, users_db, logger); + +describe('Multi User', async function() { + let user = null; + const user_to_test = 'admin'; + before(async function() { + user = await auth_api.login('admin', 'pass'); + console.log('hi') + }); + describe('Authentication', function() { + it('login', async function() { + assert(user); + }); + }); + describe('Video player - normal', function() { + const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e'; + it('Get video', async function() { + const video_obj = db_api.getVideo(video_to_test, 'admin'); + assert(video_obj); + }); + + it('Video access - disallowed', async function() { + await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test); + const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + assert(!video_obj); + }); + + it('Video access - allowed', async function() { + await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test); + const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + assert(video_obj); + }); + }); + // describe('Video player - subscription', function() { + // const sub_to_test = ''; + // const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e'; + // it('Get video', async function() { + // const video_obj = db_api.getVideo(video_to_test, 'admin', ); + // assert(video_obj); + // }); + + // it('Video access - disallowed', async function() { + // await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test, sub_to_test); + // const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + // assert(!video_obj); + // }); + + // it('Video access - allowed', async function() { + // await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test, sub_to_test); + // const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + // assert(video_obj); + // }); + // }); + +}); \ No newline at end of file From 07b48a4da154094aef63b50d5e59d9733f28e3e0 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:55:27 -0600 Subject: [PATCH 06/20] Fixed backend security issues with several dependencies --- backend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 74f76fa..e7edcd2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -366,9 +366,9 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1885,9 +1885,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.defaults": { "version": "4.2.0", From 419fe3c3c6a0739fdfc7cc7d3805a987aa26ca4c Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:58:16 -0600 Subject: [PATCH 07/20] Fixed frontend security issues for several depepndencies --- package-lock.json | 316 +++++++++++++++++++--------------------------- 1 file changed, 131 insertions(+), 185 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ca348b..61b14b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -180,9 +180,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "semver": { @@ -316,6 +316,12 @@ "ms": "2.1.2" } }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "resolve": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", @@ -432,9 +438,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "semver": { @@ -705,9 +711,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true } } @@ -784,9 +790,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true } } @@ -1592,9 +1598,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } }, @@ -1609,9 +1615,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } }, @@ -1760,6 +1766,12 @@ "semver-intersect": "1.4.0" }, "dependencies": { + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -2616,15 +2628,6 @@ "tweetnacl": "^0.14.3" } }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3059,12 +3062,6 @@ "caller-callsite": "^2.0.0" } }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, "callsites": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", @@ -4513,24 +4510,24 @@ "dev": true }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -4582,37 +4579,37 @@ } }, "engine.io": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", - "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", + "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", "dev": true, "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "0.3.1", + "cookie": "~0.4.1", "debug": "~4.1.0", "engine.io-parser": "~2.2.0", - "ws": "^7.1.2" + "ws": "~7.4.2" }, "dependencies": { "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", "dev": true }, "ws": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", - "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", "dev": true } } }, "engine.io-client": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", - "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", "dev": true, "requires": { "component-emitter": "~1.3.0", @@ -4623,8 +4620,8 @@ "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~6.1.0", - "xmlhttprequest-ssl": "~1.5.4", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", "yeast": "0.1.2" }, "dependencies": { @@ -4643,26 +4640,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", - "dev": true - }, - "parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", - "dev": true - }, "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "dev": true } } }, @@ -5930,9 +5912,9 @@ } }, "hosted-git-info": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.7.tgz", - "integrity": "sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -6338,9 +6320,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "inquirer": { @@ -6405,9 +6387,9 @@ "dev": true }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "supports-color": { @@ -7498,9 +7480,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.memoize": { "version": "4.1.2", @@ -7719,9 +7701,9 @@ } }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" @@ -8289,9 +8271,9 @@ }, "dependencies": { "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true } } @@ -8440,9 +8422,9 @@ }, "dependencies": { "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "lru-cache": { @@ -8516,12 +8498,6 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -8942,9 +8918,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "lru-cache": { @@ -9008,9 +8984,9 @@ } }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" @@ -9144,22 +9120,16 @@ } }, "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "dev": true }, "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "dev": true }, "parseurl": { "version": "1.3.3", @@ -11677,16 +11647,16 @@ } }, "socket.io": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", - "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz", + "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==", "dev": true, "requires": { "debug": "~4.1.0", - "engine.io": "~3.4.0", + "engine.io": "~3.5.0", "has-binary2": "~1.0.2", "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.3.0", + "socket.io-client": "2.4.0", "socket.io-parser": "~3.4.0" } }, @@ -11697,38 +11667,32 @@ "dev": true }, "socket.io-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", - "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", + "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", "dev": true, "requires": { "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "engine.io-client": "~3.4.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", "has-binary2": "~1.0.2", - "has-cors": "1.1.0", "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", + "parseqs": "0.0.6", + "parseuri": "0.0.6", "socket.io-parser": "~3.3.0", "to-array": "0.1.4" }, "dependencies": { - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, "isarray": { "version": "2.0.1", @@ -11743,31 +11707,14 @@ "dev": true }, "socket.io-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", - "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", + "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", "dev": true, "requires": { "component-emitter": "~1.3.0", "debug": "~3.1.0", "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } } } } @@ -12082,9 +12029,9 @@ } }, "ssri": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", - "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "dev": true, "requires": { "minipass": "^3.1.1" @@ -13074,9 +13021,9 @@ } }, "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", + "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", "dev": true, "requires": { "querystringify": "^2.1.1", @@ -13796,8 +13743,7 @@ }, "ssri": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "resolved": "", "dev": true, "requires": { "figgy-pudding": "^3.5.1" @@ -14538,9 +14484,9 @@ "dev": true }, "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", + "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==", "dev": true }, "xtend": { From e2c31319cf236bff1d5d1b26b74d101112f5f1b9 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 23 May 2021 03:59:38 -0600 Subject: [PATCH 08/20] Migrated playlist and subscription (per video and sub-wide) video downloading functionality to new schema Migrated modify playlist component to new schema Moved wait function and playlist generation function(s) to utils - added tests for zip generation --- backend/app.js | 95 ++++++------------- backend/db.js | 1 - backend/test/tests.js | 36 +++++++ backend/utils.js | 54 ++++++++++- .../custom-playlists.component.ts | 2 +- .../create-playlist.component.html | 4 +- .../modify-playlist.component.html | 54 ++++++----- .../modify-playlist.component.ts | 30 +++--- src/app/file-card/file-card.component.ts | 2 +- src/app/main/main.component.ts | 2 +- src/app/player/player.component.ts | 5 +- src/app/posts.services.ts | 18 +++- .../subscription/subscription.component.ts | 5 +- 13 files changed, 189 insertions(+), 119 deletions(-) diff --git a/backend/app.js b/backend/app.js index 8ae57ac..065b339 100644 --- a/backend/app.js +++ b/backend/app.js @@ -193,16 +193,6 @@ app.use(auth_api.passport.initialize()); // actual functions -/** - * setTimeout, but its a promise. - * @param {number} ms - */ -async function wait(ms) { - await new Promise(resolve => { - setTimeout(resolve, ms); - }); -} - async function checkMigrations() { // 3.5->3.6 migration const files_to_db_migration_complete = true; // migration phased out! previous code: db.get('files_to_db_migration_complete').value(); @@ -529,7 +519,7 @@ async function backupServerLite() { }); // wait a tiny bit for the zip to reload in fs - await wait(100); + await utils.wait(100); return true; } @@ -597,7 +587,7 @@ async function killAllDownloads() { async function setPortItemFromENV() { config_api.setConfigItem('ytdl_port', backendPort.toString()); - await wait(100); + await utils.wait(100); return true; } @@ -611,7 +601,7 @@ async function setConfigFromEnv() { let success = config_api.setConfigItems(config_items); if (success) { logger.info('Config items set using ENV variables.'); - await wait(100); + await utils.wait(100); return true; } else { logger.error('ERROR: Failed to set config items using ENV variables.'); @@ -847,47 +837,6 @@ function getVideoFormatID(name) } } -async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null, user_uid = null) { - let zipFolderPath = null; - - if (!fullPathProvided) { - zipFolderPath = (type === 'audio') ? audioFolderPath : videoFolderPath - if (user_uid) zipFolderPath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, zipFolderPath); - } else { - zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path')); - } - - let ext = (type === 'audio') ? '.mp3' : '.mp4'; - - let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip')); - - var archive = archiver('zip', { - gzip: true, - zlib: { level: 9 } // Sets the compression level. - }); - - archive.on('error', function(err) { - logger.error(err); - throw err; - }); - - // pipe archive data to the output file - archive.pipe(output); - - for (let i = 0; i < fileNames.length; i++) { - let fileName = fileNames[i]; - let fileNamePathRemoved = path.parse(fileName).base; - let file_path = !fullPathProvided ? path.join(zipFolderPath, fileName + ext) : fileName; - archive.file(file_path, {name: fileNamePathRemoved + ext}) - } - - await archive.finalize(); - - // wait a tiny bit for the zip to reload in fs - await wait(100); - return path.join(zipFolderPath,outputName + '.zip'); -} - // TODO: add to db_api and support multi-user mode async function deleteFile(uid, uuid = null, blacklistMode = false) { const file_obj = await db_api.getVideo(uid, uuid); @@ -2523,18 +2472,19 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => { let include_file_metadata = req.body.include_file_metadata; const playlist = await db_api.getPlaylist(playlist_id, uuid); + const file_objs = []; if (playlist && include_file_metadata) { - playlist['file_objs'] = []; for (let i = 0; i < playlist['uids'].length; i++) { const uid = playlist['uids'][i]; const file_obj = await db_api.getVideo(uid, uuid); - playlist['file_objs'].push(file_obj); + file_objs.push(file_obj); } } res.send({ playlist: playlist, + file_objs: file_objs, type: playlist && playlist.type, success: !!playlist }); @@ -2616,32 +2566,47 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { app.post('/api/downloadFile', optionalJwt, async (req, res) => { let uid = req.body.uid; - let is_playlist = req.body.is_playlist; let uuid = req.body.uuid; + let playlist_id = req.body.playlist_id; + let sub_id = req.body.sub_id; let file_path_to_download = null; if (!uuid && req.user) uuid = req.user.uid; - if (is_playlist) { + + let zip_file_generated = false; + if (playlist_id) { + zip_file_generated = true; const playlist_files_to_download = []; - const playlist = db_api.getPlaylist(uid, uuid); + const playlist = await db_api.getPlaylist(playlist_id, uuid); for (let i = 0; i < playlist['uids'].length; i++) { - const uid = playlist['uids'][i]; - const file_obj = await db_api.getVideo(uid, uuid); - playlist_files_to_download.push(file_obj.path); + const playlist_file_uid = playlist['uids'][i]; + const file_obj = await db_api.getVideo(playlist_file_uid, uuid); + playlist_files_to_download.push(file_obj); } // generate zip - file_path_to_download = await createPlaylistZipFile(playlist_files_to_download, playlist.type, playlist.name); + file_path_to_download = await utils.createContainerZipFile(playlist, playlist_files_to_download); + } else if (sub_id && !uid) { + zip_file_generated = true; + const sub_files_to_download = []; + const sub = subscriptions_api.getSubscription(sub_id, uuid); + for (let i = 0; i < sub['videos'].length; i++) { + const sub_file = sub['videos'][i]; + sub_files_to_download.push(sub_file); + } + + // generate zip + file_path_to_download = await utils.createContainerZipFile(sub, sub_files_to_download); } else { - const file_obj = await db_api.getVideo(uid, uuid) + const file_obj = await db_api.getVideo(uid, uuid, sub_id) file_path_to_download = file_obj.path; } if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download); res.sendFile(file_path_to_download, function (err) { if (err) { logger.error(err); - } else if (is_playlist) { + } else if (zip_file_generated) { try { // delete generated zip file fs.unlinkSync(file_path_to_download); diff --git a/backend/db.js b/backend/db.js index 061280b..106c3f2 100644 --- a/backend/db.js +++ b/backend/db.js @@ -245,7 +245,6 @@ exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = fal if (uid) playlist['uids'].push(uid); else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`); } - delete playlist['fileNames']; exports.updatePlaylist(playlist, user_uid); } diff --git a/backend/test/tests.js b/backend/test/tests.js index c9726a0..9697a36 100644 --- a/backend/test/tests.js +++ b/backend/test/tests.js @@ -35,13 +35,19 @@ const logger = winston.createLogger({ var auth_api = require('../authentication/auth'); var db_api = require('../db'); +const utils = require('../utils'); +const subscriptions_api = require('../subscriptions'); +const fs = require('fs-extra'); db_api.initialize(db, users_db, logger); auth_api.initialize(db, users_db, logger); +subscriptions_api.initialize(db, users_db, logger, db_api); describe('Multi User', async function() { let user = null; const user_to_test = 'admin'; + const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c'; + const playlist_to_test = 'ysabVZz4x'; before(async function() { user = await auth_api.login('admin', 'pass'); console.log('hi') @@ -70,6 +76,36 @@ describe('Multi User', async function() { assert(video_obj); }); }); + describe('Zip generators', function() { + it('Playlist zip generator', async function() { + const playlist = await db_api.getPlaylist(playlist_to_test, user_to_test); + assert(playlist); + const playlist_files_to_download = []; + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const playlist_file = await db_api.getVideo(uid, user_to_test); + playlist_files_to_download.push(playlist_file); + } + const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download); + const zip_exists = fs.pathExistsSync(zip_path); + assert(zip_exists); + if (zip_exists) fs.unlinkSync(zip_path); + }); + + it('Subscription zip generator', async function() { + const sub = subscriptions_api.getSubscription(sub_to_test, user_to_test); + assert(sub); + const sub_files_to_download = []; + for (let i = 0; i < sub['videos'].length; i++) { + const sub_file = sub['videos'][i]; + sub_files_to_download.push(sub_file); + } + const zip_path = await utils.createContainerZipFile(sub, sub_files_to_download); + const zip_exists = fs.pathExistsSync(zip_path); + assert(zip_exists); + if (zip_exists) fs.unlinkSync(zip_path); + }); + }); // describe('Video player - subscription', function() { // const sub_to_test = ''; // const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e'; diff --git a/backend/utils.js b/backend/utils.js index cd7c23d..2b825dd 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -1,6 +1,7 @@ -var fs = require('fs-extra') -var path = require('path') +const fs = require('fs-extra') +const path = require('path') const config_api = require('./config'); +const archiver = require('archiver'); const is_windows = process.platform === 'win32'; @@ -52,6 +53,43 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) { return files; } +async function createContainerZipFile(container_obj, container_file_objs) { + const container_files_to_download = []; + for (let i = 0; i < container_file_objs.length; i++) { + const container_file_obj = container_file_objs[i]; + container_files_to_download.push(container_file_obj.path); + } + return await createZipFile(path.join('appdata', container_obj.name + '.zip'), container_files_to_download); +} + +async function createZipFile(zip_file_path, file_paths) { + let output = fs.createWriteStream(zip_file_path); + + var archive = archiver('zip', { + gzip: true, + zlib: { level: 9 } // Sets the compression level. + }); + + archive.on('error', function(err) { + logger.error(err); + throw err; + }); + + // pipe archive data to the output file + archive.pipe(output); + + for (let file_path of file_paths) { + const file_name = path.parse(file_path).base; + archive.file(file_path, {name: file_name}) + } + + await archive.finalize(); + + // wait a tiny bit for the zip to reload in fs + await wait(100); + return zip_file_path; +} + function getJSONMp4(name, customPath, openReadPerms = false) { var obj = null; // output if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path'); @@ -193,6 +231,16 @@ function removeFileExtension(filename) { return filename_parts.join('.'); } +/** + * setTimeout, but its a promise. + * @param {number} ms + */ + async function wait(ms) { + await new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + // objects function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) { @@ -221,7 +269,9 @@ module.exports = { fixVideoMetadataPerms: fixVideoMetadataPerms, deleteJSONFile: deleteJSONFile, getDownloadedFilesByType: getDownloadedFilesByType, + createContainerZipFile: createContainerZipFile, recFindByExt: recFindByExt, removeFileExtension: removeFileExtension, + wait: wait, File: File } diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 8870a8e..69c7908 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -97,7 +97,7 @@ export class CustomPlaylistsComponent implements OnInit { const index = args.index; const dialogRef = this.dialog.open(ModifyPlaylistComponent, { data: { - playlist: playlist, + playlist_id: playlist.id, width: '65vw' } }); diff --git a/src/app/create-playlist/create-playlist.component.html b/src/app/create-playlist/create-playlist.component.html index d9f108a..ad21ec0 100644 --- a/src/app/create-playlist/create-playlist.component.html +++ b/src/app/create-playlist/create-playlist.component.html @@ -20,8 +20,8 @@ Videos {{file.id}} - {{file.id}} - {{file.id}} + {{file.id}} + {{file.id}} diff --git a/src/app/dialogs/modify-playlist/modify-playlist.component.html b/src/app/dialogs/modify-playlist/modify-playlist.component.html index 69f4cad..a8471bb 100644 --- a/src/app/dialogs/modify-playlist/modify-playlist.component.html +++ b/src/app/dialogs/modify-playlist/modify-playlist.component.html @@ -1,38 +1,40 @@

Modify playlist

- -
- - - -
- -
-
- Normal order  - Reverse order  - +
+ +
+ + +
-
- -
-
+
+
+ Normal order  + Reverse order  + +
- - - -
{{playlist_item}}
-
- - - - +
+ +
+
+ + + + +
{{playlist_item.title}}
+
+ + + + +
- + \ No newline at end of file diff --git a/src/app/dialogs/modify-playlist/modify-playlist.component.ts b/src/app/dialogs/modify-playlist/modify-playlist.component.ts index 414fc92..161cab8 100644 --- a/src/app/dialogs/modify-playlist/modify-playlist.component.ts +++ b/src/app/dialogs/modify-playlist/modify-playlist.component.ts @@ -10,8 +10,12 @@ import { PostsService } from 'app/posts.services'; }) export class ModifyPlaylistComponent implements OnInit { + playlist_id = null; + original_playlist = null; playlist = null; + playlist_file_objs = null; + available_files = []; all_files = []; playlist_updated = false; @@ -23,9 +27,8 @@ export class ModifyPlaylistComponent implements OnInit { ngOnInit(): void { if (this.data) { - this.playlist = JSON.parse(JSON.stringify(this.data.playlist)); - this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist)); - this.getFiles(); + this.playlist_id = this.data.playlist_id; + this.getPlaylist(); } this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true'; @@ -44,11 +47,12 @@ export class ModifyPlaylistComponent implements OnInit { } processFiles(new_files = null) { - if (new_files) { this.all_files = new_files.map(file => file.id); } - this.available_files = this.all_files.filter(e => !this.playlist.fileNames.includes(e)) + if (new_files) { this.all_files = new_files; } + this.available_files = this.all_files.filter(e => !this.playlist_file_objs.includes(e)) } updatePlaylist() { + this.playlist['uids'] = this.playlist_file_objs.map(playlist_file_obj => playlist_file_obj['uid']) this.postsService.updatePlaylist(this.playlist).subscribe(res => { this.playlist_updated = true; this.postsService.openSnackBar('Playlist updated successfully.'); @@ -61,24 +65,26 @@ export class ModifyPlaylistComponent implements OnInit { } getPlaylist() { - this.postsService.getPlaylist(this.playlist.id, this.playlist.type, null).subscribe(res => { + this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => { if (res['playlist']) { this.playlist = res['playlist']; + this.playlist_file_objs = res['file_objs']; this.original_playlist = JSON.parse(JSON.stringify(this.playlist)); + this.getFiles(); } }); } addContent(file) { - this.playlist.fileNames.push(file); + this.playlist_file_objs.push(file); this.processFiles(); } removeContent(index) { if (this.reverse_order) { - index = this.playlist.fileNames.length - 1 - index; + index = this.playlist_file_objs.length - 1 - index; } - this.playlist.fileNames.splice(index, 1); + this.playlist_file_objs.splice(index, 1); this.processFiles(); } @@ -89,10 +95,10 @@ export class ModifyPlaylistComponent implements OnInit { drop(event: CdkDragDrop) { if (this.reverse_order) { - event.previousIndex = this.playlist.fileNames.length - 1 - event.previousIndex; - event.currentIndex = this.playlist.fileNames.length - 1 - event.currentIndex; + event.previousIndex = this.playlist_file_objs.length - 1 - event.previousIndex; + event.currentIndex = this.playlist_file_objs.length - 1 - event.currentIndex; } - moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex); + moveItemInArray(this.playlist_file_objs, event.previousIndex, event.currentIndex); } } diff --git a/src/app/file-card/file-card.component.ts b/src/app/file-card/file-card.component.ts index 90b906c..5596eec 100644 --- a/src/app/file-card/file-card.component.ts +++ b/src/app/file-card/file-card.component.ts @@ -84,7 +84,7 @@ export class FileCardComponent implements OnInit { editPlaylistDialog() { const dialogRef = this.dialog.open(ModifyPlaylistComponent, { data: { - playlist: this.playlist, + playlist_id: this.playlist.id, width: '65vw' } }); diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index e37d041..cd1e3ab 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -657,7 +657,7 @@ export class MainComponent implements OnInit { } downloadPlaylist(playlist) { - this.postsService.downloadFileFromServer(playlist.id, null, true).subscribe(res => { + this.postsService.downloadPlaylistFromServer(playlist.id).subscribe(res => { if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false }; const blob: Blob = res; saveAs(blob, playlist.name + '.zip'); diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 601271d..d0dd480 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -187,6 +187,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.postsService.getPlaylist(this.playlist_id, this.uuid, true).subscribe(res => { if (res['playlist']) { this.db_playlist = res['playlist']; + this.db_playlist['file_objs'] = res['file_objs']; this.uids = this.db_playlist.uids; this.type = res['type']; this.show_player = true; @@ -316,7 +317,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0]; this.downloading = true; - this.postsService.downloadFileFromServer(this.playlist_id, this.uuid, true).subscribe(res => { + this.postsService.downloadPlaylistFromServer(this.playlist_id, this.uuid).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, zipName + '.zip'); @@ -330,7 +331,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const ext = (this.type === 'audio') ? '.mp3' : '.mp4'; const filename = this.playlist[0].title; this.downloading = true; - this.postsService.downloadFileFromServer(this.uid, this.uuid, false).subscribe(res => { + this.postsService.downloadFileFromServer(this.uid, this.uuid, this.sub_id).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, filename + ext); diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index cf04447..f0fc20e 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -247,17 +247,29 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid, sub: sub}, this.httpOptions); } - downloadFileFromServer(uid, uuid = null, is_playlist = false) { + downloadFileFromServer(uid, uuid = null, sub_id = null) { return this.http.post(this.path + 'downloadFile', { uid: uid, uuid: uuid, - is_playlist: is_playlist + sub_id: sub_id }, {responseType: 'blob', params: this.httpOptions.params}); } downloadPlaylistFromServer(playlist_id, uuid = null) { - return this.http.post(this.path + 'downloadPlaylist', {playlist_id: playlist_id, uuid: uuid}); + return this.http.post(this.path + 'downloadFile', { + uuid: uuid, + playlist_id: playlist_id + }, + {responseType: 'blob', params: this.httpOptions.params}); + } + + downloadSubFromServer(sub_id, uuid = null) { + return this.http.post(this.path + 'downloadFile', { + uuid: uuid, + sub_id: sub_id + }, + {responseType: 'blob', params: this.httpOptions.params}); } checkConcurrentStream(uid) { diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index af08088..cb40b1c 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -153,15 +153,14 @@ export class SubscriptionComponent implements OnInit, OnDestroy { } this.downloading = true; - // TODO: add download subscription route - /*this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { + this.postsService.downloadSubFromServer(this.subscription.id).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, this.subscription.name + '.zip'); }, err => { console.log(err); this.downloading = false; - });*/ + }); } editSubscription() { From 4ea239170ee2e4641833ac9f5370f4aacd37de26 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 30 May 2021 00:39:00 -0600 Subject: [PATCH 09/20] If multiple videos exist in one URL, a playlist will be auto generated Removed tomp3 and tomp4 routes, replaced with /downloadFile Simplified category->playlist conversion Simplified playlist creation Simplified file deletion Playlist duration calculation is now done on the backend (categories uses this now too) removeIDFromArchive moved from subscriptions->utils Added plumbing to support type agnostic playlists --- backend/app.js | 223 +++----------- backend/authentication/auth.js | 109 +------ backend/categories.js | 21 ++ backend/db.js | 155 +++++++++- backend/subscriptions.js | 30 +- backend/utils.js | 50 ++++ .../custom-playlists.component.ts | 2 - .../recent-videos/recent-videos.component.ts | 2 +- .../create-playlist.component.ts | 35 +-- .../share-media-dialog.component.html | 3 +- .../share-media-dialog.component.ts | 6 +- src/app/main/main.component.ts | 278 ++++++------------ src/app/player/player.component.html | 8 +- src/app/player/player.component.ts | 19 +- src/app/posts.services.ts | 40 +-- 15 files changed, 381 insertions(+), 600 deletions(-) diff --git a/backend/app.js b/backend/app.js index 065b339..10db1ac 100644 --- a/backend/app.js +++ b/backend/app.js @@ -231,7 +231,7 @@ async function runFilesToDBMigration() { const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value(); if (!file_already_in_db) { logger.verbose(`Migrating file ${file_obj.id}`); - await db_api.registerFileDB(file_obj.id + '.mp3', 'audio'); + db_api.registerFileDB(file_obj.id + '.mp3', 'audio'); } } @@ -240,7 +240,7 @@ async function runFilesToDBMigration() { const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value(); if (!file_already_in_db) { logger.verbose(`Migrating file ${file_obj.id}`); - await db_api.registerFileDB(file_obj.id + '.mp4', 'video'); + db_api.registerFileDB(file_obj.id + '.mp4', 'video'); } } @@ -837,87 +837,6 @@ function getVideoFormatID(name) } } -// TODO: add to db_api and support multi-user mode -async function deleteFile(uid, uuid = null, blacklistMode = false) { - const file_obj = await db_api.getVideo(uid, uuid); - const type = file_obj.isAudio ? 'audio' : 'video'; - const folderPath = path.dirname(file_obj.path); - const ext = type === 'audio' ? 'mp3' : 'mp4'; - const name = file_obj.id; - const filePathNoExtension = utils.removeFileExtension(file_obj.path); - - var jsonPath = `${file_obj.path}.info.json`; - var altJSONPath = `${filePathNoExtension}.info.json`; - var thumbnailPath = `${filePathNoExtension}.webp`; - var altThumbnailPath = `${filePathNoExtension}.jpg`; - - jsonPath = path.join(__dirname, jsonPath); - altJSONPath = path.join(__dirname, altJSONPath); - - let jsonExists = await fs.pathExists(jsonPath); - let thumbnailExists = await fs.pathExists(thumbnailPath); - - if (!jsonExists) { - if (await fs.pathExists(altJSONPath)) { - jsonExists = true; - jsonPath = altJSONPath; - } - } - - if (!thumbnailExists) { - if (await fs.pathExists(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } - } - - let fileExists = await fs.pathExists(file_obj.path); - - if (config_api.descriptors[name]) { - try { - for (let i = 0; i < config_api.descriptors[name].length; i++) { - config_api.descriptors[name][i].destroy(); - } - } catch(e) { - - } - } - - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, `archive_${type}.txt`); - - // get ID from JSON - - var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath)); - let id = null; - if (jsonobj) id = jsonobj.id; - - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (await fs.pathExists(archive_path)) { - const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) await writeToBlacklist(type, line); - } else { - logger.info('Could not find archive file for audio files. Creating...'); - await fs.close(await fs.open(archive_path, 'w')); - } - } - - if (jsonExists) await fs.unlink(jsonPath); - if (thumbnailExists) await fs.unlink(thumbnailPath); - if (fileExists) { - await fs.unlink(file_obj.path); - if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) { - return false; - } else { - return true; - } - } else { - // TODO: tell user that the file didn't exist - return true; - } -} - /** * @param {'audio' | 'video'} type * @param {string[]} fileNames @@ -1036,7 +955,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { download['downloading'] = false; download['timestamp_end'] = Date.now(); - var file_uid = null; + var file_objs = []; let new_date = Date.now(); let difference = (new_date - date)/1000; logger.debug(`${is_audio ? 'Audio' : 'Video'} download delay: ${difference} seconds.`); @@ -1108,9 +1027,12 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { } // registers file in DB - file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath, category, options.cropFileSettings); + const file_obj = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath, category, options.cropFileSettings); + // TODO: remove the following line if (file_name) file_names.push(file_name); + + file_objs.push(file_obj); } let is_playlist = file_names.length > 1; @@ -1126,12 +1048,22 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { download['fileNames'] = is_playlist ? file_names : [full_file_path] updateDownloads(); - var videopathEncoded = encodeURIComponent(file_names[0]); + let container = null; + + if (file_objs.length > 1) { + // create playlist + const playlist_name = file_objs.map(file_obj => file_obj.title).join(', '); + const duration = file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); + container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, file_objs[0]['thumbnailURL'], options.user); + } else if (file_objs.length === 1) { + container = file_objs[0]; + } else { + logger.error('Downloaded file failed to result in metadata object.'); + } resolve({ - [(type === 'audio') ? 'audiopathEncoded' : 'videopathEncoded']: videopathEncoded, - file_names: is_playlist ? file_names : null, - uid: file_uid + file_uids: file_objs.map(file_obj => file_obj.uid), + container: container }); } }); @@ -1260,7 +1192,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) { videopathEncoded = encodeURIComponent(utils.removeFileExtension(base_file_name)); resolve({ - [is_audio ? 'audiopathEncoded' : 'videopathEncoded']: videopathEncoded, + encodedPath: videopathEncoded, file_names: /*is_playlist ? file_names :*/ null, // playlist support is not ready uid: file_uid }); @@ -1727,18 +1659,18 @@ app.use(function(req, res, next) { app.use(compression()); -const optionalJwt = function (req, res, next) { +const optionalJwt = async function (req, res, next) { const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') || req.path.includes('/api/stream') || req.path.includes('/api/getPlaylist') || - req.path.includes('/api/downloadFile'))) { + req.path.includes('/api/downloadFileFromServer'))) { // check if shared video const using_body = req.body && req.body.uuid; const uuid = using_body ? req.body.uuid : req.query.uuid; const uid = using_body ? req.body.uid : req.query.uid; const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id; - const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : db_api.getPlaylist(playlist_id, uuid, true); + const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await db_api.getPlaylist(playlist_id, uuid, true); if (file) { req.can_watch = true; return next(); @@ -1783,38 +1715,10 @@ app.post('/api/restartServer', optionalJwt, (req, res) => { res.send({success: true}); }); -app.post('/api/tomp3', optionalJwt, async function(req, res) { - var url = req.body.url; - var options = { - customArgs: req.body.customArgs, - customOutput: req.body.customOutput, - maxBitrate: req.body.maxBitrate, - customQualityConfiguration: req.body.customQualityConfiguration, - youtubeUsername: req.body.youtubeUsername, - youtubePassword: req.body.youtubePassword, - ui_uid: req.body.ui_uid, - user: req.isAuthenticated() ? req.user.uid : null - } - - const safeDownloadOverride = config_api.getConfigItem('ytdl_safe_download_override') || config_api.globalArgsRequiresSafeDownload(); - if (safeDownloadOverride) logger.verbose('Download is running with the safe download override.'); - const is_playlist = url.includes('playlist'); - - let result_obj = null; - if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate) - result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID); - else - result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID); - if (result_obj) { - res.send(result_obj); - } else { - res.sendStatus(500); - } -}); - -app.post('/api/tomp4', optionalJwt, async function(req, res) { +app.post('/api/downloadFile', optionalJwt, async function(req, res) { req.setTimeout(0); // remove timeout in case of long videos - var url = req.body.url; + const url = req.body.url; + const type = req.body.type; var options = { customArgs: req.body.customArgs, customOutput: req.body.customOutput, @@ -1833,7 +1737,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) { let result_obj = null; if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu')) - result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID); + result_obj = await downloadFileByURL_exec(url, type, options, req.query.sessionID); else result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID); if (result_obj) { @@ -1936,43 +1840,22 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) { // these are returned let files = null; let playlists = null; + const uuid = req.isAuthenticated() ? req.user.uid : null; let subscriptions = config_api.getConfigItem('ytdl_allow_subscriptions') ? (subscriptions_api.getSubscriptions(req.isAuthenticated() ? req.user.uid : null)) : []; // get basic info depending on multi-user mode being enabled - if (req.isAuthenticated()) { + if (uuid) { files = auth_api.getUserVideos(req.user.uid); playlists = auth_api.getUserPlaylists(req.user.uid, files); } else { files = db.get('files').value(); playlists = JSON.parse(JSON.stringify(db.get('playlists').value())); - const categories = db.get('categories').value(); - if (categories) { - categories.forEach(category => { - const audio_files = files && files.filter(file => file.category && file.category.uid === category.uid && file.isAudio); - const video_files = files && files.filter(file => file.category && file.category.uid === category.uid && !file.isAudio); - if (audio_files && audio_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: audio_files[0].thumbnailURL, - thumbnailPath: audio_files[0].thumbnailPath, - fileNames: audio_files.map(file => file.id), - type: 'audio', - auto: true - }); - } - if (video_files && video_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: video_files[0].thumbnailURL, - thumbnailPath: video_files[0].thumbnailPath, - fileNames: video_files.map(file => file.id), - type: 'video', - auto: true - }); - } - }); - } + } + + const categories = categories_api.getCategoriesAsPlaylists(files); + if (categories) { + playlists = playlists.concat(categories); } // loop through subscriptions and add videos @@ -2439,26 +2322,8 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => { let uids = req.body.uids; let type = req.body.type; let thumbnailURL = req.body.thumbnailURL; - let duration = req.body.duration; - - let new_playlist = { - name: playlistName, - uids: uids, - id: shortid.generate(), - thumbnailURL: thumbnailURL, - type: type, - registered: Date.now(), - duration: duration - }; - - if (req.isAuthenticated()) { - auth_api.addPlaylist(req.user.uid, new_playlist, type); - } else { - db.get(`playlists`) - .push(new_playlist) - .write(); - } + const new_playlist = await db_api.createPlaylist(playlistName, uids, type, thumbnailURL, req.isAuthenticated() ? req.user.uid : null); res.send({ new_playlist: new_playlist, @@ -2517,7 +2382,7 @@ app.post('/api/updatePlaylistFiles', optionalJwt, async (req, res) => { app.post('/api/updatePlaylist', optionalJwt, async (req, res) => { let playlist = req.body.playlist; - let success = db_api.updatePlaylist(playlist, req.user && req.user.uid); + let success = await db_api.updatePlaylist(playlist, req.user && req.user.uid); res.send({ success: success }); @@ -2551,20 +2416,14 @@ app.post('/api/deletePlaylist', optionalJwt, async (req, res) => { app.post('/api/deleteFile', optionalJwt, async (req, res) => { const uid = req.body.uid; const blacklistMode = req.body.blacklistMode; - - if (req.isAuthenticated()) { - let success = await auth_api.deleteUserFile(req.user.uid, uid, blacklistMode); - res.send(success); - return; - } + const uuid = req.isAuthenticated() ? req.user.uid : null; let wasDeleted = false; - wasDeleted = await deleteFile(uid, null, blacklistMode); - db.get('files').remove({uid: uid}).write(); + wasDeleted = await db_api.deleteFile(uid, uuid, blacklistMode); res.send(wasDeleted); }); -app.post('/api/downloadFile', optionalJwt, async (req, res) => { +app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => { let uid = req.body.uid; let uuid = req.body.uuid; let playlist_id = req.body.playlist_id; diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index 3af31e7..6e83ec3 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -1,12 +1,10 @@ const path = require('path'); const config_api = require('../config'); const consts = require('../consts'); -var subscriptions_api = require('../subscriptions') const fs = require('fs-extra'); -var jwt = require('jsonwebtoken'); +const jwt = require('jsonwebtoken'); const { uuid } = require('uuidv4'); -var bcrypt = require('bcryptjs'); - +const bcrypt = require('bcryptjs'); var LocalStrategy = require('passport-local').Strategy; var LdapStrategy = require('passport-ldapauth'); @@ -299,11 +297,6 @@ exports.getUserVideo = function(user_uid, file_uid, requireSharing = false) { return file; } -exports.addPlaylist = function(user_uid, new_playlist) { - users_db.get('users').find({uid: user_uid}).get(`playlists`).push(new_playlist).write(); - return true; -} - exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames) { users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).assign({fileNames: new_filenames}); return true; @@ -317,35 +310,6 @@ exports.removePlaylist = function(user_uid, playlistID) { exports.getUserPlaylists = function(user_uid, user_files = null) { const user = users_db.get('users').find({uid: user_uid}).value(); const playlists = JSON.parse(JSON.stringify(user['playlists'])); - const categories = db.get('categories').value(); - if (categories && user_files) { - categories.forEach(category => { - const audio_files = user_files && user_files.filter(file => file.category && file.category.uid === category.uid && file.isAudio); - const video_files = user_files && user_files.filter(file => file.category && file.category.uid === category.uid && !file.isAudio); - if (audio_files && audio_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: audio_files[0].thumbnailURL, - thumbnailPath: audio_files[0].thumbnailPath, - fileNames: audio_files.map(file => file.id), - type: 'audio', - uid: user_uid, - auto: true - }); - } - if (video_files && video_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: video_files[0].thumbnailURL, - thumbnailPath: video_files[0].thumbnailPath, - fileNames: video_files.map(file => file.id), - type: 'video', - uid: user_uid, - auto: true - }); - } - }); - } return playlists; } @@ -369,75 +333,6 @@ exports.registerUserFile = function(user_uid, file_object) { .write(); } -exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = false) { - let success = false; - const file_obj = users_db.get('users').find({uid: user_uid}).get(`files`).find({uid: file_uid}).value(); - if (file_obj) { - const type = file_obj.isAudio ? 'audio' : 'video'; - const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - const ext = type === 'audio' ? '.mp3' : '.mp4'; - - // close descriptors - if (config_api.descriptors[file_obj.id]) { - try { - for (let i = 0; i < config_api.descriptors[file_obj.id].length; i++) { - config_api.descriptors[file_obj.id][i].destroy(); - } - } catch(e) { - - } - } - - const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext); - users_db.get('users').find({uid: user_uid}).get(`files`) - .remove({ - uid: file_uid - }).write(); - if (await fs.pathExists(full_path)) { - // remove json and file - const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json'); - const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json'); - let youtube_id = null; - if (await fs.pathExists(json_path)) { - youtube_id = await fs.readJSON(json_path).id; - await fs.unlink(json_path); - } else if (await fs.pathExists(alternate_json_path)) { - youtube_id = await fs.readJSON(alternate_json_path).id; - await fs.unlink(alternate_json_path); - } - - await fs.unlink(full_path); - - // do archive stuff - - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`); - - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (await fs.pathExists(archive_path)) { - const line = youtube_id ? await subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null; - if (blacklistMode && line) { - let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`); - // adds newline to the beginning of the line - line = '\n' + line; - await fs.appendFile(blacklistPath, line); - } - } else { - logger.info(`Could not find archive file for ${type} files. Creating...`); - await fs.ensureFile(archive_path); - } - } - } - success = true; - } else { - success = false; - logger.warn(`User file ${file_uid} does not exist!`); - } - - return success; -} - exports.changeSharingMode = function(user_uid, file_uid, is_playlist, enabled) { let success = false; const user_db_obj = users_db.get('users').find({uid: user_uid}); diff --git a/backend/categories.js b/backend/categories.js index 2134373..ce56d5c 100644 --- a/backend/categories.js +++ b/backend/categories.js @@ -1,4 +1,5 @@ const config_api = require('./config'); +const utils = require('./utils'); var logger = null; var db = null; @@ -68,6 +69,24 @@ function getCategories() { return categories ? categories : null; } +function getCategoriesAsPlaylists(files = null) { + const categories_as_playlists = []; + const available_categories = getCategories(); + if (available_categories && files) { + for (category of available_categories) { + const files_that_match = utils.addUIDsToCategory(category, files); + if (files_that_match && files_that_match.length > 0) { + category['thumbnailURL'] = files_that_match[0].thumbnailURL; + category['thumbnailPath'] = files_that_match[0].thumbnailPath; + category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); + category['id'] = category['uid']; + categories_as_playlists.push(category); + } + } + } + return categories_as_playlists; +} + function applyCategoryRules(file_json, rules, category_name) { let rules_apply = false; for (let i = 0; i < rules.length; i++) { @@ -126,4 +145,6 @@ async function addTagToExistingTags(tag) { module.exports = { initialize: initialize, categorize: categorize, + getCategories: getCategories, + getCategoriesAsPlaylists: getCategoriesAsPlaylists } \ No newline at end of file diff --git a/backend/db.js b/backend/db.js index 106c3f2..719161c 100644 --- a/backend/db.js +++ b/backend/db.js @@ -53,14 +53,14 @@ exports.registerFileDB = (file_path, type, multiUserMode = null, sub = null, cus } } - const file_uid = registerFileDBManual(db_path, file_object); + const file_obj = registerFileDBManual(db_path, file_object); // remove metadata JSON if needed if (!config_api.getConfigItem('ytdl_include_metadata')) { utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path) } - return file_uid; + return file_obj; } function registerFileDBManual(db_path, file_object) { @@ -75,7 +75,7 @@ function registerFileDBManual(db_path, file_object) { // add new file to db db_path.push(file_object).write(); - return file_object['uid']; + return file_object; } function generateFileObject(id, type, customPath = null, sub = null) { @@ -224,17 +224,47 @@ exports.preimportUnregisteredSubscriptionFile = async (sub, appendedBasePath) => return preimported_file_paths; } +exports.createPlaylist = async (playlist_name, uids, type, thumbnail_url, user_uid = null) => { + let new_playlist = { + name: playlist_name, + uids: uids, + id: uuid(), + thumbnailURL: thumbnail_url, + type: type, + registered: Date.now(), + }; + + const duration = await exports.calculatePlaylistDuration(new_playlist, user_uid); + new_playlist.duration = duration; + + if (user_uid) { + users_db.get('users').find({uid: user_uid}).get(`playlists`).push(new_playlist).write(); + } else { + db.get(`playlists`) + .push(new_playlist) + .write(); + } + + return new_playlist; +} + exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => { let playlist = null if (user_uid) { playlist = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlist_id}).value(); - - // prevent unauthorized users from accessing the file info - if (require_sharing && !playlist['sharingEnabled']) return null; } else { playlist = db.get(`playlists`).find({id: playlist_id}).value(); } + if (!playlist) { + playlist = db.get('categories').find({uid: playlist_id}).value(); + if (playlist) { + // category found + const files = await exports.getFiles(user_uid); + utils.addUIDsToCategory(playlist, files); + } + } + // converts playlists to new UID-based schema if (playlist && playlist['fileNames'] && !playlist['uids']) { playlist['uids'] = []; @@ -248,11 +278,18 @@ exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = fal exports.updatePlaylist(playlist, user_uid); } + // prevent unauthorized users from accessing the file info + if (require_sharing && !playlist['sharingEnabled']) return null; + return playlist; } -exports.updatePlaylist = (playlist, user_uid = null) => { +exports.updatePlaylist = async (playlist, user_uid = null) => { let playlistID = playlist.id; + + const duration = await exports.calculatePlaylistDuration(playlist, user_uid); + playlist.duration = duration; + let db_loc = null; if (user_uid) { db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}); @@ -263,6 +300,103 @@ exports.updatePlaylist = (playlist, user_uid = null) => { return true; } +exports.calculatePlaylistDuration = async (playlist, uuid, playlist_file_objs = null) => { + if (!playlist_file_objs) { + playlist_file_objs = []; + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const file_obj = await exports.getVideo(uid, uuid); + if (file_obj) playlist_file_objs.push(file_obj); + } + } + + return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); +} + +exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => { + const file_obj = await exports.getVideo(uid, uuid); + const type = file_obj.isAudio ? 'audio' : 'video'; + const folderPath = path.dirname(file_obj.path); + const ext = type === 'audio' ? 'mp3' : 'mp4'; + const name = file_obj.id; + const filePathNoExtension = utils.removeFileExtension(file_obj.path); + + var jsonPath = `${file_obj.path}.info.json`; + var altJSONPath = `${filePathNoExtension}.info.json`; + var thumbnailPath = `${filePathNoExtension}.webp`; + var altThumbnailPath = `${filePathNoExtension}.jpg`; + + jsonPath = path.join(__dirname, jsonPath); + altJSONPath = path.join(__dirname, altJSONPath); + + let jsonExists = await fs.pathExists(jsonPath); + let thumbnailExists = await fs.pathExists(thumbnailPath); + + if (!jsonExists) { + if (await fs.pathExists(altJSONPath)) { + jsonExists = true; + jsonPath = altJSONPath; + } + } + + if (!thumbnailExists) { + if (await fs.pathExists(altThumbnailPath)) { + thumbnailExists = true; + thumbnailPath = altThumbnailPath; + } + } + + let fileExists = await fs.pathExists(file_obj.path); + + if (config_api.descriptors[uid]) { + try { + for (let i = 0; i < config_api.descriptors[uid].length; i++) { + config_api.descriptors[uid][i].destroy(); + } + } catch(e) { + + } + } + + let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + if (useYoutubeDLArchive) { + const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`); + + // get ID from JSON + + var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath)); + let id = null; + if (jsonobj) id = jsonobj.id; + + // use subscriptions API to remove video from the archive file, and write it to the blacklist + if (await fs.pathExists(archive_path)) { + const line = id ? await utils.removeIDFromArchive(archive_path, id) : null; + if (blacklistMode && line) await writeToBlacklist(type, line); + } else { + logger.info('Could not find archive file for audio files. Creating...'); + await fs.close(await fs.open(archive_path, 'w')); + } + } + + if (jsonExists) await fs.unlink(jsonPath); + if (thumbnailExists) await fs.unlink(thumbnailPath); + + const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; + base_db_path.get('files').remove({uid: uid}).write(); + + if (fileExists) { + await fs.unlink(file_obj.path); + if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) { + return false; + } else { + return true; + } + } else { + // TODO: tell user that the file didn't exist + return true; + } +} + // Video ID is basically just the file name without the base path and file extension - this method helps us get away from that exports.getVideoUIDByID = (file_id, uuid = null) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; @@ -270,12 +404,17 @@ exports.getVideoUIDByID = (file_id, uuid = null) => { return file_obj ? file_obj['uid'] : null; } -exports.getVideo = async (file_uid, uuid, sub_id) => { +exports.getVideo = async (file_uid, uuid = null, sub_id = null) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); return sub_db_path.find({uid: file_uid}).value(); } +exports.getFiles = async (uuid = null) => { + const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; + return base_db_path.get('files').value(); +} + exports.setVideoProperty = async (file_uid, assignment_obj, uuid, sub_id) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 7ff1d06..8f29cae 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -243,7 +243,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, const archive_path = path.join(sub.archive, 'archive.txt') // if archive exists, remove line with video ID if (await fs.pathExists(archive_path)) { - await removeIDFromArchive(archive_path, retrievedID); + utils.removeIDFromArchive(archive_path, retrievedID); } } return true; @@ -597,33 +597,6 @@ function getAppendedBasePath(sub, base_path) { return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name); } -async function removeIDFromArchive(archive_path, id) { - let data = await fs.readFile(archive_path, {encoding: 'utf-8'}); - if (!data) { - logger.error('Archive could not be found.'); - return; - } - - let dataArray = data.split('\n'); // convert file data in an array - const searchKeyword = id; // we are looking for a line, contains, key word id in the file - let lastIndex = -1; // let say, we have not found the keyword - - for (let index=0; index= 0; i--) { + num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i)); + } + return num_sum; +} + +function getMatchingCategoryFiles(category, files) { + return files && files.filter(file => file.category && file.category.uid === category.uid); +} + +function addUIDsToCategory(category, files) { + const files_that_match = getMatchingCategoryFiles(category, files); + category['uids'] = files_that_match.map(file => file.uid); + return files_that_match; +} async function recFindByExt(base,ext,files,result) { @@ -268,8 +314,12 @@ module.exports = { getExpectedFileSize: getExpectedFileSize, fixVideoMetadataPerms: fixVideoMetadataPerms, deleteJSONFile: deleteJSONFile, + removeIDFromArchive, removeIDFromArchive, getDownloadedFilesByType: getDownloadedFilesByType, createContainerZipFile: createContainerZipFile, + durationStringToNumber: durationStringToNumber, + getMatchingCategoryFiles: getMatchingCategoryFiles, + addUIDsToCategory: addUIDsToCategory, recFindByExt: recFindByExt, removeFileExtension: removeFileExtension, wait: wait, diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 69c7908..3914586 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -53,14 +53,12 @@ export class CustomPlaylistsComponent implements OnInit { goToPlaylist(info_obj) { const playlist = info_obj.file; const playlistID = playlist.id; - const type = playlist.type; if (playlist) { if (this.postsService.config['Extra']['download_only_mode']) { this.downloadPlaylist(playlist.id, playlist.name); } else { localStorage.setItem('player_navigator', this.router.url); - const fileNames = playlist.fileNames; this.router.navigate(['/player', {playlist_id: playlistID, auto: playlist.auto}]); } } else { diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 31ed771..05deb2b 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -221,7 +221,7 @@ export class RecentVideosComponent implements OnInit { if (!this.postsService.config.Extra.file_manager_enabled) { // tell server to delete the file once downloaded - this.postsService.deleteFile(name, type).subscribe(delRes => { + this.postsService.deleteFile(file.uid).subscribe(delRes => { // reload mp4s this.getAllFiles(); }); diff --git a/src/app/create-playlist/create-playlist.component.ts b/src/app/create-playlist/create-playlist.component.ts index b9cf976..c22d32d 100644 --- a/src/app/create-playlist/create-playlist.component.ts +++ b/src/app/create-playlist/create-playlist.component.ts @@ -51,9 +51,8 @@ export class CreatePlaylistComponent implements OnInit { createPlaylist() { const thumbnailURL = this.getThumbnailURL(); - const duration = this.calculateDuration(); this.create_in_progress = true; - this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL, duration).subscribe(res => { + this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => { this.create_in_progress = false; if (res['success']) { this.dialogRef.close(true); @@ -78,36 +77,4 @@ export class CreatePlaylistComponent implements OnInit { } return null; } - - getDuration(file_id) { - let properFilesToSelectFrom = this.filesToSelectFrom; - if (!this.filesToSelectFrom) { - properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom; - } - for (let i = 0; i < properFilesToSelectFrom.length; i++) { - const file = properFilesToSelectFrom[i]; - if (file.id === file_id) { - return file.duration; - } - } - return null; - } - - calculateDuration() { - let sum = 0; - for (let i = 0; i < this.filesSelect.value.length; i++) { - const duration_val = this.getDuration(this.filesSelect.value[i]); - sum += typeof duration_val === 'string' ? this.durationStringToNumber(duration_val) : duration_val; - } - return sum; - } - - durationStringToNumber(dur_str) { - let num_sum = 0; - const dur_str_parts = dur_str.split(':'); - for (let i = dur_str_parts.length-1; i >= 0; i--) { - num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i)); - } - return num_sum; - } } diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.html b/src/app/dialogs/share-media-dialog/share-media-dialog.component.html index fcd8f3c..7175b52 100644 --- a/src/app/dialogs/share-media-dialog/share-media-dialog.component.html +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.html @@ -1,7 +1,6 @@

Share playlist - Share video - Share audio + Share file

diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts index 9b687ff..332a461 100644 --- a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts @@ -11,7 +11,6 @@ import { PostsService } from 'app/posts.services'; }) export class ShareMediaDialogComponent implements OnInit { - type = null; uid = null; uuid = null; share_url = null; @@ -26,7 +25,6 @@ export class ShareMediaDialogComponent implements OnInit { ngOnInit(): void { if (this.data) { - this.type = this.data.type; this.uid = this.data.uid; this.uuid = this.data.uuid; this.sharing_enabled = this.data.sharing_enabled; @@ -65,7 +63,7 @@ export class ShareMediaDialogComponent implements OnInit { sharingChanged(event) { if (event.checked) { - this.postsService.enableSharing(this.uid, this.type, this.is_playlist).subscribe(res => { + this.postsService.enableSharing(this.uid, this.is_playlist).subscribe(res => { if (res['success']) { this.openSnackBar('Sharing enabled.'); this.sharing_enabled = true; @@ -76,7 +74,7 @@ export class ShareMediaDialogComponent implements OnInit { this.openSnackBar('Failed to enable sharing - server error.'); }); } else { - this.postsService.disableSharing(this.uid, this.type, this.is_playlist).subscribe(res => { + this.postsService.disableSharing(this.uid, this.is_playlist).subscribe(res => { if (res['success']) { this.openSnackBar('Sharing disabled.'); this.sharing_enabled = false; diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index cd1e3ab..bf99b73 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -342,12 +342,8 @@ export class MainComponent implements OnInit { } } - public goToFile(name, isAudio, uid) { - if (isAudio) { - this.downloadHelperMp3(name, uid, false, false, null, true); - } else { - this.downloadHelperMp4(name, uid, false, false, null, true); - } + public goToFile(container, isAudio, uid) { + this.downloadHelper(container, isAudio ? 'audio' : 'video', false, false, null, true); } public goToPlaylist(playlistID, type) { @@ -379,56 +375,26 @@ export class MainComponent implements OnInit { // download helpers - downloadHelperMp3(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) { + downloadHelper(container, type, is_playlist = false, force_view = false, new_download = null, navigate_mode = false) { this.downloadingfile = false; if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) { // do nothing this.reloadRecentVideos(); } else { // if download only mode, just download the file. no redirect - if (forceView === false && this.downloadOnlyMode && !this.iOS) { + if (force_view === false && this.downloadOnlyMode && !this.iOS) { if (is_playlist) { - const zipName = name[0].split(' ')[0] + name[1].split(' ')[0]; - this.downloadPlaylist(name, 'audio', zipName); + this.downloadPlaylist(container['uid']); } else { - this.downloadAudioFile(decodeURI(name)); + this.downloadFileFromServer(container, type); } this.reloadRecentVideos(); } else { localStorage.setItem('player_navigator', this.router.url.split(';')[0]); if (is_playlist) { - this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]); + this.router.navigate(['/player', {playlist_id: container['id'], type: type}]); } else { - this.router.navigate(['/player', {type: 'audio', uid: uid}]); - } - } - } - - // remove download from current downloads - this.removeDownloadFromCurrentDownloads(new_download); - } - - downloadHelperMp4(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) { - this.downloadingfile = false; - if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) { - // do nothing - this.reloadRecentVideos(); - } else { - // if download only mode, just download the file. no redirect - if (forceView === false && this.downloadOnlyMode) { - if (is_playlist) { - const zipName = name[0].split(' ')[0] + name[1].split(' ')[0]; - this.downloadPlaylist(name, 'video', zipName); - } else { - this.downloadVideoFile(decodeURI(name)); - } - this.reloadRecentVideos(); - } else { - localStorage.setItem('player_navigator', this.router.url.split(';')[0]); - if (is_playlist) { - this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]); - } else { - this.router.navigate(['/player', {type: 'video', uid: uid}]); + this.router.navigate(['/player', {type: type, uid: container['uid']}]); } } } @@ -439,133 +405,85 @@ export class MainComponent implements OnInit { // download click handler downloadClicked() { - if (this.ValidURL(this.url)) { - this.urlError = false; - this.path = ''; - - // get common args - const customArgs = (this.customArgsEnabled ? this.customArgs : null); - const customOutput = (this.customOutputEnabled ? this.customOutput : null); - const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null); - const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null); - - // set advanced inputs - if (this.allowAdvancedDownload) { - if (customArgs) { - localStorage.setItem('customArgs', customArgs); - } - if (customOutput) { - localStorage.setItem('customOutput', customOutput); - } - if (youtubeUsername) { - localStorage.setItem('youtubeUsername', youtubeUsername); - } - } - - if (this.audioOnly) { - // create download object - const new_download: Download = { - uid: uuid(), - type: 'audio', - percent_complete: 0, - url: this.url, - downloading: true, - is_playlist: this.url.includes('playlist'), - error: false - }; - this.downloads.push(new_download); - if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; - this.downloadingfile = true; - - let customQualityConfiguration = null; - if (this.selectedQuality !== '') { - customQualityConfiguration = this.getSelectedAudioFormat(); - } - - this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality), - customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid).subscribe(posts => { - // update download object - new_download.downloading = false; - new_download.percent_complete = 100; - - const is_playlist = !!(posts['file_names']); - this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded']; - - this.current_download = null; - - if (this.path !== '-1') { - this.downloadHelperMp3(this.path, posts['uid'], is_playlist, false, new_download); - } - }, error => { // can't access server or failed to download for other reasons - this.downloadingfile = false; - this.current_download = null; - new_download['downloading'] = false; - // removes download from list of downloads - const downloads_index = this.downloads.indexOf(new_download); - if (downloads_index !== -1) { - this.downloads.splice(downloads_index) - } - this.openSnackBar('Download failed!', 'OK.'); - }); - } else { - // create download object - const new_download: Download = { - uid: uuid(), - type: 'video', - percent_complete: 0, - url: this.url, - downloading: true, - is_playlist: this.url.includes('playlist'), - error: false - }; - this.downloads.push(new_download); - if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; - this.downloadingfile = true; - - const customQualityConfiguration = this.getSelectedVideoFormat(); - - let cropFileSettings = null; - - if (this.cropFile) { - cropFileSettings = { - cropFileStart: this.cropFileStart, - cropFileEnd: this.cropFileEnd - } - } - - this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality), - customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(posts => { - // update download object - new_download.downloading = false; - new_download.percent_complete = 100; - - const is_playlist = !!(posts['file_names']); - this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded']; - - this.current_download = null; - - if (this.path !== '-1') { - this.downloadHelperMp4(this.path, posts['uid'], is_playlist, false, new_download); - } - }, error => { // can't access server - this.downloadingfile = false; - this.current_download = null; - new_download['downloading'] = false; - // removes download from list of downloads - const downloads_index = this.downloads.indexOf(new_download); - if (downloads_index !== -1) { - this.downloads.splice(downloads_index) - } - this.openSnackBar('Download failed!', 'OK.'); - }); - } - - if (this.multiDownloadMode) { - this.url = ''; - this.downloadingfile = false; - } - } else { + if (!this.ValidURL(this.url)) { this.urlError = true; + return; + } + + this.urlError = false; + + // get common args + const customArgs = (this.customArgsEnabled ? this.customArgs : null); + const customOutput = (this.customOutputEnabled ? this.customOutput : null); + const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null); + const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null); + + // set advanced inputs + if (this.allowAdvancedDownload) { + if (customArgs) { + localStorage.setItem('customArgs', customArgs); + } + if (customOutput) { + localStorage.setItem('customOutput', customOutput); + } + if (youtubeUsername) { + localStorage.setItem('youtubeUsername', youtubeUsername); + } + } + + const type = this.audioOnly ? 'audio' : 'video'; + // create download object + const new_download: Download = { + uid: uuid(), + type: type, + percent_complete: 0, + url: this.url, + downloading: true, + is_playlist: this.url.includes('playlist'), + error: false + }; + this.downloads.push(new_download); + if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; + this.downloadingfile = true; + + let customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat(); + + let cropFileSettings = null; + + if (this.cropFile) { + cropFileSettings = { + cropFileStart: this.cropFileStart, + cropFileEnd: this.cropFileEnd + } + } + + this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality), + customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(res => { + // update download object + new_download.downloading = false; + new_download.percent_complete = 100; + + const container = res['container']; + const is_playlist = res['file_uids'].length > 1; + + this.current_download = null; + + this.downloadHelper(container, type, is_playlist, false, new_download); + }, error => { // can't access server + this.downloadingfile = false; + this.current_download = null; + new_download['downloading'] = false; + // removes download from list of downloads + const downloads_index = this.downloads.indexOf(new_download); + if (downloads_index !== -1) { + this.downloads.splice(downloads_index) + } + this.openSnackBar('Download failed!', 'OK.'); + }); + + if (this.multiDownloadMode) { + this.url = ''; + this.downloadingfile = false; } } @@ -626,27 +544,13 @@ export class MainComponent implements OnInit { } } - downloadAudioFile(file) { - this.downloading_content['audio'][file.id] = true; + downloadFileFromServer(file, type) { + const ext = type === 'audio' ? 'mp3' : 'mp4' + this.downloading_content[type][file.id] = true; this.postsService.downloadFileFromServer(file.uid).subscribe(res => { - this.downloading_content['audio'][file.id] = false; + this.downloading_content[type][file.id] = false; const blob: Blob = res; - saveAs(blob, decodeURIComponent(file.id) + '.mp3'); - - if (!this.fileManagerEnabled) { - // tell server to delete the file once downloaded - this.postsService.deleteFile(file.uid).subscribe(delRes => { - }); - } - }); - } - - downloadVideoFile(file) { - this.downloading_content['video'][file.id] = true; - this.postsService.downloadFileFromServer(file.uid).subscribe(res => { - this.downloading_content['video'][file.id] = false; - const blob: Blob = res; - saveAs(blob, decodeURIComponent(file.id) + '.mp4'); + saveAs(blob, decodeURIComponent(file.id) + `.${ext}`); if (!this.fileManagerEnabled) { // tell server to delete the file once downloaded diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 9de791a..fc8dbac 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -1,10 +1,10 @@
-
+
-
- -