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 @@