mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
1 Commits
v4.2
...
youtube-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c28f8dd48 |
@@ -261,12 +261,12 @@ paths:
|
||||
$ref: '#/components/schemas/inline_response_200_10'
|
||||
security:
|
||||
- Auth query parameter: []
|
||||
/api/getSubscriptions:
|
||||
/api/getAllSubscriptions:
|
||||
post:
|
||||
tags:
|
||||
- subscriptions
|
||||
summary: Get all subscriptions
|
||||
operationId: post-api-getSubscriptions
|
||||
operationId: post-api-getAllSubscriptions
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
||||
|
||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 11](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||
|
||||
Now with [Docker](#Docker) support!
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
|
||||
427
backend/app.js
427
backend/app.js
@@ -13,7 +13,7 @@ var express = require("express");
|
||||
var bodyParser = require("body-parser");
|
||||
var archiver = require('archiver');
|
||||
var unzipper = require('unzipper');
|
||||
var db_api = require('./db');
|
||||
var db_api = require('./db')
|
||||
var utils = require('./utils')
|
||||
var mergeFiles = require('merge-files');
|
||||
const low = require('lowdb')
|
||||
@@ -27,7 +27,6 @@ const url_api = require('url');
|
||||
var config_api = require('./config.js');
|
||||
var subscriptions_api = require('./subscriptions')
|
||||
var categories_api = require('./categories');
|
||||
var twitch_api = require('./twitch');
|
||||
const CONSTS = require('./consts')
|
||||
const { spawn } = require('child_process')
|
||||
const read_last_lines = require('read-last-lines');
|
||||
@@ -39,7 +38,6 @@ var app = express();
|
||||
|
||||
// database setup
|
||||
const FileSync = require('lowdb/adapters/FileSync');
|
||||
const config = require('./config.js');
|
||||
|
||||
const adapter = new FileSync('./appdata/db.json');
|
||||
const db = low(adapter)
|
||||
@@ -79,7 +77,7 @@ const logger = winston.createLogger({
|
||||
});
|
||||
|
||||
config_api.initialize(logger);
|
||||
auth_api.initialize(db, users_db, logger);
|
||||
auth_api.initialize(users_db, logger);
|
||||
db_api.initialize(db, users_db, logger);
|
||||
subscriptions_api.initialize(db, users_db, logger, db_api);
|
||||
categories_api.initialize(db, users_db, logger, db_api);
|
||||
@@ -87,8 +85,14 @@ categories_api.initialize(db, users_db, logger, db_api);
|
||||
// Set some defaults
|
||||
db.defaults(
|
||||
{
|
||||
playlists: [],
|
||||
files: [],
|
||||
playlists: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
files: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
configWriteFlag: false,
|
||||
downloads: {},
|
||||
subscriptions: [],
|
||||
@@ -210,20 +214,6 @@ async function checkMigrations() {
|
||||
else { logger.error('Migration failed: 3.5->3.6+'); }
|
||||
}
|
||||
|
||||
// 4.1->4.2 migration
|
||||
|
||||
const simplified_db_migration_complete = db.get('simplified_db_migration_complete').value();
|
||||
if (!simplified_db_migration_complete) {
|
||||
logger.info('Beginning migration: 4.1->4.2+')
|
||||
let success = await simplifyDBFileStructure();
|
||||
success = success && await addMetadataPropertyToDB('view_count');
|
||||
success = success && await addMetadataPropertyToDB('description');
|
||||
success = success && await addMetadataPropertyToDB('height');
|
||||
success = success && await addMetadataPropertyToDB('abr');
|
||||
if (success) { logger.info('4.1->4.2+ migration complete!'); }
|
||||
else { logger.error('Migration failed: 4.1->4.2+'); }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -259,65 +249,6 @@ async function runFilesToDBMigration() {
|
||||
}
|
||||
}
|
||||
|
||||
async function simplifyDBFileStructure() {
|
||||
// back up db files
|
||||
const old_db_file = fs.readJSONSync('./appdata/db.json');
|
||||
const old_users_db_file = fs.readJSONSync('./appdata/users.json');
|
||||
fs.writeJSONSync('appdata/db.old.json', old_db_file);
|
||||
fs.writeJSONSync('appdata/users.old.json', old_users_db_file);
|
||||
|
||||
// simplify
|
||||
let users = users_db.get('users').value();
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i];
|
||||
if (user['files']['video'] !== undefined && user['files']['audio'] !== undefined) {
|
||||
const user_files = user['files']['video'].concat(user['files']['audio']);
|
||||
const user_db_path = users_db.get('users').find({uid: user['uid']});
|
||||
user_db_path.assign({files: user_files}).write();
|
||||
}
|
||||
if (user['playlists']['video'] !== undefined && user['playlists']['audio'] !== undefined) {
|
||||
const user_playlists = user['playlists']['video'].concat(user['playlists']['audio']);
|
||||
const user_db_path = users_db.get('users').find({uid: user['uid']});
|
||||
user_db_path.assign({playlists: user_playlists}).write();
|
||||
}
|
||||
}
|
||||
|
||||
if (db.get('files.video').value() !== undefined && db.get('files.audio').value() !== undefined) {
|
||||
const files = db.get('files.video').value().concat(db.get('files.audio').value());
|
||||
db.assign({files: files}).write();
|
||||
}
|
||||
|
||||
if (db.get('playlists.video').value() !== undefined && db.get('playlists.audio').value() !== undefined) {
|
||||
const playlists = db.get('playlists.video').value().concat(db.get('playlists.audio').value());
|
||||
db.assign({playlists: playlists}).write();
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function addMetadataPropertyToDB(property_key) {
|
||||
try {
|
||||
const dirs_to_check = db_api.getFileDirectoriesAndDBs();
|
||||
for (const dir_to_check of dirs_to_check) {
|
||||
// recursively get all files in dir's path
|
||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
|
||||
for (const file of files) {
|
||||
if (file[property_key]) {
|
||||
dir_to_check.dbPath.find({id: file.id}).assign({[property_key]: file[property_key]}).write();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sets migration to complete
|
||||
db.set('simplified_db_migration_complete', true).write();
|
||||
return true;
|
||||
} catch(err) {
|
||||
logger.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
if (process.env.USING_HEROKU && process.env.PORT) {
|
||||
// default to heroku port if using heroku
|
||||
@@ -627,17 +558,11 @@ async function loadConfig() {
|
||||
// creates archive path if missing
|
||||
await fs.ensureDir(archivePath);
|
||||
|
||||
// check migrations
|
||||
await checkMigrations();
|
||||
|
||||
// now this is done here due to youtube-dl's repo takedown
|
||||
await startYoutubeDL();
|
||||
|
||||
// get subscriptions
|
||||
if (allowSubscriptions) {
|
||||
// set downloading to false
|
||||
let subscriptions = subscriptions_api.getAllSubscriptions();
|
||||
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
|
||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||
watchSubscriptions();
|
||||
setInterval(() => {
|
||||
@@ -647,6 +572,9 @@ async function loadConfig() {
|
||||
|
||||
db_api.importUnregisteredFiles();
|
||||
|
||||
// check migrations
|
||||
await checkMigrations();
|
||||
|
||||
// load in previous downloads
|
||||
downloads = db.get('downloads').value();
|
||||
|
||||
@@ -696,20 +624,27 @@ function calculateSubcriptionRetrievalDelay(subscriptions_amount) {
|
||||
}
|
||||
|
||||
async function watchSubscriptions() {
|
||||
let subscriptions = subscriptions_api.getAllSubscriptions();
|
||||
let subscriptions = null;
|
||||
|
||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
if (multiUserMode) {
|
||||
subscriptions = [];
|
||||
let users = users_db.get('users').value();
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i]['subscriptions']) subscriptions = subscriptions.concat(users[i]['subscriptions']);
|
||||
}
|
||||
} else {
|
||||
subscriptions = subscriptions_api.getAllSubscriptions();
|
||||
}
|
||||
|
||||
if (!subscriptions) return;
|
||||
|
||||
const valid_subscriptions = subscriptions.filter(sub => !sub.paused);
|
||||
|
||||
let subscriptions_amount = valid_subscriptions.length;
|
||||
let subscriptions_amount = subscriptions.length;
|
||||
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
||||
|
||||
let current_delay = 0;
|
||||
|
||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
for (let i = 0; i < valid_subscriptions.length; i++) {
|
||||
let sub = valid_subscriptions[i];
|
||||
for (let i = 0; i < subscriptions.length; i++) {
|
||||
let sub = subscriptions[i];
|
||||
|
||||
// don't check the sub if the last check for the same subscription has not completed
|
||||
if (subscription_timeouts[sub.id]) {
|
||||
@@ -763,6 +698,64 @@ function generateEnvVarConfigItem(key) {
|
||||
return {key: key, value: process['env'][key]};
|
||||
}
|
||||
|
||||
async function getMp3s() {
|
||||
let mp3s = [];
|
||||
var files = await utils.recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
var file_path = file.substring(audioFolderPath.length, file.length);
|
||||
|
||||
var stats = await fs.stat(file);
|
||||
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = await utils.getJSONMp3(id, audioFolderPath);
|
||||
if (!jsonobj) continue;
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = true;
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
||||
mp3s.push(file_obj);
|
||||
}
|
||||
return mp3s;
|
||||
}
|
||||
|
||||
async function getMp4s(relative_path = true) {
|
||||
let mp4s = [];
|
||||
var files = await utils.recFindByExt(videoFolderPath, 'mp4');
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
var file_path = file.substring(videoFolderPath.length, file.length);
|
||||
|
||||
var stats = fs.statSync(file);
|
||||
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = await utils.getJSONMp4(id, videoFolderPath);
|
||||
if (!jsonobj) continue;
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var isaudio = false;
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
||||
mp4s.push(file_obj);
|
||||
}
|
||||
return mp4s;
|
||||
}
|
||||
|
||||
function getThumbnailMp3(name)
|
||||
{
|
||||
var obj = utils.getJSONMp3(name, audioFolderPath);
|
||||
@@ -1126,10 +1119,10 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
|
||||
// get video info prior to download
|
||||
let info = await getVideoInfoByURL(url, downloadConfig, download);
|
||||
if (!info && url.includes('youtu')) {
|
||||
if (!info) {
|
||||
resolve(false);
|
||||
return;
|
||||
} else if (info) {
|
||||
} else {
|
||||
// check if it fits into a category. If so, then get info again using new downloadConfig
|
||||
category = await categories_api.categorize(info);
|
||||
|
||||
@@ -1193,13 +1186,6 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
var full_file_path = filepath_no_extension + ext;
|
||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||
|
||||
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||
&& config.getConfigItem('ytdl_use_twitch_api') && config.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||
let vodId = url.split('twitch.tv/videos/')[1];
|
||||
vodId = vodId.split('?')[0];
|
||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, options.user);
|
||||
}
|
||||
|
||||
// renames file if necessary due to bug
|
||||
if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) {
|
||||
try {
|
||||
@@ -1222,7 +1208,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
const customPath = options.noRelativePath ? path.dirname(full_file_path).split(path.sep).pop() : null;
|
||||
|
||||
// registers file in DB
|
||||
file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath, category);
|
||||
file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath);
|
||||
|
||||
if (file_name) file_names.push(file_name);
|
||||
}
|
||||
@@ -1510,10 +1496,6 @@ async function generateArgs(url, type, options) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// filter out incompatible args
|
||||
downloadConfig = filterArgs(downloadConfig, is_audio);
|
||||
|
||||
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
||||
return downloadConfig;
|
||||
}
|
||||
@@ -1544,13 +1526,6 @@ async function getVideoInfoByURL(url, args = [], download = null) {
|
||||
});
|
||||
}
|
||||
|
||||
function filterArgs(args, isAudio) {
|
||||
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
|
||||
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
|
||||
const args_to_remove = isAudio ? video_only_args : audio_only_args;
|
||||
return args.filter(x => !args_to_remove.includes(x));
|
||||
}
|
||||
|
||||
// currently only works for single urls
|
||||
async function getUrlInfos(urls) {
|
||||
let startDate = Date.now();
|
||||
@@ -1942,8 +1917,8 @@ async function addThumbnails(files) {
|
||||
|
||||
// gets all download mp3s
|
||||
app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
||||
var mp3s = db.get('files').value().filter(file => file.isAudio === true);
|
||||
var playlists = db.get('playlists').value();
|
||||
var mp3s = db.get('files.audio').value(); // getMp3s();
|
||||
var playlists = db.get('playlists.audio').value();
|
||||
const is_authenticated = req.isAuthenticated();
|
||||
if (is_authenticated) {
|
||||
// get user audio files/playlists
|
||||
@@ -1954,6 +1929,12 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
||||
|
||||
mp3s = JSON.parse(JSON.stringify(mp3s));
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
// await addThumbnails(mp3s);
|
||||
}
|
||||
|
||||
|
||||
res.send({
|
||||
mp3s: mp3s,
|
||||
playlists: playlists
|
||||
@@ -1962,8 +1943,8 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
||||
|
||||
// gets all download mp4s
|
||||
app.get('/api/getMp4s', optionalJwt, async function(req, res) {
|
||||
var mp4s = db.get('files').value().filter(file => file.isAudio === false);
|
||||
var playlists = db.get('playlists').value();
|
||||
var mp4s = db.get('files.video').value(); // getMp4s();
|
||||
var playlists = db.get('playlists.video').value();
|
||||
|
||||
const is_authenticated = req.isAuthenticated();
|
||||
if (is_authenticated) {
|
||||
@@ -1975,6 +1956,11 @@ app.get('/api/getMp4s', optionalJwt, async function(req, res) {
|
||||
|
||||
mp4s = JSON.parse(JSON.stringify(mp4s));
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
// await addThumbnails(mp4s);
|
||||
}
|
||||
|
||||
res.send({
|
||||
mp4s: mp4s,
|
||||
playlists: playlists
|
||||
@@ -1989,15 +1975,23 @@ app.post('/api/getFile', optionalJwt, function (req, res) {
|
||||
var file = null;
|
||||
|
||||
if (req.isAuthenticated()) {
|
||||
file = auth_api.getUserVideo(req.user.uid, uid);
|
||||
file = auth_api.getUserVideo(req.user.uid, uid, type);
|
||||
} else if (uuid) {
|
||||
file = auth_api.getUserVideo(uuid, uid, true);
|
||||
file = auth_api.getUserVideo(uuid, uid, type, true);
|
||||
} else {
|
||||
file = db.get('files').find({uid: uid}).value();
|
||||
if (!type) {
|
||||
file = db.get('files.audio').find({uid: uid}).value();
|
||||
if (!file) {
|
||||
file = db.get('files.video').find({uid: uid}).value();
|
||||
if (file) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (!file && type) file = db.get(`files.${type}`).find({uid: uid}).value();
|
||||
}
|
||||
|
||||
// check if chat exists for twitch videos
|
||||
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
|
||||
|
||||
if (file) {
|
||||
res.send({
|
||||
@@ -2013,47 +2007,32 @@ app.post('/api/getFile', optionalJwt, function (req, res) {
|
||||
|
||||
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
// these are returned
|
||||
let files = null;
|
||||
let playlists = null;
|
||||
let files = [];
|
||||
let playlists = [];
|
||||
let subscription_files = [];
|
||||
|
||||
let subscriptions = config_api.getConfigItem('ytdl_allow_subscriptions') ? (subscriptions_api.getSubscriptions(req.isAuthenticated() ? req.user.uid : null)) : [];
|
||||
let videos = null;
|
||||
let audios = null;
|
||||
let audio_playlists = null;
|
||||
let video_playlists = null;
|
||||
let subscriptions = config_api.getConfigItem('ytdl_allow_subscriptions') ? (subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null)) : [];
|
||||
|
||||
// get basic info depending on multi-user mode being enabled
|
||||
if (req.isAuthenticated()) {
|
||||
files = auth_api.getUserVideos(req.user.uid);
|
||||
playlists = auth_api.getUserPlaylists(req.user.uid, files);
|
||||
videos = auth_api.getUserVideos(req.user.uid, 'video');
|
||||
audios = auth_api.getUserVideos(req.user.uid, 'audio');
|
||||
audio_playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
|
||||
video_playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
|
||||
} 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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
videos = db.get('files.audio').value();
|
||||
audios = db.get('files.video').value();
|
||||
audio_playlists = db.get('playlists.audio').value();
|
||||
video_playlists = db.get('playlists.video').value();
|
||||
}
|
||||
|
||||
files = videos.concat(audios);
|
||||
playlists = video_playlists.concat(audio_playlists);
|
||||
|
||||
// loop through subscriptions and add videos
|
||||
for (let i = 0; i < subscriptions.length; i++) {
|
||||
sub = subscriptions[i];
|
||||
@@ -2079,55 +2058,16 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => {
|
||||
var id = req.body.id;
|
||||
var type = req.body.type;
|
||||
var uuid = req.body.uuid;
|
||||
var sub = req.body.sub;
|
||||
var user_uid = null;
|
||||
|
||||
if (req.isAuthenticated()) user_uid = req.user.uid;
|
||||
|
||||
const chat_file = await twitch_api.getTwitchChatByFileID(id, type, user_uid, uuid, sub);
|
||||
|
||||
res.send({
|
||||
chat: chat_file
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => {
|
||||
var id = req.body.id;
|
||||
var type = req.body.type;
|
||||
var vodId = req.body.vodId;
|
||||
var uuid = req.body.uuid;
|
||||
var sub = req.body.sub;
|
||||
var user_uid = null;
|
||||
|
||||
if (req.isAuthenticated()) user_uid = req.user.uid;
|
||||
|
||||
// check if file already exists. if so, send that instead
|
||||
const file_exists_check = await twitch_api.getTwitchChatByFileID(id, type, user_uid, uuid, sub);
|
||||
if (file_exists_check) {
|
||||
res.send({chat: file_exists_check});
|
||||
return;
|
||||
}
|
||||
|
||||
const full_chat = await twitch_api.downloadTwitchChatByVODID(vodId, id, type, user_uid, sub);
|
||||
|
||||
res.send({
|
||||
chat: full_chat
|
||||
});
|
||||
});
|
||||
|
||||
// video sharing
|
||||
app.post('/api/enableSharing', optionalJwt, function(req, res) {
|
||||
var type = req.body.type;
|
||||
var uid = req.body.uid;
|
||||
var is_playlist = req.body.is_playlist;
|
||||
let success = false;
|
||||
// multi-user mode
|
||||
if (req.isAuthenticated()) {
|
||||
// if multi user mode, use this method instead
|
||||
success = auth_api.changeSharingMode(req.user.uid, uid, is_playlist, true);
|
||||
success = auth_api.changeSharingMode(req.user.uid, uid, type, is_playlist, true);
|
||||
res.send({success: success});
|
||||
return;
|
||||
}
|
||||
@@ -2136,12 +2076,12 @@ app.post('/api/enableSharing', optionalJwt, function(req, res) {
|
||||
try {
|
||||
success = true;
|
||||
if (!is_playlist && type !== 'subscription') {
|
||||
db.get(`files`)
|
||||
db.get(`files.${type}`)
|
||||
.find({uid: uid})
|
||||
.assign({sharingEnabled: true})
|
||||
.write();
|
||||
} else if (is_playlist) {
|
||||
db.get(`playlists`)
|
||||
db.get(`playlists.${type}`)
|
||||
.find({id: uid})
|
||||
.assign({sharingEnabled: true})
|
||||
.write();
|
||||
@@ -2170,7 +2110,7 @@ app.post('/api/disableSharing', optionalJwt, function(req, res) {
|
||||
// multi-user mode
|
||||
if (req.isAuthenticated()) {
|
||||
// if multi user mode, use this method instead
|
||||
success = auth_api.changeSharingMode(req.user.uid, uid, is_playlist, false);
|
||||
success = auth_api.changeSharingMode(req.user.uid, uid, type, is_playlist, false);
|
||||
res.send({success: success});
|
||||
return;
|
||||
}
|
||||
@@ -2179,12 +2119,12 @@ app.post('/api/disableSharing', optionalJwt, function(req, res) {
|
||||
try {
|
||||
success = true;
|
||||
if (!is_playlist && type !== 'subscription') {
|
||||
db.get(`files`)
|
||||
db.get(`files.${type}`)
|
||||
.find({uid: uid})
|
||||
.assign({sharingEnabled: false})
|
||||
.write();
|
||||
} else if (is_playlist) {
|
||||
db.get(`playlists`)
|
||||
db.get(`playlists.${type}`)
|
||||
.find({id: uid})
|
||||
.assign({sharingEnabled: false})
|
||||
.write();
|
||||
@@ -2205,27 +2145,6 @@ app.post('/api/disableSharing', optionalJwt, function(req, res) {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/incrementViewCount', optionalJwt, async (req, res) => {
|
||||
let file_uid = req.body.file_uid;
|
||||
let sub_id = req.body.sub_id;
|
||||
let uuid = req.body.uuid;
|
||||
|
||||
if (!uuid && req.isAuthenticated()) {
|
||||
uuid = req.user.uid;
|
||||
}
|
||||
|
||||
const file_obj = await db_api.getVideo(file_uid, uuid, sub_id);
|
||||
|
||||
const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0;
|
||||
const new_view_count = current_view_count + 1;
|
||||
|
||||
await db_api.setVideoProperty(file_uid, {local_view_count: new_view_count}, uuid, sub_id);
|
||||
|
||||
res.send({
|
||||
success: true
|
||||
});
|
||||
});
|
||||
|
||||
// categories
|
||||
|
||||
app.post('/api/getAllCategories', optionalJwt, async (req, res) => {
|
||||
@@ -2418,18 +2337,12 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
||||
var size = stats.size;
|
||||
|
||||
var isaudio = false;
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date, jsonobj.description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
||||
parsed_files.push(file_obj);
|
||||
}
|
||||
} else {
|
||||
// loop through files for extra processing
|
||||
for (let i = 0; i < parsed_files.length; i++) {
|
||||
const file = parsed_files[i];
|
||||
// check if chat exists for twitch videos
|
||||
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.send({
|
||||
subscription: subscription,
|
||||
files: parsed_files
|
||||
@@ -2440,7 +2353,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
||||
if (subscription.videos) {
|
||||
for (let i = 0; i < subscription.videos.length; i++) {
|
||||
const video = subscription.videos[i];
|
||||
parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date, video.view_count, video.height, video.abr));
|
||||
parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date));
|
||||
}
|
||||
}
|
||||
res.send({
|
||||
@@ -2473,11 +2386,11 @@ app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/getSubscriptions', optionalJwt, async (req, res) => {
|
||||
app.post('/api/getAllSubscriptions', optionalJwt, async (req, res) => {
|
||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||
|
||||
// get subs from api
|
||||
let subscriptions = subscriptions_api.getSubscriptions(user_uid);
|
||||
let subscriptions = subscriptions_api.getAllSubscriptions(user_uid);
|
||||
|
||||
res.send({
|
||||
subscriptions: subscriptions
|
||||
@@ -2504,7 +2417,7 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
||||
if (req.isAuthenticated()) {
|
||||
auth_api.addPlaylist(req.user.uid, new_playlist, type);
|
||||
} else {
|
||||
db.get(`playlists`)
|
||||
db.get(`playlists.${type}`)
|
||||
.push(new_playlist)
|
||||
.write();
|
||||
}
|
||||
@@ -2518,19 +2431,31 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
||||
|
||||
app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
|
||||
let playlistID = req.body.playlistID;
|
||||
let type = req.body.type;
|
||||
let uuid = req.body.uuid;
|
||||
|
||||
let playlist = null;
|
||||
|
||||
if (req.isAuthenticated()) {
|
||||
playlist = auth_api.getUserPlaylist(uuid ? uuid : req.user.uid, playlistID);
|
||||
playlist = auth_api.getUserPlaylist(uuid ? uuid : req.user.uid, playlistID, type);
|
||||
type = playlist.type;
|
||||
} else {
|
||||
playlist = db.get(`playlists`).find({id: playlistID}).value();
|
||||
if (!type) {
|
||||
playlist = db.get('playlists.audio').find({id: playlistID}).value();
|
||||
if (!playlist) {
|
||||
playlist = db.get('playlists.video').find({id: playlistID}).value();
|
||||
if (playlist) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (!playlist) playlist = db.get(`playlists.${type}`).find({id: playlistID}).value();
|
||||
}
|
||||
|
||||
res.send({
|
||||
playlist: playlist,
|
||||
type: playlist && playlist.type,
|
||||
type: type,
|
||||
success: !!playlist
|
||||
});
|
||||
});
|
||||
@@ -2538,13 +2463,14 @@ 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 type = req.body.type;
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
if (req.isAuthenticated()) {
|
||||
auth_api.updatePlaylistFiles(req.user.uid, playlistID, fileNames);
|
||||
auth_api.updatePlaylistFiles(req.user.uid, playlistID, fileNames, type);
|
||||
} else {
|
||||
db.get(`playlists`)
|
||||
db.get(`playlists.${type}`)
|
||||
.find({id: playlistID})
|
||||
.assign({fileNames: fileNames})
|
||||
.write();
|
||||
@@ -2570,14 +2496,15 @@ app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
|
||||
|
||||
app.post('/api/deletePlaylist', optionalJwt, async (req, res) => {
|
||||
let playlistID = req.body.playlistID;
|
||||
let type = req.body.type;
|
||||
|
||||
let success = null;
|
||||
try {
|
||||
if (req.isAuthenticated()) {
|
||||
auth_api.removePlaylist(req.user.uid, playlistID);
|
||||
auth_api.removePlaylist(req.user.uid, playlistID, type);
|
||||
} else {
|
||||
// removes playlist from playlists
|
||||
db.get(`playlists`)
|
||||
db.get(`playlists.${type}`)
|
||||
.remove({id: playlistID})
|
||||
.write();
|
||||
}
|
||||
@@ -2599,23 +2526,23 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
||||
var blacklistMode = req.body.blacklistMode;
|
||||
|
||||
if (req.isAuthenticated()) {
|
||||
let success = await auth_api.deleteUserFile(req.user.uid, uid, blacklistMode);
|
||||
let success = auth_api.deleteUserFile(req.user.uid, uid, type, blacklistMode);
|
||||
res.send(success);
|
||||
return;
|
||||
}
|
||||
|
||||
var file_obj = db.get(`files`).find({uid: uid}).value();
|
||||
var file_obj = db.get(`files.${type}`).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.basename(fullpath), blacklistMode) : await deleteVideoFile(name, path.basename(fullpath), blacklistMode);
|
||||
db.get('files').remove({uid: uid}).write();
|
||||
wasDeleted = true;
|
||||
db.get('files.video').remove({uid: uid}).write();
|
||||
// wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
} else if (video_obj) {
|
||||
db.get('files').remove({uid: uid}).write();
|
||||
db.get('files.video').remove({uid: uid}).write();
|
||||
wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
} else {
|
||||
@@ -2751,7 +2678,7 @@ app.get('/api/stream/:id', optionalJwt, (req, res) => {
|
||||
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;
|
||||
let file_path = req.query.file_path ? decodeURIComponent(req.query.file_path) : null;
|
||||
if (!file_path && (req.isAuthenticated() || req.can_watch)) {
|
||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
if (optionalParams['subName']) {
|
||||
@@ -2768,7 +2695,7 @@ app.get('/api/stream/:id', optionalJwt, (req, res) => {
|
||||
}
|
||||
|
||||
if (!file_path) {
|
||||
file_path = path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
|
||||
file_path = path.join(videoFolderPath, id + ext);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(file_path)
|
||||
|
||||
@@ -26,10 +26,7 @@
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_auto_download_chat": false
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
@@ -38,8 +35,7 @@
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"redownload_fresh_uploads": false
|
||||
"subscriptions_check_interval": "300"
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
|
||||
@@ -15,16 +15,15 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
||||
|
||||
// other required vars
|
||||
let logger = null;
|
||||
let db = null;
|
||||
let users_db = null;
|
||||
var users_db = null;
|
||||
let SERVER_SECRET = null;
|
||||
let JWT_EXPIRATION = null;
|
||||
let opts = null;
|
||||
let saltRounds = null;
|
||||
|
||||
exports.initialize = function(input_db, input_users_db, input_logger) {
|
||||
exports.initialize = function(input_users_db, input_logger) {
|
||||
setLogger(input_logger)
|
||||
setDB(input_db, input_users_db);
|
||||
setDB(input_users_db);
|
||||
|
||||
/*************************
|
||||
* Authentication module
|
||||
@@ -62,8 +61,7 @@ function setLogger(input_logger) {
|
||||
logger = input_logger;
|
||||
}
|
||||
|
||||
function setDB(input_db, input_users_db) {
|
||||
db = input_db;
|
||||
function setDB(input_users_db) {
|
||||
users_db = input_users_db;
|
||||
}
|
||||
|
||||
@@ -91,12 +89,6 @@ exports.registerUser = function(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (plaintextPassword === "") {
|
||||
res.sendStatus(400);
|
||||
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||
return;
|
||||
}
|
||||
|
||||
bcrypt.hash(plaintextPassword, saltRounds)
|
||||
.then(function(hash) {
|
||||
let new_user = generateUserObject(userid, username, hash);
|
||||
@@ -285,11 +277,22 @@ exports.adminExists = function() {
|
||||
|
||||
exports.getUserVideos = function(user_uid, type) {
|
||||
const user = users_db.get('users').find({uid: user_uid}).value();
|
||||
return type ? user['files'].filter(file => file.isAudio = (type === 'audio')) : user['files'];
|
||||
return user['files'][type];
|
||||
}
|
||||
|
||||
exports.getUserVideo = function(user_uid, file_uid, requireSharing = false) {
|
||||
let file = users_db.get('users').find({uid: user_uid}).get(`files`).find({uid: file_uid}).value();
|
||||
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
||||
let file = null;
|
||||
if (!type) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
||||
if (!file) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
|
||||
if (file) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
||||
@@ -297,58 +300,38 @@ 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();
|
||||
exports.addPlaylist = function(user_uid, new_playlist, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).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});
|
||||
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.removePlaylist = function(user_uid, playlistID) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists`).remove({id: playlistID}).write();
|
||||
exports.removePlaylist = function(user_uid, playlistID, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.getUserPlaylists = function(user_uid, user_files = null) {
|
||||
exports.getUserPlaylists = function(user_uid, type) {
|
||||
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;
|
||||
return user['playlists'][type];
|
||||
}
|
||||
|
||||
exports.getUserPlaylist = function(user_uid, playlistID, requireSharing = false) {
|
||||
let playlist = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).value();
|
||||
exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing = false) {
|
||||
let playlist = null;
|
||||
if (!type) {
|
||||
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.audio`).find({id: playlistID}).value();
|
||||
if (!playlist) {
|
||||
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.video`).find({id: playlistID}).value();
|
||||
if (playlist) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
if (!playlist) playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).value();
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
|
||||
@@ -356,22 +339,21 @@ exports.getUserPlaylist = function(user_uid, playlistID, requireSharing = false)
|
||||
return playlist;
|
||||
}
|
||||
|
||||
exports.registerUserFile = function(user_uid, file_object) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`files`)
|
||||
exports.registerUserFile = function(user_uid, file_object, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
users_db.get('users').find({uid: user_uid}).get(`files`)
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
}
|
||||
|
||||
exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = false) {
|
||||
exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode = false) {
|
||||
let success = false;
|
||||
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files`).find({uid: file_uid}).value();
|
||||
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).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';
|
||||
|
||||
@@ -387,7 +369,7 @@ exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = fals
|
||||
}
|
||||
|
||||
const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
|
||||
users_db.get('users').find({uid: user_uid}).get(`files`)
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.remove({
|
||||
uid: file_uid
|
||||
}).write();
|
||||
@@ -436,11 +418,11 @@ exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = fals
|
||||
return success;
|
||||
}
|
||||
|
||||
exports.changeSharingMode = function(user_uid, file_uid, is_playlist, enabled) {
|
||||
exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
|
||||
let success = false;
|
||||
const user_db_obj = users_db.get('users').find({uid: user_uid});
|
||||
if (user_db_obj.value()) {
|
||||
const file_db_obj = is_playlist ? user_db_obj.get(`playlists`).find({id: file_uid}) : user_db_obj.get(`files`).find({uid: file_uid});
|
||||
const file_db_obj = is_playlist ? user_db_obj.get(`playlists.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
|
||||
if (file_db_obj.value()) {
|
||||
success = true;
|
||||
file_db_obj.assign({sharingEnabled: enabled}).write();
|
||||
|
||||
@@ -203,10 +203,7 @@ DEFAULT_CONFIG = {
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_auto_download_chat": false
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
@@ -215,8 +212,7 @@ DEFAULT_CONFIG = {
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"redownload_fresh_uploads": false
|
||||
"subscriptions_check_interval": "300"
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
|
||||
@@ -86,18 +86,6 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_youtube_api_key',
|
||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||
},
|
||||
'ytdl_use_twitch_api': {
|
||||
'key': 'ytdl_use_twitch_api',
|
||||
'path': 'YoutubeDLMaterial.API.use_twitch_API'
|
||||
},
|
||||
'ytdl_twitch_api_key': {
|
||||
'key': 'ytdl_twitch_api_key',
|
||||
'path': 'YoutubeDLMaterial.API.twitch_API_key'
|
||||
},
|
||||
'ytdl_twitch_auto_download_chat': {
|
||||
'key': 'ytdl_twitch_auto_download_chat',
|
||||
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||
},
|
||||
|
||||
// Themes
|
||||
'ytdl_default_theme': {
|
||||
@@ -126,10 +114,6 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_redownload_fresh_uploads': {
|
||||
'key': 'ytdl_subscriptions_redownload_fresh_uploads',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.redownload_fresh_uploads'
|
||||
},
|
||||
|
||||
// Users
|
||||
'ytdl_users_base_path': {
|
||||
@@ -196,5 +180,5 @@ AVAILABLE_PERMISSIONS = [
|
||||
module.exports = {
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
||||
CURRENT_VERSION: 'v4.2'
|
||||
CURRENT_VERSION: 'v4.1'
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ function initialize(input_db, input_users_db, input_logger) {
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null, category = null) {
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null) {
|
||||
let db_path = null;
|
||||
const file_id = file_path.substring(0, file_path.length-4);
|
||||
const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
|
||||
@@ -29,15 +29,12 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null, custo
|
||||
// add thumbnail path
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, customPath || multiUserMode && multiUserMode.file_path);
|
||||
|
||||
// if category exists, only include essential info
|
||||
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
||||
|
||||
if (!sub) {
|
||||
if (multiUserMode) {
|
||||
const user_uid = multiUserMode.user;
|
||||
db_path = users_db.get('users').find({uid: user_uid}).get(`files`);
|
||||
db_path = users_db.get('users').find({uid: user_uid}).get(`files.${type}`);
|
||||
} else {
|
||||
db_path = db.get(`files`);
|
||||
db_path = db.get(`files.${type}`)
|
||||
}
|
||||
} else {
|
||||
if (multiUserMode) {
|
||||
@@ -97,18 +94,18 @@ function generateFileObject(id, type, customPath = null, sub = null) {
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = type === 'audio';
|
||||
var description = jsonobj.description;
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date);
|
||||
return file_obj;
|
||||
}
|
||||
|
||||
function updatePlaylist(playlist, user_uid) {
|
||||
let playlistID = playlist.id;
|
||||
let type = playlist.type;
|
||||
let db_loc = null;
|
||||
if (user_uid) {
|
||||
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID});
|
||||
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID});
|
||||
} else {
|
||||
db_loc = db.get(`playlists`).find({id: playlistID});
|
||||
db_loc = db.get(`playlists.${type}`).find({id: playlistID});
|
||||
}
|
||||
db_loc.assign(playlist).write();
|
||||
return true;
|
||||
@@ -118,7 +115,7 @@ function getAppendedBasePathSub(sub, base_path) {
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
function getFileDirectoriesAndDBs() {
|
||||
async function importUnregisteredFiles() {
|
||||
let dirs_to_check = [];
|
||||
let subscriptions_to_check = [];
|
||||
const subscriptions_base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); // only for single-user mode
|
||||
@@ -135,14 +132,14 @@ function getFileDirectoriesAndDBs() {
|
||||
// add user's audio dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: path.join(usersFileFolder, user.uid, 'audio'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files.audio'),
|
||||
type: 'audio'
|
||||
});
|
||||
|
||||
// add user's video dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: path.join(usersFileFolder, user.uid, 'video'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files.video'),
|
||||
type: 'video'
|
||||
});
|
||||
}
|
||||
@@ -156,14 +153,14 @@ function getFileDirectoriesAndDBs() {
|
||||
// add audio dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: audioFolderPath,
|
||||
dbPath: db.get('files'),
|
||||
dbPath: db.get('files.audio'),
|
||||
type: 'audio'
|
||||
});
|
||||
|
||||
// add video dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: videoFolderPath,
|
||||
dbPath: db.get('files'),
|
||||
dbPath: db.get('files.video'),
|
||||
type: 'video'
|
||||
});
|
||||
}
|
||||
@@ -184,12 +181,6 @@ function getFileDirectoriesAndDBs() {
|
||||
});
|
||||
}
|
||||
|
||||
return dirs_to_check;
|
||||
}
|
||||
|
||||
async function importUnregisteredFiles() {
|
||||
const dirs_to_check = 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) {
|
||||
// recursively get all files in dir's path
|
||||
@@ -208,28 +199,9 @@ async function importUnregisteredFiles() {
|
||||
|
||||
}
|
||||
|
||||
async function getVideo(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) {
|
||||
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});
|
||||
if (!(file_db_path.value())) {
|
||||
logger.error(`Failed to find file with uid ${file_uid}`);
|
||||
}
|
||||
sub_db_path.find({uid: file_uid}).assign(assignment_obj).write();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize: initialize,
|
||||
registerFileDB: registerFileDB,
|
||||
updatePlaylist: updatePlaylist,
|
||||
getFileDirectoriesAndDBs: getFileDirectoriesAndDBs,
|
||||
importUnregisteredFiles: importUnregisteredFiles,
|
||||
getVideo: getVideo,
|
||||
setVideoProperty: setVideoProperty
|
||||
importUnregisteredFiles: importUnregisteredFiles
|
||||
}
|
||||
|
||||
20
backend/package-lock.json
generated
20
backend/package-lock.json
generated
@@ -252,14 +252,6 @@
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
|
||||
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
|
||||
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"backoff": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
||||
@@ -1080,11 +1072,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
|
||||
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
@@ -1863,9 +1850,10 @@
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==",
|
||||
"optional": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"axios": "^0.21.0",
|
||||
"bcryptjs": "^2.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
@@ -43,7 +42,6 @@
|
||||
"lowdb": "^1.0.0",
|
||||
"md5": "^2.2.1",
|
||||
"merge-files": "^0.1.2",
|
||||
"moment": "^2.29.1",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-id3": "^0.1.14",
|
||||
|
||||
@@ -6,8 +6,7 @@ var path = require('path');
|
||||
|
||||
var youtubedl = require('youtube-dl');
|
||||
const config_api = require('./config');
|
||||
const twitch_api = require('./twitch');
|
||||
var utils = require('./utils');
|
||||
var utils = require('./utils')
|
||||
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
@@ -255,6 +254,10 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
}
|
||||
|
||||
async function getVideosForSub(sub, user_uid = null) {
|
||||
if (!subExists(sub.id, user_uid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get sub_db
|
||||
let sub_db = null;
|
||||
if (user_uid)
|
||||
@@ -262,99 +265,6 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
else
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
|
||||
const latest_sub_obj = sub_db.value();
|
||||
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
||||
|
||||
// get basePath
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
|
||||
let multiUserMode = null;
|
||||
if (user_uid) {
|
||||
multiUserMode = {
|
||||
user: user_uid,
|
||||
file_path: appendedBasePath
|
||||
}
|
||||
}
|
||||
|
||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
||||
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
|
||||
return new Promise(resolve => {
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, async function(err, output) {
|
||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
if (err && !output) {
|
||||
logger.error(err.stderr ? err.stderr : err.message);
|
||||
if (err.stderr.includes('This video is unavailable')) {
|
||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||
try {
|
||||
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
for (let i = 0; i < outputs.length; i++) {
|
||||
const output = JSON.parse(outputs[i]);
|
||||
handleOutputJSON(sub, sub_db, output, i === 0, multiUserMode)
|
||||
if (err.stderr.includes(output['id']) && archive_path) {
|
||||
// we found a video that errored! add it to the archive to prevent future errors
|
||||
if (sub.archive) {
|
||||
archive_dir = sub.archive;
|
||||
archive_path = path.join(archive_dir, 'archive.txt')
|
||||
fs.appendFileSync(archive_path, output['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
logger.error('Backup method failed. See error below:');
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
resolve(false);
|
||||
} else if (output) {
|
||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
logger.verbose('No additional videos to download for ' + sub.name);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reset_videos = i === 0;
|
||||
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||
await setFreshUploads(sub, user_uid);
|
||||
checkVideosForFreshUploads(sub, user_uid);
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
logger.error(err);
|
||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||
});
|
||||
}
|
||||
|
||||
async function generateArgsForSubscription(sub, user_uid, redownload = false, desired_path = null) {
|
||||
// get basePath
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
@@ -364,16 +274,25 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
|
||||
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
let appendedBasePath = null
|
||||
appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
|
||||
let multiUserMode = null;
|
||||
if (user_uid) {
|
||||
multiUserMode = {
|
||||
user: user_uid,
|
||||
file_path: appendedBasePath
|
||||
}
|
||||
}
|
||||
|
||||
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
||||
|
||||
let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`;
|
||||
if (desired_path) {
|
||||
fullOutput = `${desired_path}.%(ext)s`;
|
||||
} else if (sub.custom_output) {
|
||||
if (sub.custom_output) {
|
||||
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
|
||||
}
|
||||
|
||||
let downloadConfig = ['-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
|
||||
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
|
||||
|
||||
let qualityPath = null;
|
||||
if (sub.type && sub.type === 'audio') {
|
||||
@@ -400,7 +319,7 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
let archive_dir = null;
|
||||
let archive_path = null;
|
||||
|
||||
if (useArchive && !redownload) {
|
||||
if (useArchive) {
|
||||
if (sub.archive) {
|
||||
archive_dir = sub.archive;
|
||||
archive_path = path.join(archive_dir, 'archive.txt')
|
||||
@@ -413,7 +332,7 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
downloadConfig = ['-f', 'best', '--dump-json'];
|
||||
}
|
||||
|
||||
if (sub.timerange && !redownload) {
|
||||
if (sub.timerange) {
|
||||
downloadConfig.push('--dateafter', sub.timerange);
|
||||
}
|
||||
|
||||
@@ -430,7 +349,60 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
return downloadConfig;
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
|
||||
return new Promise(resolve => {
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
if (err && !output) {
|
||||
logger.error(err.stderr ? err.stderr : err.message);
|
||||
if (err.stderr.includes('This video is unavailable')) {
|
||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||
try {
|
||||
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
for (let i = 0; i < outputs.length; i++) {
|
||||
const output = JSON.parse(outputs[i]);
|
||||
handleOutputJSON(sub, sub_db, output, i === 0, multiUserMode)
|
||||
if (err.stderr.includes(output['id']) && archive_path) {
|
||||
// we found a video that errored! add it to the archive to prevent future errors
|
||||
fs.appendFileSync(archive_path, output['id']);
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
logger.error('Backup method failed. See error below:');
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
resolve(false);
|
||||
} else if (output) {
|
||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
logger.verbose('No additional videos to download for ' + sub.name);
|
||||
resolve(true);
|
||||
}
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reset_videos = i === 0;
|
||||
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
|
||||
|
||||
// TODO: Potentially store downloaded files in db?
|
||||
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_videos = false) {
|
||||
@@ -445,49 +417,17 @@ function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_
|
||||
// add to db
|
||||
sub_db.get('videos').push(output_json).write();
|
||||
} else {
|
||||
path_object = path.parse(output_json['_filename']);
|
||||
const path_string = path.format(path_object);
|
||||
|
||||
if (sub_db.get('videos').find({path: path_string}).value()) {
|
||||
// file already exists in DB, return early to avoid reseting the download date
|
||||
return;
|
||||
}
|
||||
|
||||
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
|
||||
const url = output_json['webpage_url'];
|
||||
if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||
const file_name = path.basename(output_json['_filename']);
|
||||
const id = file_name.substring(0, file_name.length-4);
|
||||
let vodId = url.split('twitch.tv/videos/')[1];
|
||||
vodId = vodId.split('?')[0];
|
||||
twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSubscriptions(user_uid = null) {
|
||||
function getAllSubscriptions(user_uid = null) {
|
||||
if (user_uid)
|
||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').value();
|
||||
else
|
||||
return db.get('subscriptions').value();
|
||||
}
|
||||
|
||||
function getAllSubscriptions() {
|
||||
let subscriptions = null;
|
||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
if (multiUserMode) {
|
||||
subscriptions = [];
|
||||
let users = users_db.get('users').value();
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i]['subscriptions']) subscriptions = subscriptions.concat(users[i]['subscriptions']);
|
||||
}
|
||||
} else {
|
||||
subscriptions = getSubscriptions();
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
function getSubscription(subID, user_uid = null) {
|
||||
if (user_uid)
|
||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
|
||||
@@ -511,21 +451,6 @@ function updateSubscription(sub, user_uid = null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
||||
subs.forEach(sub => {
|
||||
updateSubscriptionProperty(sub, assignment_obj, sub.user_uid);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSubscriptionProperty(sub, assignment_obj, user_uid = null) {
|
||||
if (user_uid) {
|
||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(assignment_obj).write();
|
||||
} else {
|
||||
db.get('subscriptions').find({id: sub.id}).assign(assignment_obj).write();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function subExists(subID, user_uid = null) {
|
||||
if (user_uid)
|
||||
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
|
||||
@@ -533,52 +458,6 @@ function subExists(subID, user_uid = null) {
|
||||
return !!db.get('subscriptions').find({id: subID}).value();
|
||||
}
|
||||
|
||||
async function setFreshUploads(sub, user_uid) {
|
||||
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||
sub.videos.forEach(async video => {
|
||||
if (current_date === video['upload_date'].replace(/-/g, '')) {
|
||||
// set upload as fresh
|
||||
const video_uid = video['uid'];
|
||||
await db_api.setVideoProperty(video_uid, {'fresh_upload': true}, user_uid, sub['id']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkVideosForFreshUploads(sub, user_uid) {
|
||||
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||
sub.videos.forEach(async video => {
|
||||
if (video['fresh_upload'] && current_date > video['upload_date'].replace(/-/g, '')) {
|
||||
checkVideoIfBetterExists(video, sub, user_uid)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
||||
const new_path = file_obj['path'].substring(0, file_obj['path'].length - 4);
|
||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
|
||||
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
|
||||
// simulate a download to verify that a better version exists
|
||||
youtubedl.getInfo(file_obj['url'], downloadConfig, (err, output) => {
|
||||
if (err) {
|
||||
// video is not available anymore for whatever reason
|
||||
} else if (output) {
|
||||
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
||||
if (output[metric_to_compare] > file_obj[metric_to_compare]) {
|
||||
// download new video as the simulated one is better
|
||||
youtubedl.exec(file_obj['url'], downloadConfig, async (err, output) => {
|
||||
if (err) {
|
||||
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
||||
} else if (output) {
|
||||
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
|
||||
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]}, user_uid, sub['id']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false}, user_uid, sub['id']);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
function getAppendedBasePath(sub, base_path) {
|
||||
@@ -616,7 +495,6 @@ async function removeIDFromArchive(archive_path, id) {
|
||||
module.exports = {
|
||||
getSubscription : getSubscription,
|
||||
getSubscriptionByName : getSubscriptionByName,
|
||||
getSubscriptions : getSubscriptions,
|
||||
getAllSubscriptions : getAllSubscriptions,
|
||||
updateSubscription : updateSubscription,
|
||||
subscribe : subscribe,
|
||||
@@ -625,6 +503,5 @@ module.exports = {
|
||||
getVideosForSub : getVideosForSub,
|
||||
removeIDFromArchive : removeIDFromArchive,
|
||||
setLogger : setLogger,
|
||||
initialize : initialize,
|
||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple
|
||||
initialize : initialize
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
var moment = require('moment');
|
||||
var Axios = require('axios');
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path');
|
||||
const config_api = require('./config');
|
||||
|
||||
async function getCommentsForVOD(clientID, vodId) {
|
||||
let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
|
||||
batch,
|
||||
cursor;
|
||||
|
||||
let comments = null;
|
||||
|
||||
try {
|
||||
do {
|
||||
batch = (await Axios.get(url, {
|
||||
headers: {
|
||||
'Client-ID': clientID,
|
||||
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
}
|
||||
})).data;
|
||||
|
||||
const str = batch.comments.map(c => {
|
||||
let {
|
||||
created_at: msgCreated,
|
||||
content_offset_seconds: timestamp,
|
||||
commenter: {
|
||||
name,
|
||||
_id,
|
||||
created_at: acctCreated
|
||||
},
|
||||
message: {
|
||||
body: msg,
|
||||
user_color: user_color
|
||||
}
|
||||
} = c;
|
||||
|
||||
const timestamp_str = moment.duration(timestamp, 'seconds')
|
||||
.toISOString()
|
||||
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
|
||||
(_, ...ms) => {
|
||||
const seg = v => v ? v.padStart(2, '0') : '00';
|
||||
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
|
||||
});
|
||||
|
||||
acctCreated = moment(acctCreated).utc();
|
||||
msgCreated = moment(msgCreated).utc();
|
||||
|
||||
if (!comments) comments = [];
|
||||
|
||||
comments.push({
|
||||
timestamp: timestamp,
|
||||
timestamp_str: timestamp_str,
|
||||
name: name,
|
||||
message: msg,
|
||||
user_color: user_color
|
||||
});
|
||||
// let line = `${timestamp},${msgCreated.format(tsFormat)},${name},${_id},"${msg.replace(/"/g, '""')}",${acctCreated.format(tsFormat)}`;
|
||||
// return line;
|
||||
}).join('\n');
|
||||
|
||||
cursor = batch._next;
|
||||
url = `https://api.twitch.tv/v5/videos/${vodId}/comments?cursor=${cursor}`;
|
||||
await new Promise(res => setTimeout(res, 300));
|
||||
} while (cursor);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
||||
let file_path = null;
|
||||
|
||||
if (user_uid) {
|
||||
if (sub) {
|
||||
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
} else {
|
||||
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
||||
}
|
||||
} else {
|
||||
if (sub) {
|
||||
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
} else {
|
||||
file_path = path.join(type, id + '.twitch_chat.json');
|
||||
}
|
||||
}
|
||||
|
||||
var chat_file = null;
|
||||
if (fs.existsSync(file_path)) {
|
||||
chat_file = fs.readJSONSync(file_path);
|
||||
}
|
||||
|
||||
return chat_file;
|
||||
}
|
||||
|
||||
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
|
||||
const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
|
||||
const chat = await getCommentsForVOD(twitch_api_key, vodId);
|
||||
|
||||
// save file if needed params are included
|
||||
let file_path = null;
|
||||
if (user_uid) {
|
||||
if (sub) {
|
||||
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
} else {
|
||||
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
||||
}
|
||||
} else {
|
||||
if (sub) {
|
||||
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
} else {
|
||||
file_path = path.join(type, id + '.twitch_chat.json');
|
||||
}
|
||||
}
|
||||
|
||||
if (chat) fs.writeJSONSync(file_path, chat);
|
||||
|
||||
return chat;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCommentsForVOD: getCommentsForVOD,
|
||||
getTwitchChatByFileID: getTwitchChatByFileID,
|
||||
downloadTwitchChatByVODID: downloadTwitchChatByVODID
|
||||
}
|
||||
@@ -20,7 +20,7 @@ function getTrueFileName(unfixed_path, type) {
|
||||
return fixed_path;
|
||||
}
|
||||
|
||||
async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
||||
async function getDownloadedFilesByType(basePath, type) {
|
||||
// return empty array if the path doesn't exist
|
||||
if (!(await fs.pathExists(basePath))) return [];
|
||||
|
||||
@@ -36,17 +36,18 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = await getJSONByType(type, id, basePath);
|
||||
if (!jsonobj) continue;
|
||||
if (full_metadata) {
|
||||
jsonobj['id'] = id;
|
||||
files.push(jsonobj);
|
||||
continue;
|
||||
}
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var isaudio = type === 'audio';
|
||||
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
|
||||
stats.size, file, upload_date, jsonobj.description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
||||
files.push(file_obj);
|
||||
}
|
||||
return files;
|
||||
@@ -183,7 +184,7 @@ async function recFindByExt(base,ext,files,result)
|
||||
|
||||
// objects
|
||||
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
@@ -194,10 +195,6 @@ function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, p
|
||||
this.size = size;
|
||||
this.path = path;
|
||||
this.upload_date = upload_date;
|
||||
this.description = description;
|
||||
this.view_count = view_count;
|
||||
this.height = height;
|
||||
this.abr = abr;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
20
chrome-extension/background.js
Normal file
20
chrome-extension/background.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// background.js
|
||||
|
||||
// Called when the user clicks on the browser action.
|
||||
chrome.browserAction.onClicked.addListener(function(tab) {
|
||||
// get the frontend_url
|
||||
chrome.storage.sync.get({
|
||||
frontend_url: 'http://localhost',
|
||||
audio_only: false
|
||||
}, function(items) {
|
||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||
var activeTab = tabs[0];
|
||||
var url = activeTab.url;
|
||||
if (url.includes('youtube.com')) {
|
||||
var new_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(url) + ';audioOnly=' + items.audio_only;
|
||||
chrome.tabs.create({ url: new_url });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "YoutubeDL-Material",
|
||||
"version": "0.4",
|
||||
"version": "0.3",
|
||||
"description": "The Official Firefox & Chrome Extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": "favicon.png",
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "YoutubeDL-Material"
|
||||
"default_icon": "favicon.png"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage",
|
||||
"contextMenus"
|
||||
"storage"
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Scripts -->
|
||||
<script src="js/jquery-3.4.1.min.js"></script>
|
||||
<script src="js/popper.min.js"></script>
|
||||
<script src="js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Cascading Style Sheets -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div style="width: 400px; margin: 0 auto;">
|
||||
<div style="margin: 10px;">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="audio_only">
|
||||
Audio only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="url_input" type="text" class="form-control" placeholder="URL" aria-label="URL" aria-describedby="basic-addon2">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" id="download">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,50 +0,0 @@
|
||||
function audioOnlyClicked() {
|
||||
console.log('audio only clicked');
|
||||
var audio_only = document.getElementById("audio_only").checked;
|
||||
|
||||
// save state
|
||||
|
||||
chrome.storage.sync.set({
|
||||
audio_only: audio_only
|
||||
}, function() {});
|
||||
}
|
||||
|
||||
function downloadVideo() {
|
||||
var input_url = document.getElementById("url_input").value
|
||||
// get the frontend_url
|
||||
chrome.storage.sync.get({
|
||||
frontend_url: 'http://localhost',
|
||||
audio_only: false
|
||||
}, function(items) {
|
||||
var download_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(input_url) + ';audioOnly=' + items.audio_only;
|
||||
chrome.tabs.create({ url: download_url });
|
||||
});
|
||||
}
|
||||
|
||||
function loadInputs() {
|
||||
// load audio-only input
|
||||
chrome.storage.sync.get({
|
||||
frontend_url: 'http://localhost',
|
||||
audio_only: false
|
||||
}, function(items) {
|
||||
document.getElementById("audio_only").checked = items.audio_only;
|
||||
});
|
||||
|
||||
// load url input
|
||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||
var activeTab = tabs[0];
|
||||
var current_url = activeTab.url;
|
||||
console.log(current_url);
|
||||
if (current_url && current_url.includes('youtube.com')) {
|
||||
document.getElementById("url_input").value = current_url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('download').addEventListener('click',
|
||||
downloadVideo);
|
||||
|
||||
document.getElementById('audio_only').addEventListener('click',
|
||||
audioOnlyClicked);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadInputs);
|
||||
Binary file not shown.
Binary file not shown.
12952
package-lock.json
generated
12952
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.2.0",
|
||||
"version": "4.1.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -18,20 +18,19 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "^11.0.4",
|
||||
"@angular/animations": "^11.0.4",
|
||||
"@angular/cdk": "^11.0.2",
|
||||
"@angular/common": "^11.0.4",
|
||||
"@angular/compiler": "^11.0.4",
|
||||
"@angular/core": "^11.0.4",
|
||||
"@angular/forms": "^11.0.4",
|
||||
"@angular/localize": "^11.0.4",
|
||||
"@angular/material": "^11.0.2",
|
||||
"@angular/platform-browser": "^11.0.4",
|
||||
"@angular/platform-browser-dynamic": "^11.0.4",
|
||||
"@angular/router": "^11.0.4",
|
||||
"@angular-devkit/core": "^9.0.6",
|
||||
"@angular/animations": "^9.1.0",
|
||||
"@angular/cdk": "^9.2.0",
|
||||
"@angular/common": "^9.1.0",
|
||||
"@angular/compiler": "^9.1.0",
|
||||
"@angular/core": "^9.0.7",
|
||||
"@angular/forms": "^9.1.0",
|
||||
"@angular/localize": "^9.1.0",
|
||||
"@angular/material": "^9.2.0",
|
||||
"@angular/platform-browser": "^9.1.0",
|
||||
"@angular/platform-browser-dynamic": "^9.1.0",
|
||||
"@angular/router": "^9.1.0",
|
||||
"@ngneat/content-loader": "^5.0.0",
|
||||
"@videogular/ngx-videogular": "^2.1.0",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^6.1.0",
|
||||
@@ -40,34 +39,35 @@
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ngx-avatar": "^4.0.0",
|
||||
"ngx-file-drop": "^9.0.1",
|
||||
"rxjs": "^6.6.3",
|
||||
"ngx-videogular": "^9.0.1",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "~4.0.5",
|
||||
"tslib": "^1.10.0",
|
||||
"typescript": "~3.7.5",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.1100.4",
|
||||
"@angular/cli": "^11.0.4",
|
||||
"@angular/compiler-cli": "^11.0.4",
|
||||
"@angular/language-service": "^11.0.4",
|
||||
"@angular-devkit/build-angular": "^0.901.0",
|
||||
"@angular/cli": "^9.0.7",
|
||||
"@angular/compiler-cli": "^9.0.7",
|
||||
"@angular/language-service": "^9.0.7",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasmine": "2.5.45",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"codelyzer": "^5.1.2",
|
||||
"electron": "^8.0.1",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.0.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"jasmine-core": "~2.6.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.0.4",
|
||||
"tslint": "~6.1.0"
|
||||
"tslint": "~5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const routes: Routes = [
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' })],
|
||||
imports: [RouterModule.forRoot(routes, { useHash: true })],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
@@ -11,19 +11,19 @@ describe('AppComponent', () => {
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', waitForAsync(() => {
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
|
||||
it(`should have as title 'app'`, waitForAsync(() => {
|
||||
it(`should have as title 'app'`, async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('app');
|
||||
}));
|
||||
|
||||
it('should render title in a h1 tag', waitForAsync(() => {
|
||||
it('should render title in a h1 tag', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
|
||||
@@ -32,17 +32,14 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { FileCardComponent } from './file-card/file-card.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
import { VgControlsModule } from '@videogular/ngx-videogular/controls';
|
||||
import { VgBufferingModule } from '@videogular/ngx-videogular/buffering';
|
||||
import { VgOverlayPlayModule } from '@videogular/ngx-videogular/overlay-play';
|
||||
import { VgCoreModule } from '@videogular/ngx-videogular/core';
|
||||
import { VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule } from 'ngx-videogular';
|
||||
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||
@@ -83,9 +80,6 @@ import { RecentVideosComponent } from './components/recent-videos/recent-videos.
|
||||
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
|
||||
import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit-category-dialog.component';
|
||||
import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component';
|
||||
import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component';
|
||||
import { H401Interceptor } from './http.interceptor';
|
||||
|
||||
registerLocaleData(es, 'es');
|
||||
|
||||
@@ -112,7 +106,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
VideoInfoDialogComponent,
|
||||
ArgModifierDialogComponent,
|
||||
HighlightPipe,
|
||||
LinkifyPipe,
|
||||
UpdaterComponent,
|
||||
UpdateProgressDialogComponent,
|
||||
ShareMediaDialogComponent,
|
||||
@@ -132,9 +125,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
RecentVideosComponent,
|
||||
EditSubscriptionDialogComponent,
|
||||
CustomPlaylistsComponent,
|
||||
EditCategoryDialogComponent,
|
||||
TwitchChatComponent,
|
||||
SeeMoreComponent
|
||||
EditCategoryDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -192,12 +183,10 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
SettingsComponent
|
||||
],
|
||||
providers: [
|
||||
PostsService,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: H401Interceptor, multi: true }
|
||||
PostsService
|
||||
],
|
||||
exports: [
|
||||
HighlightPipe,
|
||||
LinkifyPipe
|
||||
HighlightPipe
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CustomPlaylistsComponent } from './custom-playlists.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('CustomPlaylistsComponent', () => {
|
||||
let component: CustomPlaylistsComponent;
|
||||
let fixture: ComponentFixture<CustomPlaylistsComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CustomPlaylistsComponent ]
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
} 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', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DownloadsComponent } from './downloads.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('DownloadsComponent', () => {
|
||||
let component: DownloadsComponent;
|
||||
let fixture: ComponentFixture<DownloadsComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DownloadsComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ export class LoginComponent implements OnInit {
|
||||
constructor(private postsService: PostsService, private snackBar: MatSnackBar, private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.isLoggedIn && localStorage.getItem('jwt_token') !== 'null') {
|
||||
if (this.postsService.isLoggedIn) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogsViewerComponent } from './logs-viewer.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('LogsViewerComponent', () => {
|
||||
let component: LogsViewerComponent;
|
||||
let fixture: ComponentFixture<LogsViewerComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LogsViewerComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ManageRoleComponent } from './manage-role.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ManageRoleComponent', () => {
|
||||
let component: ManageRoleComponent;
|
||||
let fixture: ComponentFixture<ManageRoleComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ManageRoleComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ManageUserComponent } from './manage-user.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ManageUserComponent', () => {
|
||||
let component: ManageUserComponent;
|
||||
let fixture: ComponentFixture<ManageUserComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ManageUserComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModifyUsersComponent } from './modify-users.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ModifyUsersComponent', () => {
|
||||
let component: ModifyUsersComponent;
|
||||
let fixture: ComponentFixture<ModifyUsersComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ModifyUsersComponent ]
|
||||
})
|
||||
|
||||
@@ -30,13 +30,10 @@
|
||||
<div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<ng-container *ngIf="normal_files_received && paged_data">
|
||||
<div *ngFor="let file of paged_data; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<ng-container *ngIf="normal_files_received">
|
||||
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? '?jwt=' + this.postsService.token : ''"></app-unified-file-card>
|
||||
</div>
|
||||
<div *ngIf="filtered_files.length === 0">
|
||||
<ng-container i18n="No videos found">No videos found.</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
@@ -45,9 +42,4 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-paginator class="paginator" #paginator *ngIf="filtered_files && filtered_files.length > 0" (page)="pageChangeEvent($event)" [length]="filtered_files.length"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
@@ -47,10 +47,6 @@
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.paginator {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.my-videos-title {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RecentVideosComponent } from './recent-videos.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('RecentVideosComponent', () => {
|
||||
let component: RecentVideosComponent;
|
||||
let fixture: ComponentFixture<RecentVideosComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RecentVideosComponent ]
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recent-videos',
|
||||
@@ -51,16 +50,10 @@ export class RecentVideosComponent implements OnInit {
|
||||
};
|
||||
filterProperty = this.filterProperties['upload_date'];
|
||||
|
||||
pageSize = 10;
|
||||
paged_data = null;
|
||||
|
||||
@ViewChild('paginator') paginator: MatPaginator
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router) {
|
||||
// get cached file count
|
||||
if (localStorage.getItem('cached_file_count')) {
|
||||
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
|
||||
|
||||
this.cached_file_count = +localStorage.getItem('cached_file_count');
|
||||
this.loading_files = Array(this.cached_file_count).fill(0);
|
||||
}
|
||||
}
|
||||
@@ -98,8 +91,7 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
private filterFiles(value: string) {
|
||||
const filterValue = value.toLowerCase();
|
||||
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue) || option.category?.name?.toLowerCase().includes(filterValue));
|
||||
this.pageChangeEvent({pageSize: this.pageSize, pageIndex: this.paginator.pageIndex});
|
||||
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
filterByProperty(prop) {
|
||||
@@ -108,7 +100,6 @@ export class RecentVideosComponent implements OnInit {
|
||||
} else {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
|
||||
}
|
||||
if (this.paginator) { this.pageChangeEvent({pageSize: this.pageSize, pageIndex: this.paginator.pageIndex}) };
|
||||
}
|
||||
|
||||
filterOptionChanged(value) {
|
||||
@@ -127,11 +118,9 @@ export class RecentVideosComponent implements OnInit {
|
||||
this.normal_files_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
this.files = res['files'];
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
const file = this.files[i];
|
||||
this.files.forEach(file => {
|
||||
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
|
||||
file.index = i;
|
||||
}
|
||||
});
|
||||
this.files.sort(this.sortFiles);
|
||||
if (this.search_mode) {
|
||||
this.filterFiles(this.search_text);
|
||||
@@ -144,8 +133,6 @@ export class RecentVideosComponent implements OnInit {
|
||||
localStorage.setItem('cached_file_count', '' + this.files.length);
|
||||
|
||||
this.normal_files_received = true;
|
||||
|
||||
this.paged_data = this.filtered_files.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -249,9 +236,7 @@ export class RecentVideosComponent implements OnInit {
|
||||
this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.postsService.openSnackBar('Delete success!', 'OK.');
|
||||
this.files.splice(file.index, 1);
|
||||
for (let i = 0; i < this.files.length; i++) { this.files[i].index = i }
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
this.files.splice(index, 1);
|
||||
} else {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
@@ -291,18 +276,13 @@ export class RecentVideosComponent implements OnInit {
|
||||
const result = b.registered - a.registered;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
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));
|
||||
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;
|
||||
}
|
||||
|
||||
pageChangeEvent(event) {
|
||||
const offset = ((event.pageIndex + 1) - 1) * event.pageSize;
|
||||
this.paged_data = this.filtered_files.slice(offset).slice(0, event.pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<span class="text" [ngStyle]="{'-webkit-line-clamp': !see_more_active ? line_limit : null}" [innerHTML]="text | linkify"></span>
|
||||
<span>
|
||||
<a [routerLink]="" (click)="toggleSeeMore()">
|
||||
<ng-container *ngIf="!see_more_active" i18n="See more">
|
||||
See more.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="see_more_active" i18n="See less">
|
||||
See less.
|
||||
</ng-container>
|
||||
</a>
|
||||
</span>
|
||||
@@ -1,7 +0,0 @@
|
||||
.text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SeeMoreComponent } from './see-more.component';
|
||||
|
||||
describe('SeeMoreComponent', () => {
|
||||
let component: SeeMoreComponent;
|
||||
let fixture: ComponentFixture<SeeMoreComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SeeMoreComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SeeMoreComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Component, Input, OnInit, Pipe, PipeTransform } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Pipe({ name: 'linkify' })
|
||||
export class LinkifyPipe implements PipeTransform {
|
||||
|
||||
constructor(private _domSanitizer: DomSanitizer) {}
|
||||
|
||||
transform(value: any, args?: any): any {
|
||||
return this._domSanitizer.bypassSecurityTrustHtml(this.stylize(value));
|
||||
}
|
||||
|
||||
// Modify this method according to your custom logic
|
||||
private stylize(text: string): string {
|
||||
let stylizedText: string = '';
|
||||
if (text && text.length > 0) {
|
||||
for (let line of text.split("\n")) {
|
||||
for (let t of line.split(" ")) {
|
||||
if (t.startsWith("http") && t.length>7) {
|
||||
stylizedText += `<a target="_blank" href="${t}">${t}</a> `;
|
||||
}
|
||||
else
|
||||
stylizedText += t + " ";
|
||||
}
|
||||
stylizedText += '<br>';
|
||||
}
|
||||
return stylizedText;
|
||||
}
|
||||
else return text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-see-more',
|
||||
templateUrl: './see-more.component.html',
|
||||
providers: [LinkifyPipe],
|
||||
styleUrls: ['./see-more.component.scss']
|
||||
})
|
||||
export class SeeMoreComponent implements OnInit {
|
||||
|
||||
see_more_active = false;
|
||||
|
||||
@Input() text = '';
|
||||
@Input() line_limit = 2;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
toggleSeeMore() {
|
||||
this.see_more_active = !this.see_more_active;
|
||||
}
|
||||
|
||||
parseText() {
|
||||
return this.text.replace(/(http.*?\s)/, "<a href=\"$1\">$1</a>")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
|
||||
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
|
||||
<div #chat style="max-width: 250px" *ngFor="let chat of visible_chat; let last = last">
|
||||
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: {{chat.message}}
|
||||
{{last ? scrollToBottom() : ''}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="chat_response_received && !full_chat">
|
||||
<button [disabled]="downloading_chat" (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
|
||||
<mat-spinner *ngIf="downloading_chat" class="downloading-spinner" [diameter]="30"></mat-spinner>
|
||||
</ng-container>
|
||||
@@ -1,13 +0,0 @@
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.downloading-spinner {
|
||||
top: 50%;
|
||||
left: 80px;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { TwitchChatComponent } from './twitch-chat.component';
|
||||
|
||||
describe('TwitchChatComponent', () => {
|
||||
let component: TwitchChatComponent;
|
||||
let fixture: ComponentFixture<TwitchChatComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TwitchChatComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TwitchChatComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-twitch-chat',
|
||||
templateUrl: './twitch-chat.component.html',
|
||||
styleUrls: ['./twitch-chat.component.scss']
|
||||
})
|
||||
export class TwitchChatComponent implements OnInit, AfterViewInit {
|
||||
|
||||
full_chat = null;
|
||||
visible_chat = null;
|
||||
chat_response_received = false;
|
||||
downloading_chat = false;
|
||||
|
||||
current_chat_index = null;
|
||||
|
||||
CHAT_CHECK_INTERVAL_MS = 200;
|
||||
chat_check_interval_obj = null;
|
||||
|
||||
scrollContainer = null;
|
||||
|
||||
@Input() db_file = null;
|
||||
@Input() sub = null;
|
||||
@Input() current_timestamp = null;
|
||||
|
||||
@ViewChild('scrollContainer') scrollRef: ElementRef;
|
||||
@ViewChildren('chat') chat: QueryList<any>;
|
||||
|
||||
constructor(private postsService: PostsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getFullChat();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
}
|
||||
|
||||
private isUserNearBottom(): boolean {
|
||||
const threshold = 150;
|
||||
const position = this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight;
|
||||
const height = this.scrollContainer.scrollHeight;
|
||||
return position > height - threshold;
|
||||
}
|
||||
|
||||
scrollToBottom = (force_scroll) => {
|
||||
if (force_scroll || this.isUserNearBottom()) {
|
||||
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
addNewChatMessages() {
|
||||
const next_chat_index = this.getIndexOfNextChat();
|
||||
if (!this.scrollContainer) {
|
||||
this.scrollContainer = this.scrollRef.nativeElement;
|
||||
}
|
||||
if (this.current_chat_index === null) {
|
||||
this.current_chat_index = next_chat_index;
|
||||
}
|
||||
|
||||
if (Math.abs(next_chat_index - this.current_chat_index) > 25) {
|
||||
this.visible_chat = [];
|
||||
this.current_chat_index = next_chat_index - 25;
|
||||
setTimeout(() => this.scrollToBottom(true), 100);
|
||||
}
|
||||
|
||||
const latest_chat_timestamp = this.visible_chat.length ? this.visible_chat[this.visible_chat.length - 1]['timestamp'] : 0;
|
||||
|
||||
for (let i = this.current_chat_index + 1; i < this.full_chat.length; i++) {
|
||||
if (this.full_chat[i]['timestamp'] >= latest_chat_timestamp && this.full_chat[i]['timestamp'] <= this.current_timestamp) {
|
||||
this.visible_chat.push(this.full_chat[i]);
|
||||
this.current_chat_index = i;
|
||||
} else if (this.full_chat[i]['timestamp'] > this.current_timestamp) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getIndexOfNextChat() {
|
||||
const index = binarySearch(this.full_chat, 'timestamp', this.current_timestamp);
|
||||
return index;
|
||||
}
|
||||
|
||||
getFullChat() {
|
||||
this.postsService.getFullTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', null, this.sub).subscribe(res => {
|
||||
this.chat_response_received = true;
|
||||
if (res['chat']) {
|
||||
this.initializeChatCheck(res['chat']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
downloadTwitchChat() {
|
||||
this.downloading_chat = true;
|
||||
let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1];
|
||||
vodId = vodId.split('?')[0];
|
||||
if (!vodId) {
|
||||
this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"');
|
||||
}
|
||||
this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null, this.sub).subscribe(res => {
|
||||
if (res['chat']) {
|
||||
this.initializeChatCheck(res['chat']);
|
||||
} else {
|
||||
this.downloading_chat = false;
|
||||
this.postsService.openSnackBar('Download failed.')
|
||||
}
|
||||
}, err => {
|
||||
this.downloading_chat = false;
|
||||
this.postsService.openSnackBar('Chat could not be downloaded.')
|
||||
});
|
||||
}
|
||||
|
||||
initializeChatCheck(full_chat) {
|
||||
this.full_chat = full_chat;
|
||||
this.visible_chat = [];
|
||||
this.chat_check_interval_obj = setInterval(() => this.addNewChatMessages(), this.CHAT_CHECK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function binarySearch(arr, key, n) {
|
||||
let min = 0;
|
||||
let max = arr.length - 1;
|
||||
let mid;
|
||||
while (min <= max) {
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
mid = (min + max) >>> 1;
|
||||
if (arr[mid][key] === n) {
|
||||
return mid;
|
||||
} else if (arr[mid][key] < n) {
|
||||
min = mid + 1;
|
||||
} else {
|
||||
max = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
<div (mouseover)="elevated=true" (mouseout)="elevated=false" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
|
||||
<div *ngIf="!loading" class="download-time">
|
||||
<mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
|
||||
|
||||
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
|
||||
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
|
||||
</div>
|
||||
<div *ngIf="!loading" class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon> {{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</div>
|
||||
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
|
||||
<!-- The context menu trigger must be kept above the "more info" menu -->
|
||||
<div style="visibility: hidden; position: fixed"
|
||||
@@ -12,7 +7,7 @@
|
||||
[style.top]="contextMenuPosition.y"
|
||||
[matMenuTriggerFor]="context_menu">
|
||||
</div>
|
||||
<button *ngIf="!file_obj || !file_obj.auto" [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #context_menu>
|
||||
<ng-container *ngIf="!loading">
|
||||
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
|
||||
|
||||
@@ -111,11 +111,6 @@
|
||||
top: 1px;
|
||||
left: 5px;
|
||||
z-index: 99999;
|
||||
width: calc(100% - 8px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.audio-video-icon {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UnifiedFileCardComponent } from './unified-file-card.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('UnifiedFileCardComponent', () => {
|
||||
let component: UnifiedFileCardComponent;
|
||||
let fixture: ComponentFixture<UnifiedFileCardComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UnifiedFileCardComponent ]
|
||||
})
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const CURRENT_VERSION = 'v4.2';
|
||||
export const CURRENT_VERSION = 'v4.1';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreatePlaylistComponent } from './create-playlist.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('CreatePlaylistComponent', () => {
|
||||
let component: CreatePlaylistComponent;
|
||||
let fixture: ComponentFixture<CreatePlaylistComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CreatePlaylistComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AboutDialogComponent } from './about-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('AboutDialogComponent', () => {
|
||||
let component: AboutDialogComponent;
|
||||
let fixture: ComponentFixture<AboutDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AboutDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddUserDialogComponent } from './add-user-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('AddUserDialogComponent', () => {
|
||||
let component: AddUserDialogComponent;
|
||||
let fixture: ComponentFixture<AddUserDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddUserDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ArgModifierDialogComponent } from './arg-modifier-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ArgModifierDialogComponent', () => {
|
||||
let component: ArgModifierDialogComponent;
|
||||
let fixture: ComponentFixture<ArgModifierDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ArgModifierDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConfirmDialogComponent } from './confirm-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ConfirmDialogComponent', () => {
|
||||
let component: ConfirmDialogComponent;
|
||||
let fixture: ComponentFixture<ConfirmDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ConfirmDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CookiesUploaderDialogComponent } from './cookies-uploader-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('CookiesUploaderDialogComponent', () => {
|
||||
let component: CookiesUploaderDialogComponent;
|
||||
let fixture: ComponentFixture<CookiesUploaderDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CookiesUploaderDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditCategoryDialogComponent } from './edit-category-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('EditCategoryDialogComponent', () => {
|
||||
let component: EditCategoryDialogComponent;
|
||||
let fixture: ComponentFixture<EditCategoryDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditCategoryDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<h4 mat-dialog-title><ng-container i18n="Edit subscription dialog title prefix">Editing</ng-container> {{sub.name}} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
|
||||
<h4 mat-dialog-title i18n="Edit subscription dialog title prefix">Editing</h4> {{sub.name}}
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox [(ngModel)]="new_sub.paused"><ng-container i18n="Paused subscription setting">Paused</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
|
||||
</div>
|
||||
@@ -34,7 +31,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-1">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditSubscriptionDialogComponent } from './edit-subscription-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('EditSubscriptionDialogComponent', () => {
|
||||
let component: EditSubscriptionDialogComponent;
|
||||
let fixture: ComponentFixture<EditSubscriptionDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditSubscriptionDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -61,13 +61,9 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
];
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialog: MatDialog, private postsService: PostsService) {
|
||||
this.sub = JSON.parse(JSON.stringify(this.data.sub));
|
||||
this.sub = this.data.sub;
|
||||
this.new_sub = JSON.parse(JSON.stringify(this.sub));
|
||||
|
||||
// ignore videos to keep requests small
|
||||
delete this.sub['videos'];
|
||||
delete this.new_sub['videos'];
|
||||
|
||||
this.audioOnlyMode = this.sub.type === 'audio';
|
||||
this.download_all = !this.sub.timerange;
|
||||
|
||||
|
||||
@@ -8,24 +8,14 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 10px; height: 40px;">
|
||||
<div style="float: left">
|
||||
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order </span>
|
||||
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order </span>
|
||||
<button (click)="togglePlaylistOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<div style="float: right">
|
||||
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add content">Add content</ng-container></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist order -->
|
||||
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
|
||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of (reverse_order ? playlist.fileNames.slice().reverse() : playlist.fileNames); let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of playlist.fileNames; let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
|
||||
<div class="add-content-button">
|
||||
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add more content">Add more content</ng-container></button>
|
||||
</div>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
|
||||
</mat-menu>
|
||||
|
||||
@@ -30,6 +30,11 @@ border: none;
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.add-content-button {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.remove-item-button {
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModifyPlaylistComponent } from './modify-playlist.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ModifyPlaylistComponent', () => {
|
||||
let component: ModifyPlaylistComponent;
|
||||
let fixture: ComponentFixture<ModifyPlaylistComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ModifyPlaylistComponent ]
|
||||
})
|
||||
|
||||
@@ -15,7 +15,6 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
available_files = [];
|
||||
all_files = [];
|
||||
playlist_updated = false;
|
||||
reverse_order = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private postsService: PostsService,
|
||||
@@ -27,8 +26,6 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist));
|
||||
this.getFiles();
|
||||
}
|
||||
|
||||
this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true';
|
||||
}
|
||||
|
||||
getFiles() {
|
||||
@@ -75,23 +72,11 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
}
|
||||
|
||||
removeContent(index) {
|
||||
if (this.reverse_order) {
|
||||
index = this.playlist.fileNames.length - 1 - index;
|
||||
}
|
||||
this.playlist.fileNames.splice(index, 1);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
togglePlaylistOrder() {
|
||||
this.reverse_order = !this.reverse_order;
|
||||
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
if (this.reverse_order) {
|
||||
event.previousIndex = this.playlist.fileNames.length - 1 - event.previousIndex;
|
||||
event.currentIndex = this.playlist.fileNames.length - 1 - event.currentIndex;
|
||||
}
|
||||
moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SetDefaultAdminDialogComponent } from './set-default-admin-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SetDefaultAdminDialogComponent', () => {
|
||||
let component: SetDefaultAdminDialogComponent;
|
||||
let fixture: ComponentFixture<SetDefaultAdminDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SetDefaultAdminDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ShareMediaDialogComponent } from './share-media-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ShareMediaDialogComponent', () => {
|
||||
let component: ShareMediaDialogComponent;
|
||||
let fixture: ComponentFixture<ShareMediaDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ShareMediaDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscribeDialogComponent } from './subscribe-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SubscribeDialogComponent', () => {
|
||||
let component: SubscribeDialogComponent;
|
||||
let fixture: ComponentFixture<SubscribeDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscribeDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h4 mat-dialog-title>{{sub.name}} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
|
||||
<h4 mat-dialog-title>{{sub.name}}</h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="info-item">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionInfoDialogComponent } from './subscription-info-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SubscriptionInfoDialogComponent', () => {
|
||||
let component: SubscriptionInfoDialogComponent;
|
||||
let fixture: ComponentFixture<SubscriptionInfoDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionInfoDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UpdateProgressDialogComponent } from './update-progress-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('UpdateProgressDialogComponent', () => {
|
||||
let component: UpdateProgressDialogComponent;
|
||||
let fixture: ComponentFixture<UpdateProgressDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UpdateProgressDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserProfileDialogComponent } from './user-profile-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('UserProfileDialogComponent', () => {
|
||||
let component: UserProfileDialogComponent;
|
||||
let fixture: ComponentFixture<UserProfileDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UserProfileDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
<div class="info-item-label"><strong><ng-container i18n="Video upload date property">Upload Date:</ng-container> </strong></div>
|
||||
<div class="info-item-value">{{file.upload_date ? file.upload_date : 'N/A'}}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-item-label"><strong><ng-container i18n="Category property">Category:</ng-container> </strong></div>
|
||||
<div class="info-item-value"><ng-container *ngIf="file.category"><mat-chip-list><mat-chip>{{file.category.name}}</mat-chip></mat-chip-list></ng-container><ng-container *ngIf="!file.category">N/A</ng-container></div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VideoInfoDialogComponent } from './video-info-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('VideoInfoDialogComponent', () => {
|
||||
let component: VideoInfoDialogComponent;
|
||||
let fixture: ComponentFixture<VideoInfoDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ VideoInfoDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DownloadItemComponent } from './download-item.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('DownloadItemComponent', () => {
|
||||
let component: DownloadItemComponent;
|
||||
let fixture: ComponentFixture<DownloadItemComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DownloadItemComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FileCardComponent } from './file-card.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('FileCardComponent', () => {
|
||||
let component: FileCardComponent;
|
||||
let fixture: ComponentFixture<FileCardComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FileCardComponent ]
|
||||
})
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class H401Interceptor implements HttpInterceptor {
|
||||
|
||||
constructor(private router: Router, private snackBar: MatSnackBar) { }
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(catchError(err => {
|
||||
if (err.status === 401) {
|
||||
localStorage.setItem('jwt_token', null);
|
||||
if (this.router.url !== '/login') {
|
||||
this.router.navigate(['/login']).then(() => {
|
||||
this.openSnackBar('Login expired, please login again.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const error = err.error.message || err.statusText;
|
||||
return throwError(error);
|
||||
}));
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InputDialogComponent } from './input-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('InputDialogComponent', () => {
|
||||
let component: InputDialogComponent;
|
||||
let fixture: ComponentFixture<InputDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ InputDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -187,3 +187,92 @@
|
||||
<h4 style="text-align: center">Custom playlists</h4>
|
||||
<app-custom-playlists></app-custom-playlists>
|
||||
</ng-container>
|
||||
|
||||
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<ng-container i18n="Audio files title">
|
||||
Audio
|
||||
</ng-container>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<ng-container i18n="Audio files description">
|
||||
Your audio files are here
|
||||
</ng-container>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp3s.length > 0;else nomp3s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
|
||||
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="true" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6 i18n="Playlists title">Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
|
||||
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="true" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('audio')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.audio.length === 0">
|
||||
<ng-container i18n="No video playlists available text">
|
||||
No playlists available. Create one from your downloading audio files by clicking the blue plus button.
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (opened)="accordionOpened('video')" (closed)="accordionClosed('video')" (mouseleave)="accordionLeft('video')" (mouseenter)="accordionEntered('video')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<ng-container i18n="Video files title">
|
||||
Video
|
||||
</ng-container>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<ng-container i18n="Video files description">
|
||||
Your video files are here
|
||||
</ng-container>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp4s.length > 0;else nomp4s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
|
||||
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="false" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6 i18n="Playlists title">Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
|
||||
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="false" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
|
||||
<!-- Add video playlist button --<
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.video.length === 0">
|
||||
<ng-container i18n="No video playlists available text">
|
||||
No playlists available. Create one from your downloading video files by clicking the blue plus button.
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>-->
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MainComponent } from './main.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('MainComponent', () => {
|
||||
let component: MainComponent;
|
||||
let fixture: ComponentFixture<MainComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ MainComponent ]
|
||||
})
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {FileCardComponent} from '../file-card/file-card.component';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import 'rxjs/add/observable/fromEvent'
|
||||
import 'rxjs/add/operator/filter'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
|
||||
@@ -243,6 +252,13 @@ export class MainComponent implements OnInit {
|
||||
this.useDefaultDownloadingAgent = this.postsService.config['Advanced']['use_default_downloading_agent'];
|
||||
this.customDownloadingAgent = this.postsService.config['Advanced']['custom_downloading_agent'];
|
||||
|
||||
|
||||
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
}
|
||||
|
||||
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
|
||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||
this.attachToInput();
|
||||
@@ -327,6 +343,61 @@ export class MainComponent implements OnInit {
|
||||
this.setCols();
|
||||
}
|
||||
|
||||
// file manager stuff
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
const mp3s = result['mp3s'];
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp3s) !== JSON.stringify(mp3s)) { this.mp3s = mp3s };
|
||||
this.playlists.audio = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.audio.length; i++) {
|
||||
const playlist = this.playlists.audio[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp3s.length; j++) {
|
||||
if (this.mp3s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp3s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
const mp4s = result['mp4s'];
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp4s) !== JSON.stringify(mp4s)) { this.mp4s = mp4s };
|
||||
this.playlists.video = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.video.length; i++) {
|
||||
const playlist = this.playlists.video[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp4s.length; j++) {
|
||||
if (this.mp4s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp4s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public setCols() {
|
||||
if (window.innerWidth <= 350) {
|
||||
this.files_cols = 1;
|
||||
@@ -374,6 +445,44 @@ export class MainComponent implements OnInit {
|
||||
return null;
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name || this.mp3s[i].id + '.mp3' === name) {
|
||||
this.mp3s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.getMp3s();
|
||||
}
|
||||
|
||||
public removePlaylistMp3(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.audio.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
for (let i = 0; i < this.mp4s.length; i++) {
|
||||
if (this.mp4s[i].id === name || this.mp4s[i].id + '.mp4' === name) {
|
||||
this.mp4s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.getMp4s();
|
||||
}
|
||||
|
||||
public removePlaylistMp4(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'video').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.video.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
|
||||
// download helpers
|
||||
|
||||
downloadHelperMp3(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) {
|
||||
@@ -403,6 +512,16 @@ export class MainComponent implements OnInit {
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp3s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
setTimeout(() => {
|
||||
this.audioFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
downloadHelperMp4(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) {
|
||||
@@ -432,6 +551,16 @@ export class MainComponent implements OnInit {
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp4s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp4s();
|
||||
setTimeout(() => {
|
||||
this.videoFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// download click handler
|
||||
@@ -624,6 +753,8 @@ export class MainComponent implements OnInit {
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, 'video').subscribe(delRes => {
|
||||
// reload mp3s
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -639,6 +770,8 @@ export class MainComponent implements OnInit {
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, 'audio').subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -985,6 +1118,25 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
// creating a playlist
|
||||
openCreatePlaylistDialog(type) {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
filesToSelectFrom: (type === 'audio') ? this.mp3s : this.mp4s,
|
||||
type: type
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
if (type === 'audio') { this.getMp3s() };
|
||||
if (type === 'video') { this.getMp4s() };
|
||||
this.openSnackBar('Successfully created playlist!', '');
|
||||
} else if (result === false) {
|
||||
this.openSnackBar('ERROR: failed to create playlist!', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// modify custom args
|
||||
openArgsModifierDialog() {
|
||||
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
}
|
||||
|
||||
.spinner {
|
||||
bottom: 1px;
|
||||
left: 2px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,35 @@
|
||||
<div style="height: 100%" *ngIf="playlist.length > 0 && show_player">
|
||||
<div style="height: 100%" [ngClass]="(type === 'audio') ? null : 'container-video'">
|
||||
<div style="max-width: 100%; margin-left: 0px; height: 100%">
|
||||
<mat-drawer-container style="height: 100%" class="example-container" autosize>
|
||||
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
|
||||
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||
</video>
|
||||
</vg-player>
|
||||
</div>
|
||||
<div *ngIf="db_file" style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-2 col-lg-1">
|
||||
<ng-container *ngIf="db_file">{{db_file['local_view_count'] ? db_file['local_view_count']+1 : 1}} <ng-container i18n="View count label">views</ng-container></ng-container>
|
||||
</div>
|
||||
<div style="white-space: pre-line;" class="col-8 col-lg-9">
|
||||
<ng-container *ngIf="db_file && db_file['description']">
|
||||
<p>
|
||||
<app-see-more [text]="db_file['description']"></app-see-more>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!db_file || !db_file['description']">
|
||||
<p style="text-align: center;">
|
||||
No description available.
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ng-container *ngIf="playlist.length > 1">
|
||||
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
|
||||
<button *ngIf="!id" color="accent" (click)="namePlaylistDialog()" mat-icon-button><mat-icon>favorite</mat-icon></button>
|
||||
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="playlist.length === 1">
|
||||
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
|
||||
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||
</ng-container>
|
||||
<button *ngIf="db_file && db_file.url.includes('twitch.tv/videos/') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
|
||||
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv/videos/')">
|
||||
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
||||
</ng-container>
|
||||
</mat-drawer>
|
||||
|
||||
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
||||
<div class="spinner-div">
|
||||
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container> <mat-icon>update</mat-icon></button>
|
||||
|
||||
</div>
|
||||
</mat-drawer-container>
|
||||
<div *ngIf="playlist.length > 0 && show_player">
|
||||
<div [ngClass]="(type === 'audio') ? null : 'container-video'">
|
||||
<div style="max-width: 100%; margin-left: 0px; height: 70vh">
|
||||
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
|
||||
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||
</video>
|
||||
</vg-player>
|
||||
</div>
|
||||
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
||||
<div class="spinner-div">
|
||||
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container> <mat-icon>update</mat-icon></button>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="playlist.length > 1">
|
||||
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
|
||||
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
|
||||
</div>
|
||||
<div *ngIf="playlist.length === 1">
|
||||
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PlayerComponent } from './player.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('PlayerComponent', () => {
|
||||
let component: PlayerComponent;
|
||||
let fixture: ComponentFixture<PlayerComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PlayerComponent ]
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||
import { VgApiService } from '@videogular/ngx-videogular/core';
|
||||
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit } from '@angular/core';
|
||||
import { VgAPI } from 'ngx-videogular';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -7,7 +7,6 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component';
|
||||
import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component';
|
||||
|
||||
export interface IMedia {
|
||||
title: string;
|
||||
@@ -31,15 +30,13 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
currentIndex = 0;
|
||||
currentItem: IMedia = null;
|
||||
api: VgApiService;
|
||||
api_ready = false;
|
||||
api: VgAPI;
|
||||
|
||||
// params
|
||||
fileNames: string[];
|
||||
type: string;
|
||||
id = null; // used for playlists (not subscription)
|
||||
uid = null; // used for non-subscription files (audio, video, playlist)
|
||||
subscription = null;
|
||||
subscriptionName = null;
|
||||
subPlaylist = null;
|
||||
uuid = null; // used for sharing in multi-user mode, uuid is the user that downloaded the video
|
||||
@@ -68,8 +65,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
save_volume_timer = null;
|
||||
original_volume = null;
|
||||
|
||||
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.innerWidth = window.innerWidth;
|
||||
@@ -160,10 +155,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.openSnackBar('Failed to get file information from the server.', 'Dismiss');
|
||||
return;
|
||||
}
|
||||
this.postsService.incrementViewCount(this.db_file['uid'], null, this.uuid).subscribe(res => {}, err => {
|
||||
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
|
||||
@@ -185,15 +176,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
getSubscription() {
|
||||
this.postsService.getSubscription(null, this.subscriptionName).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();
|
||||
}
|
||||
@@ -207,10 +193,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
getPlaylistFiles() {
|
||||
if (this.route.snapshot.paramMap.get('auto') === 'true') {
|
||||
this.show_player = true;
|
||||
return;
|
||||
}
|
||||
this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => {
|
||||
if (res['playlist']) {
|
||||
this.db_playlist = res['playlist'];
|
||||
@@ -286,9 +268,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.original_playlist = JSON.stringify(this.playlist);
|
||||
}
|
||||
|
||||
onPlayerReady(api: VgApiService) {
|
||||
onPlayerReady(api: VgAPI) {
|
||||
this.api = api;
|
||||
this.api_ready = true;
|
||||
|
||||
// checks if volume has been previously set. if so, use that as default
|
||||
if (localStorage.getItem('player_volume')) {
|
||||
|
||||
@@ -234,14 +234,6 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
getFullTwitchChat(id, type, uuid = null, sub = null) {
|
||||
return this.http.post(this.path + 'getFullTwitchChat', {id: id, type: type, uuid: uuid, sub: sub}, this.httpOptions);
|
||||
}
|
||||
|
||||
downloadTwitchChat(id, type, vodId, uuid = null, sub = null) {
|
||||
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,
|
||||
@@ -286,10 +278,6 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'enableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
|
||||
}
|
||||
|
||||
incrementViewCount(file_uid, sub_id, uuid) {
|
||||
return this.http.post(this.path + 'incrementViewCount', {file_uid: file_uid, sub_id: sub_id, uuid: uuid}, this.httpOptions);
|
||||
}
|
||||
|
||||
disableSharing(uid, type, is_playlist) {
|
||||
return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
|
||||
}
|
||||
@@ -374,7 +362,7 @@ export class PostsService implements CanActivate {
|
||||
}
|
||||
|
||||
getAllSubscriptions() {
|
||||
return this.http.post(this.path + 'getSubscriptions', {}, this.httpOptions);
|
||||
return this.http.post(this.path + 'getAllSubscriptions', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
// current downloads
|
||||
|
||||
@@ -53,15 +53,12 @@
|
||||
<mat-hint><ng-container i18n="Subscriptions base path setting input hint">Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<div class="col-12 mt-5 mb-3">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_check_interval']" matInput placeholder="Check interval" i18n-placeholder="Check interval input setting placeholder">
|
||||
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-3">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Subscriptions']['redownload_fresh_uploads']" matTooltip="Sometimes new videos are downloaded before being fully processed. This setting will mean new videos will be checked for a higher quality version the following day." i18n-matTooltip="Redownload fresh uploads tooltip"><ng-container i18n="Redownload fresh uploads">Redownload fresh uploads</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
@@ -231,24 +228,12 @@
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12 mb-2">
|
||||
<div class="col-12 mb-3">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['API']['use_youtube_API']" [(ngModel)]="new_config['API']['youtube_API_key']" matInput placeholder="Youtube API Key" i18n-placeholder="Youtube API Key setting placeholder" required>
|
||||
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12 mb-5">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_API_key']" matInput placeholder="Twitch API Key" i18n-placeholder="Twitch API Key setting placeholder" required>
|
||||
<mat-hint><ng-container i18n="Twitch API Key setting hint AKA preamble">Also known as a Client ID.</ng-container> <a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SettingsComponent } from './settings.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SettingsComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionFileCardComponent } from './subscription-file-card.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SubscriptionFileCardComponent', () => {
|
||||
let component: SubscriptionFileCardComponent;
|
||||
let fixture: ComponentFixture<SubscriptionFileCardComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionFileCardComponent ]
|
||||
})
|
||||
|
||||
@@ -15,6 +15,9 @@ export class SubscriptionFileCardComponent implements OnInit {
|
||||
image_errored = false;
|
||||
image_loaded = false;
|
||||
|
||||
scrollSubject;
|
||||
scrollAndLoad;
|
||||
|
||||
formattedDuration = null;
|
||||
|
||||
@Input() file;
|
||||
@@ -24,7 +27,13 @@ export class SubscriptionFileCardComponent implements OnInit {
|
||||
@Output() goToFileEmit = new EventEmitter<any>();
|
||||
@Output() reloadSubscription = new EventEmitter<boolean>();
|
||||
|
||||
constructor(private snackBar: MatSnackBar, private postsService: PostsService, private dialog: MatDialog) {}
|
||||
constructor(private snackBar: MatSnackBar, private postsService: PostsService, private dialog: MatDialog) {
|
||||
this.scrollSubject = new Subject();
|
||||
this.scrollAndLoad = Observable.merge(
|
||||
Observable.fromEvent(window, 'scroll'),
|
||||
this.scrollSubject
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.file.duration) {
|
||||
@@ -36,6 +45,10 @@ export class SubscriptionFileCardComponent implements OnInit {
|
||||
this.image_errored = true;
|
||||
}
|
||||
|
||||
onHoverResponse() {
|
||||
this.scrollSubject.next();
|
||||
}
|
||||
|
||||
imageLoaded(loaded) {
|
||||
this.image_loaded = true;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
<button class="back-button" (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<h2 style="text-align: center;" *ngIf="subscription">
|
||||
{{subscription.name}} <ng-container *ngIf="subscription.paused" i18n="Paused suffix">(Paused)</ng-container>
|
||||
{{subscription.name}}
|
||||
</h2>
|
||||
<mat-progress-bar style="width: 80%; margin: 0 auto; margin-top: 15px;" *ngIf="subscription && subscription.downloading" mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
||||
<br/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionComponent } from './subscription.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SubscriptionComponent', () => {
|
||||
let component: SubscriptionComponent;
|
||||
let fixture: ComponentFixture<SubscriptionComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -9,7 +9,7 @@ import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-d
|
||||
templateUrl: './subscription.component.html',
|
||||
styleUrls: ['./subscription.component.scss']
|
||||
})
|
||||
export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
export class SubscriptionComponent implements OnInit {
|
||||
|
||||
id = null;
|
||||
subscription = null;
|
||||
@@ -44,11 +44,22 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
filterProperty = this.filterProperties['upload_date'];
|
||||
downloading = false;
|
||||
sub_interval = null;
|
||||
|
||||
initialized = false;
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.paramMap.subscribe((params: ParamMap) => {
|
||||
this.id = params.get('id');
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.initialized = true;
|
||||
this.getConfig();
|
||||
this.getSubscription();
|
||||
}
|
||||
});
|
||||
});
|
||||
if (this.route.snapshot.paramMap.get('id')) {
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
@@ -56,7 +67,6 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
if (init) {
|
||||
this.getConfig();
|
||||
this.getSubscription();
|
||||
this.sub_interval = setInterval(() => this.getSubscription(true), 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -68,25 +78,12 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// prevents subscription getter from running in the background
|
||||
if (this.sub_interval) {
|
||||
clearInterval(this.sub_interval);
|
||||
}
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['/subscriptions']);
|
||||
}
|
||||
|
||||
getSubscription(low_cost = false) {
|
||||
getSubscription() {
|
||||
this.postsService.getSubscription(this.id).subscribe(res => {
|
||||
if (low_cost && res['subscription'].videos.length === this.subscription?.videos.length) {
|
||||
if (res['subscription']['downloading'] !== this.subscription['downloading']) {
|
||||
this.subscription['downloading'] = res['subscription']['downloading'];
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.subscription = res['subscription'];
|
||||
this.files = res['files'];
|
||||
if (this.search_mode) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<mat-nav-list class="sub-nav-list">
|
||||
<mat-list-item *ngFor="let sub of channel_subscriptions">
|
||||
<a class="a-list-item" matLine (click)="goToSubscription(sub)" href="javascript:void(0)">
|
||||
<strong *ngIf="sub.name">{{ sub.name }} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></strong>
|
||||
<strong *ngIf="sub.name">{{ sub.name }}</strong>
|
||||
<div *ngIf="!sub.name">
|
||||
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<mat-nav-list class="sub-nav-list">
|
||||
<mat-list-item *ngFor="let sub of playlist_subscriptions">
|
||||
<a class="a-list-item" matLine (click)="goToSubscription(sub)" href="javascript:void(0)">
|
||||
<strong>{{ sub.name }} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></strong>
|
||||
<strong>{{ sub.name }}</strong>
|
||||
<div class="content-loading-div" *ngIf="!sub.name">
|
||||
<ng-container i18n="Subscription playlist not available text">Name not available. Playlist retrieval in progress.</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionsComponent } from './subscriptions.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SubscriptionsComponent', () => {
|
||||
let component: SubscriptionsComponent;
|
||||
let fixture: ComponentFixture<SubscriptionsComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionsComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UpdaterComponent } from './updater.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('UpdaterComponent', () => {
|
||||
let component: UpdaterComponent;
|
||||
let fixture: ComponentFixture<UpdaterComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UpdaterComponent ]
|
||||
})
|
||||
|
||||
16
src/app/youtube.service.spec.ts
Normal file
16
src/app/youtube.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { YoutubeService } from './youtube.service';
|
||||
|
||||
describe('YoutubeService', () => {
|
||||
let service: YoutubeService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(YoutubeService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
116
src/app/youtube.service.ts
Normal file
116
src/app/youtube.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class Result {
|
||||
id: string
|
||||
title: string
|
||||
desc: string
|
||||
thumbnailUrl: string
|
||||
videoUrl: string
|
||||
uploaded: any;
|
||||
|
||||
constructor(obj?: any) {
|
||||
this.id = obj && obj.id || null
|
||||
this.title = obj && obj.title || null
|
||||
this.desc = obj && obj.desc || null
|
||||
this.thumbnailUrl = obj && obj.thumbnailUrl || null
|
||||
this.uploaded = obj && obj.uploaded || null
|
||||
this.videoUrl = obj && obj.videoUrl || `https://www.youtube.com/watch?v=${this.id}`
|
||||
|
||||
this.uploaded = formatDate(Date.parse(this.uploaded));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class YoutubeService {
|
||||
|
||||
base_url = 'https://www.googleapis.com/youtube/v3/';
|
||||
key = null;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
initializeAPI(key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
search(query: string): Observable<Result[]> {
|
||||
const url_sub_path = 'search';
|
||||
|
||||
if (this.ValidURL(query)) {
|
||||
return new Observable<Result[]>();
|
||||
}
|
||||
const params: string = [
|
||||
`q=${query}`,
|
||||
`key=${this.key}`,
|
||||
`part=snippet`,
|
||||
`type=video`,
|
||||
`maxResults=5`
|
||||
].join('&')
|
||||
const queryUrl = `${this.url}?${params}`
|
||||
return this.http.get(queryUrl).map(response => {
|
||||
return <any>response['items'].map(item => {
|
||||
return new Result({
|
||||
id: item.id.videoId,
|
||||
title: item.snippet.title,
|
||||
desc: item.snippet.description,
|
||||
thumbnailUrl: item.snippet.thumbnails.high.url,
|
||||
uploaded: item.snippet.publishedAt
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getSubscribedChannels() {
|
||||
const url_sub_path = ''
|
||||
// on the first iteration, don't use a token. but because of the 50 channels limit, you need to use the returned token
|
||||
// to retrieve the next list of 50 channels until a next token is not given
|
||||
// https://stackoverflow.com/questions/52803732/youtube-api-v3-maximum-number-of-videos-only-50
|
||||
// https://developers.google.com/youtube/v3/docs/subscriptions/list?apix_params=%7B%22part%22%3A%5B%22snippet%2CcontentDetails%22%5D%2C%22maxResults%22%3A50%2C%22mine%22%3Atrue%2C%22pageToken%22%3A%22CGQQAA%22%7D
|
||||
}
|
||||
|
||||
getSubscribedChannelsWithToken() {
|
||||
|
||||
}
|
||||
|
||||
// checks if url is a valid URL
|
||||
ValidURL(str) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
return re.test(str);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateVal) {
|
||||
const newDate = new Date(dateVal);
|
||||
|
||||
const sMonth = padValue(newDate.getMonth() + 1);
|
||||
const sDay = padValue(newDate.getDate());
|
||||
const sYear = newDate.getFullYear();
|
||||
let sHour: any;
|
||||
sHour = newDate.getHours();
|
||||
const sMinute = padValue(newDate.getMinutes());
|
||||
let sAMPM = 'AM';
|
||||
|
||||
const iHourCheck = parseInt(sHour, 10);
|
||||
|
||||
if (iHourCheck > 12) {
|
||||
sAMPM = 'PM';
|
||||
sHour = iHourCheck - 12;
|
||||
} else if (iHourCheck === 0) {
|
||||
sHour = '12';
|
||||
}
|
||||
|
||||
sHour = padValue(sHour);
|
||||
|
||||
return sMonth + '-' + sDay + '-' + sYear + ' ' + sHour + ':' + sMinute + ' ' + sAMPM;
|
||||
}
|
||||
|
||||
function padValue(value) {
|
||||
return (value < 10) ? '0' + value : value;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": ""
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
@@ -59,8 +57,7 @@
|
||||
"allow_advanced_download": true,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "debug",
|
||||
"use_cookies": false,
|
||||
"default_downloader": "youtube-dlc"
|
||||
"use_cookies": false
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user