From 46f85794391d981123b30ce7efd885af344f55f6 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 22:56:16 -0600 Subject: [PATCH] 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() {