mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-04-05 20:41:29 +03:00
added basic subscriptions support for playlists and channels
update youtube-dl binary on windows updated favicon to the new icon
This commit is contained in:
165
backend/app.js
165
backend/app.js
@@ -1,4 +1,5 @@
|
|||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
const { uuid } = require('uuidv4');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var youtubedl = require('youtube-dl');
|
var youtubedl = require('youtube-dl');
|
||||||
@@ -10,7 +11,9 @@ var archiver = require('archiver');
|
|||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
var URL = require('url').URL;
|
var URL = require('url').URL;
|
||||||
const shortid = require('shortid')
|
const shortid = require('shortid')
|
||||||
|
const url_api = require('url');
|
||||||
var config_api = require('./config.js');
|
var config_api = require('./config.js');
|
||||||
|
var subscriptions_api = require('./subscriptions')
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
@@ -25,7 +28,8 @@ db.defaults(
|
|||||||
audio: [],
|
audio: [],
|
||||||
video: []
|
video: []
|
||||||
},
|
},
|
||||||
configWriteFlag: false
|
configWriteFlag: false,
|
||||||
|
subscriptions: []
|
||||||
}).write();
|
}).write();
|
||||||
|
|
||||||
// config values
|
// config values
|
||||||
@@ -39,6 +43,8 @@ var videoFolderPath = null;
|
|||||||
var downloadOnlyMode = null;
|
var downloadOnlyMode = null;
|
||||||
var useDefaultDownloadingAgent = null;
|
var useDefaultDownloadingAgent = null;
|
||||||
var customDownloadingAgent = null;
|
var customDownloadingAgent = null;
|
||||||
|
var allowSubscriptions = null;
|
||||||
|
var subscriptionsCheckInterval = null;
|
||||||
|
|
||||||
// other needed values
|
// other needed values
|
||||||
var options = null; // encryption options
|
var options = null; // encryption options
|
||||||
@@ -129,6 +135,9 @@ async function loadConfig() {
|
|||||||
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
|
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
|
||||||
useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
|
useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
|
||||||
customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
|
customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
|
||||||
|
allowSubscriptions = config_api.getConfigItem('ytdl_allow_subscriptions');
|
||||||
|
subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||||
|
|
||||||
if (!useDefaultDownloadingAgent && validDownloadingAgents.indexOf(customDownloadingAgent) !== -1 ) {
|
if (!useDefaultDownloadingAgent && validDownloadingAgents.indexOf(customDownloadingAgent) !== -1 ) {
|
||||||
console.log(`INFO: Using non-default downloading agent \'${customDownloadingAgent}\'`)
|
console.log(`INFO: Using non-default downloading agent \'${customDownloadingAgent}\'`)
|
||||||
}
|
}
|
||||||
@@ -149,6 +158,11 @@ async function loadConfig() {
|
|||||||
|
|
||||||
url_domain = new URL(url);
|
url_domain = new URL(url);
|
||||||
|
|
||||||
|
// get subscriptions
|
||||||
|
if (allowSubscriptions) {
|
||||||
|
watchSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
// start the server here
|
// start the server here
|
||||||
startServer();
|
startServer();
|
||||||
|
|
||||||
@@ -157,6 +171,34 @@ async function loadConfig() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateSubcriptionRetrievalDelay(amount) {
|
||||||
|
// frequency is 5 mins
|
||||||
|
let frequency_in_ms = subscriptionsCheckInterval * 1000;
|
||||||
|
let minimum_frequency = 60 * 1000;
|
||||||
|
const first_frequency = frequency_in_ms/amount;
|
||||||
|
return (first_frequency < minimum_frequency) ? minimum_frequency : first_frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchSubscriptions() {
|
||||||
|
let subscriptions = subscriptions_api.getAllSubscriptions();
|
||||||
|
|
||||||
|
let subscriptions_amount = subscriptions.length;
|
||||||
|
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
||||||
|
|
||||||
|
let current_delay = 0;
|
||||||
|
for (let i = 0; i < subscriptions.length; i++) {
|
||||||
|
let sub = subscriptions[i];
|
||||||
|
console.log('watching ' + sub.name + ' with delay interval of ' + delay_interval);
|
||||||
|
setTimeout(() => {
|
||||||
|
setInterval(() => {
|
||||||
|
subscriptions_api.getVideosForSub(sub);
|
||||||
|
}, subscriptionsCheckInterval * 1000);
|
||||||
|
}, current_delay);
|
||||||
|
current_delay += delay_interval;
|
||||||
|
if (current_delay >= subscriptionsCheckInterval * 1000) current_delay = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getOrigin() {
|
function getOrigin() {
|
||||||
return url_domain.origin;
|
return url_domain.origin;
|
||||||
}
|
}
|
||||||
@@ -239,9 +281,14 @@ function getJSONMp3(name)
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJSONMp4(name)
|
function getJSONMp4(name, customPath = null)
|
||||||
{
|
{
|
||||||
var jsonPath = videoFolderPath+name+".info.json";
|
let jsonPath = null;
|
||||||
|
if (!customPath) {
|
||||||
|
jsonPath = videoFolderPath+name+".info.json";
|
||||||
|
} else {
|
||||||
|
jsonPath = customPath + name + ".info.json";
|
||||||
|
}
|
||||||
if (fs.existsSync(jsonPath))
|
if (fs.existsSync(jsonPath))
|
||||||
{
|
{
|
||||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
@@ -802,6 +849,109 @@ app.post('/api/getMp4s', function(req, res) {
|
|||||||
res.end("yes");
|
res.end("yes");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/subscribe', async (req, res) => {
|
||||||
|
let name = req.body.name;
|
||||||
|
let url = req.body.url;
|
||||||
|
let timerange = req.body.timerange;
|
||||||
|
|
||||||
|
const new_sub = {
|
||||||
|
name: name,
|
||||||
|
url: url,
|
||||||
|
id: uuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
// adds timerange if it exists, otherwise all videos will be downloaded
|
||||||
|
if (timerange) {
|
||||||
|
new_sub.timerange = timerange;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result_obj = await subscriptions_api.subscribe(new_sub);
|
||||||
|
|
||||||
|
if (result_obj.success) {
|
||||||
|
res.send({
|
||||||
|
new_sub: new_sub
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.send({
|
||||||
|
new_sub: null,
|
||||||
|
error: result_obj.error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/unsubscribe', async (req, res) => {
|
||||||
|
let deleteMode = req.body.deleteMode
|
||||||
|
let sub = req.body.sub;
|
||||||
|
|
||||||
|
let result_obj = subscriptions_api.unsubscribe(sub, deleteMode);
|
||||||
|
if (result_obj.success) {
|
||||||
|
res.send({
|
||||||
|
success: result_obj.success
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
error: result_obj.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/getSubscription', async (req, res) => {
|
||||||
|
let subID = req.body.id;
|
||||||
|
|
||||||
|
// get sub from db
|
||||||
|
let subscription = subscriptions_api.getSubscription(subID);
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
// failed to get subscription from db, send 400 error
|
||||||
|
res.sendStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get sub videos
|
||||||
|
let base_path = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
let appended_base_path = path.join(base_path, subscription.isPlaylist ? 'playlists' : 'channels', subscription.name, '/');
|
||||||
|
var files = recFindByExt(appended_base_path, 'mp4');
|
||||||
|
var parsed_files = [];
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i];
|
||||||
|
var file_path = file.substring(appended_base_path.length, file.length);
|
||||||
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
|
var jsonobj = getJSONMp4(id, appended_base_path);
|
||||||
|
if (!jsonobj) continue;
|
||||||
|
var title = jsonobj.title;
|
||||||
|
|
||||||
|
var thumbnail = jsonobj.thumbnail;
|
||||||
|
var duration = jsonobj.duration;
|
||||||
|
var isaudio = false;
|
||||||
|
var file_obj = new File(id, title, thumbnail, isaudio, duration);
|
||||||
|
parsed_files.push(file_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
subscription: subscription,
|
||||||
|
files: parsed_files
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/downloadVideosForSubscription', async (req, res) => {
|
||||||
|
let subID = req.body.subID;
|
||||||
|
let sub = subscriptions_api.getSubscription(subID);
|
||||||
|
subscriptions_api.getVideosForSub(sub);
|
||||||
|
res.send({
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/getAllSubscriptions', async (req, res) => {
|
||||||
|
// get subs from api
|
||||||
|
let subscriptions = subscriptions_api.getAllSubscriptions();
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
subscriptions: subscriptions
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/createPlaylist', async (req, res) => {
|
app.post('/api/createPlaylist', async (req, res) => {
|
||||||
let playlistName = req.body.playlistName;
|
let playlistName = req.body.playlistName;
|
||||||
let fileNames = req.body.fileNames;
|
let fileNames = req.body.fileNames;
|
||||||
@@ -948,8 +1098,15 @@ app.post('/api/deleteFile', async (req, res) => {
|
|||||||
|
|
||||||
app.get('/api/video/:id', function(req , res){
|
app.get('/api/video/:id', function(req , res){
|
||||||
var head;
|
var head;
|
||||||
|
let optionalParams = url_api.parse(req.url,true).query;
|
||||||
let id = decodeURIComponent(req.params.id);
|
let id = decodeURIComponent(req.params.id);
|
||||||
const path = "video/" + id + '.mp4';
|
let path = "video/" + id + '.mp4';
|
||||||
|
if (optionalParams['subName']) {
|
||||||
|
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
const isPlaylist = optionalParams['subPlaylist'];
|
||||||
|
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
||||||
|
path = basePath + optionalParams['subName'] + '/' + id + '.mp4';
|
||||||
|
}
|
||||||
const stat = fs.statSync(path)
|
const stat = fs.statSync(path)
|
||||||
const fileSize = stat.size
|
const fileSize = stat.size
|
||||||
const range = req.headers.range
|
const range = req.headers.range
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ function setConfigFile(config) {
|
|||||||
|
|
||||||
function getConfigItem(key) {
|
function getConfigItem(key) {
|
||||||
let config_json = getConfigFile();
|
let config_json = getConfigFile();
|
||||||
if (!CONFIG_ITEMS[key]) console.log('cannot find config with key ' + key);
|
if (!CONFIG_ITEMS[key]) {
|
||||||
|
console.log('cannot find config with key ' + key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
let path = CONFIG_ITEMS[key]['path'];
|
let path = CONFIG_ITEMS[key]['path'];
|
||||||
return Object.byString(config_json, path);
|
return Object.byString(config_json, path);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,12 @@
|
|||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
"allow_theme_change": true
|
"allow_theme_change": true
|
||||||
},
|
},
|
||||||
|
"Subscriptions": {
|
||||||
|
"allow_subscriptions": true,
|
||||||
|
"subscriptions_base_path": "subscriptions/",
|
||||||
|
"subscriptions_check_interval": "300",
|
||||||
|
"subscriptions_use_youtubedl_archive": true
|
||||||
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
"use_default_downloading_agent": true,
|
"use_default_downloading_agent": true,
|
||||||
"custom_downloading_agent": "",
|
"custom_downloading_agent": "",
|
||||||
|
|||||||
@@ -28,6 +28,12 @@
|
|||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
"allow_theme_change": true
|
"allow_theme_change": true
|
||||||
},
|
},
|
||||||
|
"Subscriptions": {
|
||||||
|
"allow_subscriptions": true,
|
||||||
|
"subscriptions_base_path": "subscriptions/",
|
||||||
|
"subscriptions_check_interval": "300",
|
||||||
|
"subscriptions_use_youtubedl_archive": true
|
||||||
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
"use_default_downloading_agent": true,
|
"use_default_downloading_agent": true,
|
||||||
"custom_downloading_agent": "",
|
"custom_downloading_agent": "",
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_allow_multi_download_mode',
|
'key': 'ytdl_allow_multi_download_mode',
|
||||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// API
|
// API
|
||||||
'ytdl_use_youtube_api': {
|
'ytdl_use_youtube_api': {
|
||||||
@@ -78,6 +77,28 @@ let CONFIG_ITEMS = {
|
|||||||
'path': 'YoutubeDLMaterial.Themes.allow_theme_change'
|
'path': 'YoutubeDLMaterial.Themes.allow_theme_change'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Subscriptions
|
||||||
|
'ytdl_allow_subscriptions': {
|
||||||
|
'key': 'ytdl_allow_subscriptions',
|
||||||
|
'path': 'YoutubeDLMaterial.Subscriptions.allow_subscriptions'
|
||||||
|
},
|
||||||
|
'ytdl_subscriptions_base_path': {
|
||||||
|
'key': 'ytdl_subscriptions_base_path',
|
||||||
|
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_base_path'
|
||||||
|
},
|
||||||
|
'ytdl_subscriptions_check_interval': {
|
||||||
|
'key': 'ytdl_subscriptions_check_interval',
|
||||||
|
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||||
|
},
|
||||||
|
'ytdl_subscriptions_check_interval': {
|
||||||
|
'key': 'ytdl_subscriptions_check_interval',
|
||||||
|
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||||
|
},
|
||||||
|
'ytdl_subscriptions_use_youtubedl_archive': {
|
||||||
|
'key': 'ytdl_use_youtubedl_archive',
|
||||||
|
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_use_youtubedl_archive'
|
||||||
|
},
|
||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
'ytdl_use_default_downloading_agent': {
|
'ytdl_use_default_downloading_agent': {
|
||||||
'key': 'ytdl_use_default_downloading_agent',
|
'key': 'ytdl_use_default_downloading_agent',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
"uuidv4": "^6.0.6",
|
||||||
"youtube-dl": "^3.0.2"
|
"youtube-dl": "^3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
203
backend/subscriptions.js
Normal file
203
backend/subscriptions.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
const low = require('lowdb')
|
||||||
|
const FileSync = require('lowdb/adapters/FileSync')
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
const { uuid } = require('uuidv4');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
var youtubedl = require('youtube-dl');
|
||||||
|
const config_api = require('./config');
|
||||||
|
|
||||||
|
const adapter = new FileSync('db.json');
|
||||||
|
const db = low(adapter)
|
||||||
|
|
||||||
|
let debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
|
async function subscribe(sub) {
|
||||||
|
const result_obj = {
|
||||||
|
success: false,
|
||||||
|
error: ''
|
||||||
|
};
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
// sub should just have url and name. here we will get isPlaylist and path
|
||||||
|
sub.isPlaylist = sub.url.includes('playlist');
|
||||||
|
|
||||||
|
if (db.get('subscriptions').find({url: sub.url}).value()) {
|
||||||
|
console.log('Sub already exists');
|
||||||
|
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
|
||||||
|
resolve(result_obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add sub to db
|
||||||
|
db.get('subscriptions').push(sub).write();
|
||||||
|
|
||||||
|
await getVideosForSub(sub);
|
||||||
|
result_obj.success = true;
|
||||||
|
result_obj.sub = sub;
|
||||||
|
resolve(result_obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unsubscribe(sub, deleteMode) {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
const basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
let result_obj = { success: false, error: '' };
|
||||||
|
|
||||||
|
let id = sub.id;
|
||||||
|
db.get('subscriptions').remove({id: id}).write();
|
||||||
|
|
||||||
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
if (deleteMode && fs.existsSync(appendedBasePath)) {
|
||||||
|
if (sub.archive && fs.existsSync(sub.archive)) {
|
||||||
|
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
||||||
|
// deletes archive if it exists
|
||||||
|
if (fs.existsSync(archive_file_path)) {
|
||||||
|
fs.unlinkSync(archive_file_path);
|
||||||
|
}
|
||||||
|
fs.rmdirSync(sub.archive);
|
||||||
|
}
|
||||||
|
deleteFolderRecursive(appendedBasePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVideosForSub(sub) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||||
|
|
||||||
|
const appendedBasePath = basePath + (sub.isPlaylist ? 'playlists/%(playlist_title)s' : 'channels/%(uploader)s');
|
||||||
|
|
||||||
|
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-annotations', '--write-thumbnail', '--write-info-json', '--print-json'];
|
||||||
|
|
||||||
|
if (sub.timerange) {
|
||||||
|
downloadConfig.push('--dateafter', sub.timerange);
|
||||||
|
}
|
||||||
|
|
||||||
|
let archive_dir = null;
|
||||||
|
let archive_path = null;
|
||||||
|
let usingTempArchive = false;
|
||||||
|
|
||||||
|
if (useArchive) {
|
||||||
|
if (sub.archive) {
|
||||||
|
archive_dir = sub.archive;
|
||||||
|
archive_path = path.join(archive_dir, 'archive.txt')
|
||||||
|
} else {
|
||||||
|
usingTempArchive = true;
|
||||||
|
|
||||||
|
// set temporary archive
|
||||||
|
archive_dir = basePath + 'archives/' + sub.id;
|
||||||
|
archive_path = path.join(archive_dir, sub.id + '.txt');
|
||||||
|
|
||||||
|
// create temporary dir and archive txt
|
||||||
|
if (!fs.existsSync(archive_dir)) {
|
||||||
|
fs.mkdirSync(archive_dir);
|
||||||
|
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get videos
|
||||||
|
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||||
|
if (debugMode) {
|
||||||
|
console.log('Subscribe: got videos for subscription ' + sub.name);
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
console.log(err.stderr);
|
||||||
|
resolve(false);
|
||||||
|
} else if (output) {
|
||||||
|
if (output.length === 0) {
|
||||||
|
if (debugMode) console.log('No additional videos to download for ' + sub.name);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sub.name && output_json) {
|
||||||
|
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
|
||||||
|
// if it's now valid, update
|
||||||
|
if (sub.name) {
|
||||||
|
db.get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usingTempArchive && !sub.archive && sub.name) {
|
||||||
|
let new_archive_dir = basePath + 'archives/' + sub.name;
|
||||||
|
|
||||||
|
// TODO: clean up, code looks ugly
|
||||||
|
if (fs.existsSync(new_archive_dir)) {
|
||||||
|
if (fs.existsSync(new_archive_dir + '/archive.txt')) {
|
||||||
|
console.log('INFO: Archive file already exists. Rewriting archive.');
|
||||||
|
fs.unlinkSync(new_archive_dir + '/archive.txt')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// creates archive directory for subscription
|
||||||
|
fs.mkdirSync(new_archive_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves archive
|
||||||
|
fs.copyFileSync(archive_path, new_archive_dir + '/archive.txt');
|
||||||
|
|
||||||
|
// updates subscription
|
||||||
|
sub.archive = new_archive_dir;
|
||||||
|
db.get('subscriptions').find({id: sub.id}).assign({archive: new_archive_dir}).write();
|
||||||
|
|
||||||
|
// remove temporary archive directory
|
||||||
|
fs.unlinkSync(archive_path);
|
||||||
|
fs.rmdirSync(archive_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllSubscriptions() {
|
||||||
|
const subscriptions = db.get('subscriptions').value();
|
||||||
|
return subscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubscription(subID) {
|
||||||
|
return db.get('subscriptions').find({id: subID}).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper functions
|
||||||
|
|
||||||
|
function getAppendedBasePath(sub, base_path) {
|
||||||
|
return base_path + (sub.isPlaylist ? 'playlists/' : 'channels/') + sub.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/32197381/8088021
|
||||||
|
const deleteFolderRecursive = function(folder_to_delete) {
|
||||||
|
if (fs.existsSync(folder_to_delete)) {
|
||||||
|
fs.readdirSync(folder_to_delete).forEach((file, index) => {
|
||||||
|
const curPath = path.join(folder_to_delete, file);
|
||||||
|
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
||||||
|
deleteFolderRecursive(curPath);
|
||||||
|
} else { // delete file
|
||||||
|
fs.unlinkSync(curPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fs.rmdirSync(folder_to_delete);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getSubscription : getSubscription,
|
||||||
|
getAllSubscriptions: getAllSubscriptions,
|
||||||
|
subscribe : subscribe,
|
||||||
|
unsubscribe : unsubscribe,
|
||||||
|
getVideosForSub : getVideosForSub
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -2,9 +2,13 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { MainComponent } from './main/main.component';
|
import { MainComponent } from './main/main.component';
|
||||||
import { PlayerComponent } from './player/player.component';
|
import { PlayerComponent } from './player/player.component';
|
||||||
|
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
|
||||||
|
import { SubscriptionComponent } from './subscription/subscription/subscription.component';
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: 'home', component: MainComponent },
|
{ path: 'home', component: MainComponent },
|
||||||
{ path: 'player', component: PlayerComponent},
|
{ path: 'player', component: PlayerComponent},
|
||||||
|
{ path: 'subscriptions', component: SubscriptionsComponent },
|
||||||
|
{ path: 'subscription', component: SubscriptionComponent },
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; min-height: 100%;">
|
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
|
||||||
<mat-toolbar color="primary" class="top">
|
<div>
|
||||||
<div class="flex-row" width="100%" height="100%">
|
<mat-toolbar color="primary" class="top">
|
||||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
<div class="flex-row" width="100%" height="100%">
|
||||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||||
|
<button class="no-outline" *ngIf="router.url.split(';')[0] !== '/player'" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
|
||||||
|
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||||
|
<div>{{topBarTitle}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||||
|
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
</mat-toolbar>
|
||||||
<div>{{topBarTitle}}</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
|
||||||
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-toolbar>
|
|
||||||
|
|
||||||
<router-outlet></router-outlet>
|
<div style="height: calc(100% - 64px)">
|
||||||
|
<mat-sidenav-container style="height: 100%">
|
||||||
|
<mat-sidenav #sidenav>
|
||||||
|
<mat-nav-list>
|
||||||
|
<a mat-list-item routerLink='/home'>Home</a>
|
||||||
|
<a mat-list-item routerLink='/subscriptions'>Subscriptions</a>
|
||||||
|
</mat-nav-list>
|
||||||
|
</mat-sidenav>
|
||||||
|
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</mat-sidenav-content>
|
||||||
|
</mat-sidenav-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4,7 +4,7 @@ import {FileCardComponent} from './file-card/file-card.component';
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import {FormControl, Validators} from '@angular/forms';
|
import {FormControl, Validators} from '@angular/forms';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {MatSnackBar} from '@angular/material';
|
import {MatSnackBar, MatSidenav} from '@angular/material';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import 'rxjs/add/observable/of';
|
import 'rxjs/add/observable/of';
|
||||||
import 'rxjs/add/operator/mapTo';
|
import 'rxjs/add/operator/mapTo';
|
||||||
@@ -15,7 +15,7 @@ import 'rxjs/add/operator/debounceTime'
|
|||||||
import 'rxjs/add/operator/do'
|
import 'rxjs/add/operator/do'
|
||||||
import 'rxjs/add/operator/switch'
|
import 'rxjs/add/operator/switch'
|
||||||
import { YoutubeSearchService, Result } from './youtube-search.service';
|
import { YoutubeSearchService, Result } from './youtube-search.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router, NavigationStart } from '@angular/router';
|
||||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
import { THEMES_CONFIG } from '../themes';
|
import { THEMES_CONFIG } from '../themes';
|
||||||
|
|
||||||
@@ -34,11 +34,18 @@ export class AppComponent implements OnInit {
|
|||||||
defaultTheme = null;
|
defaultTheme = null;
|
||||||
allowThemeChange = null;
|
allowThemeChange = null;
|
||||||
|
|
||||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
@ViewChild('sidenav', {static: false}) sidenav: MatSidenav;
|
||||||
|
navigator: string = null;
|
||||||
|
|
||||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
||||||
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
|
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
|
||||||
|
|
||||||
|
this.navigator = localStorage.getItem('player_navigator');
|
||||||
|
// runs on navigate, captures the route that navigated to the player (if needed)
|
||||||
|
this.router.events.subscribe((e) => { if (e instanceof NavigationStart) {
|
||||||
|
this.navigator = localStorage.getItem('player_navigator');
|
||||||
|
} });
|
||||||
|
|
||||||
// loading config
|
// loading config
|
||||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||||
@@ -57,6 +64,10 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSidenav() {
|
||||||
|
this.sidenav.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
// theme stuff
|
// theme stuff
|
||||||
|
|
||||||
setTheme(theme) {
|
setTheme(theme) {
|
||||||
@@ -115,7 +126,11 @@ onSetTheme(theme, old_theme) {
|
|||||||
|
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
this.router.navigate(['/home']);
|
if (!this.navigator) {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
} else {
|
||||||
|
this.router.navigateByUrl(this.navigator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, Ma
|
|||||||
MatProgressBarModule, MatExpansionModule,
|
MatProgressBarModule, MatExpansionModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
MatDialogModule} from '@angular/material';
|
MatDialogModule,
|
||||||
|
MatRippleModule,
|
||||||
|
MatMenuModule} from '@angular/material';
|
||||||
import {DragDropModule} from '@angular/cdk/drag-drop';
|
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@@ -28,6 +30,11 @@ import { NgxContentLoadingModule } from 'ngx-content-loading';
|
|||||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||||
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
|
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
|
||||||
import { DownloadItemComponent } from './download-item/download-item.component';
|
import { DownloadItemComponent } from './download-item/download-item.component';
|
||||||
|
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
|
||||||
|
import { SubscribeDialogComponent } from './dialogs/subscribe-dialog/subscribe-dialog.component';
|
||||||
|
import { SubscriptionComponent } from './subscription//subscription/subscription.component';
|
||||||
|
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
|
||||||
|
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||||
|
|
||||||
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
|
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
|
||||||
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
|
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
|
||||||
@@ -41,7 +48,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
PlayerComponent,
|
PlayerComponent,
|
||||||
InputDialogComponent,
|
InputDialogComponent,
|
||||||
CreatePlaylistComponent,
|
CreatePlaylistComponent,
|
||||||
DownloadItemComponent
|
DownloadItemComponent,
|
||||||
|
SubscriptionsComponent,
|
||||||
|
SubscribeDialogComponent,
|
||||||
|
SubscriptionComponent,
|
||||||
|
SubscriptionFileCardComponent,
|
||||||
|
SubscriptionInfoDialogComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -67,6 +79,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
|
MatRippleModule,
|
||||||
|
MatMenuModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
VgCoreModule,
|
VgCoreModule,
|
||||||
@@ -80,7 +94,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
InputDialogComponent,
|
InputDialogComponent,
|
||||||
CreatePlaylistComponent
|
CreatePlaylistComponent,
|
||||||
|
SubscribeDialogComponent,
|
||||||
|
SubscriptionInfoDialogComponent
|
||||||
],
|
],
|
||||||
providers: [PostsService],
|
providers: [PostsService],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<h4 mat-dialog-title>Subscribe to playlist or channel</h4>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<input [(ngModel)]="url" matInput placeholder="URL" required aria-required="true">
|
||||||
|
<mat-hint>The playlist or channel URL</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<input [(ngModel)]="name" matInput placeholder="Custom name">
|
||||||
|
<mat-hint>This is optional</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-3">
|
||||||
|
<mat-checkbox [(ngModel)]="download_all">Download all uploads</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="col-12" *ngIf="!download_all">
|
||||||
|
Download videos uploaded in the last
|
||||||
|
<mat-form-field color="accent" style="width: 50px; text-align: center">
|
||||||
|
<input type="number" matInput [(ngModel)]="timerange_amount">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-select color="accent" class="unit-select" [(ngModel)]="timerange_unit">
|
||||||
|
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
|
||||||
|
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button mat-dialog-close>Cancel</button>
|
||||||
|
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||||
|
<button mat-button [disabled]="!url" type="submit" (click)="subscribeClicked()">Subscribe</button>
|
||||||
|
<div class="mat-spinner" *ngIf="subscribing">
|
||||||
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</mat-dialog-actions>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.unit-select {
|
||||||
|
width: 75px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-spinner {
|
||||||
|
margin-left: 5%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SubscribeDialogComponent } from './subscribe-dialog.component';
|
||||||
|
|
||||||
|
describe('SubscribeDialogComponent', () => {
|
||||||
|
let component: SubscribeDialogComponent;
|
||||||
|
let fixture: ComponentFixture<SubscribeDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ SubscribeDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscribeDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { MatSnackBar, MatDialogRef } from '@angular/material';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-subscribe-dialog',
|
||||||
|
templateUrl: './subscribe-dialog.component.html',
|
||||||
|
styleUrls: ['./subscribe-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class SubscribeDialogComponent implements OnInit {
|
||||||
|
// inputs
|
||||||
|
timerange_amount;
|
||||||
|
timerange_unit = 'days';
|
||||||
|
download_all = true;
|
||||||
|
url = null;
|
||||||
|
name = null;
|
||||||
|
|
||||||
|
// state
|
||||||
|
subscribing = false;
|
||||||
|
|
||||||
|
time_units = [
|
||||||
|
'day',
|
||||||
|
'week',
|
||||||
|
'month',
|
||||||
|
'year'
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor(private postsService: PostsService,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
public dialogRef: MatDialogRef<SubscribeDialogComponent>) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeClicked() {
|
||||||
|
if (this.url && this.url !== '') {
|
||||||
|
// timerange must be specified if download_all is false
|
||||||
|
if (!this.download_all && !this.timerange_amount) {
|
||||||
|
this.openSnackBar('You must specify an amount of time');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.subscribing = true;
|
||||||
|
|
||||||
|
let timerange = null;
|
||||||
|
if (!this.download_all) {
|
||||||
|
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postsService.createSubscription(this.url, this.name, timerange).subscribe(res => {
|
||||||
|
this.subscribing = false;
|
||||||
|
if (res['new_sub']) {
|
||||||
|
this.dialogRef.close(res['new_sub']);
|
||||||
|
} else {
|
||||||
|
if (res['error']) {
|
||||||
|
this.openSnackBar('ERROR: ' + res['error']);
|
||||||
|
}
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public openSnackBar(message: string, action = '') {
|
||||||
|
this.snackBar.open(message, action, {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<h4 mat-dialog-title>{{sub.name}}</h4>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<strong>Type:</strong> {{(sub.isPlaylist ? 'Playlist' : 'Channel')}}
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button mat-dialog-close>Close</button>
|
||||||
|
<button mat-button (click)="unsubscribe()" color="warn">Unsubscribe</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SubscriptionInfoDialogComponent } from './subscription-info-dialog.component';
|
||||||
|
|
||||||
|
describe('SubscriptionInfoDialogComponent', () => {
|
||||||
|
let component: SubscriptionInfoDialogComponent;
|
||||||
|
let fixture: ComponentFixture<SubscriptionInfoDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ SubscriptionInfoDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionInfoDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-subscription-info-dialog',
|
||||||
|
templateUrl: './subscription-info-dialog.component.html',
|
||||||
|
styleUrls: ['./subscription-info-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class SubscriptionInfoDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
sub = null;
|
||||||
|
unsubbedEmitter = null;
|
||||||
|
|
||||||
|
constructor(public dialogRef: MatDialogRef<SubscriptionInfoDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any, private postsService: PostsService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.data) {
|
||||||
|
this.sub = this.data.sub;
|
||||||
|
this.unsubbedEmitter = this.data.unsubbedEmitter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe() {
|
||||||
|
this.postsService.unsubscribe(this.sub, true).subscribe(res => {
|
||||||
|
this.unsubbedEmitter.emit(true);
|
||||||
|
this.dialogRef.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -372,6 +372,7 @@ export class MainComponent implements OnInit {
|
|||||||
this.downloading_content[type][playlistID] = true;
|
this.downloading_content[type][playlistID] = true;
|
||||||
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
||||||
} else {
|
} else {
|
||||||
|
localStorage.setItem('player_navigator', this.router.url);
|
||||||
const fileNames = playlist.fileNames;
|
const fileNames = playlist.fileNames;
|
||||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
|
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
|
||||||
}
|
}
|
||||||
@@ -444,6 +445,7 @@ export class MainComponent implements OnInit {
|
|||||||
this.downloadAudioFile(decodeURI(name));
|
this.downloadAudioFile(decodeURI(name));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
localStorage.setItem('player_navigator', this.router.url);
|
||||||
if (is_playlist) {
|
if (is_playlist) {
|
||||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
|
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
|
||||||
} else {
|
} else {
|
||||||
@@ -481,6 +483,7 @@ export class MainComponent implements OnInit {
|
|||||||
this.downloadVideoFile(decodeURI(name));
|
this.downloadVideoFile(decodeURI(name));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
localStorage.setItem('player_navigator', this.router.url);
|
||||||
if (is_playlist) {
|
if (is_playlist) {
|
||||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
|
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -31,16 +31,19 @@ export class PlayerComponent implements OnInit {
|
|||||||
// params
|
// params
|
||||||
fileNames: string[];
|
fileNames: string[];
|
||||||
type: string;
|
type: string;
|
||||||
|
id = null; // used for playlists (not subscription)
|
||||||
|
subscriptionName = null;
|
||||||
|
subPlaylist = null;
|
||||||
|
|
||||||
baseStreamPath = null;
|
baseStreamPath = null;
|
||||||
audioFolderPath = null;
|
audioFolderPath = null;
|
||||||
videoFolderPath = null;
|
videoFolderPath = null;
|
||||||
|
subscriptionFolderPath = null;
|
||||||
|
|
||||||
innerWidth: number;
|
innerWidth: number;
|
||||||
|
|
||||||
downloading = false;
|
downloading = false;
|
||||||
|
|
||||||
id = null;
|
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
onResize(event) {
|
onResize(event) {
|
||||||
this.innerWidth = window.innerWidth;
|
this.innerWidth = window.innerWidth;
|
||||||
@@ -52,6 +55,8 @@ export class PlayerComponent implements OnInit {
|
|||||||
this.fileNames = this.route.snapshot.paramMap.get('fileNames').split('|nvr|');
|
this.fileNames = this.route.snapshot.paramMap.get('fileNames').split('|nvr|');
|
||||||
this.type = this.route.snapshot.paramMap.get('type');
|
this.type = this.route.snapshot.paramMap.get('type');
|
||||||
this.id = this.route.snapshot.paramMap.get('id');
|
this.id = this.route.snapshot.paramMap.get('id');
|
||||||
|
this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName');
|
||||||
|
this.subPlaylist = this.route.snapshot.paramMap.get('subPlaylist');
|
||||||
|
|
||||||
// loading config
|
// loading config
|
||||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||||
@@ -59,6 +64,7 @@ export class PlayerComponent implements OnInit {
|
|||||||
this.baseStreamPath = this.postsService.path;
|
this.baseStreamPath = this.postsService.path;
|
||||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||||
|
this.subscriptionFolderPath = result['YoutubeDLMaterial']['Subscriptions']['subscriptions_base_path'];
|
||||||
|
|
||||||
|
|
||||||
let fileType = null;
|
let fileType = null;
|
||||||
@@ -66,15 +72,27 @@ export class PlayerComponent implements OnInit {
|
|||||||
fileType = 'audio/mp3';
|
fileType = 'audio/mp3';
|
||||||
} else if (this.type === 'video') {
|
} else if (this.type === 'video') {
|
||||||
fileType = 'video/mp4';
|
fileType = 'video/mp4';
|
||||||
|
} else if (this.type === 'subscription') {
|
||||||
|
// only supports mp4 for now
|
||||||
|
fileType = 'video/mp4';
|
||||||
} else {
|
} else {
|
||||||
// error
|
// error
|
||||||
console.error('Must have valid file type! Use \'audio\' or \video\'');
|
console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.fileNames.length; i++) {
|
for (let i = 0; i < this.fileNames.length; i++) {
|
||||||
const fileName = this.fileNames[i];
|
const fileName = this.fileNames[i];
|
||||||
const baseLocation = (this.type === 'audio') ? this.audioFolderPath : this.videoFolderPath;
|
let baseLocation = null;
|
||||||
const fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
|
let fullLocation = null;
|
||||||
|
if (!this.subscriptionName) {
|
||||||
|
baseLocation = this.type + '/';
|
||||||
|
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
|
||||||
|
} else {
|
||||||
|
// default to video but include subscription name param
|
||||||
|
baseLocation = 'video/';
|
||||||
|
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
|
||||||
|
'&subPlaylist=' + this.subPlaylist;
|
||||||
|
}
|
||||||
// if it has a slash (meaning it's in a directory), only get the file name for the label
|
// if it has a slash (meaning it's in a directory), only get the file name for the label
|
||||||
let label = null;
|
let label = null;
|
||||||
const decodedName = decodeURIComponent(fileName);
|
const decodedName = decodeURIComponent(fileName);
|
||||||
|
|||||||
@@ -136,6 +136,22 @@ export class PostsService {
|
|||||||
removePlaylist(playlistID, type) {
|
removePlaylist(playlistID, type) {
|
||||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createSubscription(url, name, timerange = null) {
|
||||||
|
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange})
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(sub, deleteMode = false) {
|
||||||
|
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode})
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscription(id) {
|
||||||
|
return this.http.post(this.path + 'getSubscription', {id: id});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllSubscriptions() {
|
||||||
|
return this.http.post(this.path + 'getAllSubscriptions', {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<div style="position: relative; width: fit-content;">
|
||||||
|
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
|
<mat-menu #action_menu="matMenu">
|
||||||
|
<button mat-menu-item><mat-icon>info</mat-icon>Info</button>
|
||||||
|
<button mat-menu-item><mat-icon>restore</mat-icon>Delete and redownload</button>
|
||||||
|
<button mat-menu-item><mat-icon>delete_forever</mat-icon>Delete forever</button>
|
||||||
|
</mat-menu>
|
||||||
|
<mat-card (click)="goToFile(file.name)" matRipple class="example-card mat-elevation-z6">
|
||||||
|
<div style="padding:5px">
|
||||||
|
<div *ngIf="!image_errored && file.thumbnailURL" class="img-div">
|
||||||
|
<img class="image" (error)="onImgError($event)" [src]="file.thumbnailURL" alt="Thumbnail">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="max-two-lines"><strong>{{file.title}}</strong></span>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.example-card {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
padding: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuButton {
|
||||||
|
right: 0px;
|
||||||
|
top: -1px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Coerce the <span> icon container away from display:inline */
|
||||||
|
.mat-icon-button .mat-button-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 200px;
|
||||||
|
height: 112.5px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-full-width-height {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
margin: 0 auto;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-div {
|
||||||
|
max-height: 80px;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 32px 0px 0px -5px;
|
||||||
|
width: calc(100% + 5px + 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-two-lines {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -moz-box;
|
||||||
|
max-height: 2.4em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
bottom: 5px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px){
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
width: 175px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 175px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SubscriptionFileCardComponent } from './subscription-file-card.component';
|
||||||
|
|
||||||
|
describe('SubscriptionFileCardComponent', () => {
|
||||||
|
let component: SubscriptionFileCardComponent;
|
||||||
|
let fixture: ComponentFixture<SubscriptionFileCardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ SubscriptionFileCardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionFileCardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-subscription-file-card',
|
||||||
|
templateUrl: './subscription-file-card.component.html',
|
||||||
|
styleUrls: ['./subscription-file-card.component.scss']
|
||||||
|
})
|
||||||
|
export class SubscriptionFileCardComponent implements OnInit {
|
||||||
|
image_errored = false;
|
||||||
|
image_loaded = false;
|
||||||
|
|
||||||
|
scrollSubject;
|
||||||
|
scrollAndLoad;
|
||||||
|
|
||||||
|
@Input() file;
|
||||||
|
|
||||||
|
@Output() goToFileEmit = new EventEmitter<any>();
|
||||||
|
|
||||||
|
constructor(private snackBar: MatSnackBar) {
|
||||||
|
this.scrollSubject = new Subject();
|
||||||
|
this.scrollAndLoad = Observable.merge(
|
||||||
|
Observable.fromEvent(window, 'scroll'),
|
||||||
|
this.scrollSubject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onImgError(event) {
|
||||||
|
this.image_errored = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onHoverResponse() {
|
||||||
|
this.scrollSubject.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
imageLoaded(loaded) {
|
||||||
|
this.image_loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToFile() {
|
||||||
|
this.goToFileEmit.emit(this.file.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public openSnackBar(message: string, action: string) {
|
||||||
|
this.snackBar.open(message, action, {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<br/>
|
||||||
|
<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}}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div *ngIf="subscription">
|
||||||
|
<h4 style="text-align: center; margin-bottom: 20px;">Videos</h4>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div *ngFor="let file of files" class="col mb-4 sub-file-col">
|
||||||
|
<app-subscription-file-card (goToFileEmit)="goToFile($event)" [file]="file"></app-subscription-file-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.sub-file-col {
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
float: left;
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SubscriptionComponent } from './subscription.component';
|
||||||
|
|
||||||
|
describe('SubscriptionComponent', () => {
|
||||||
|
let component: SubscriptionComponent;
|
||||||
|
let fixture: ComponentFixture<SubscriptionComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ SubscriptionComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
44
src/app/subscription/subscription/subscription.component.ts
Normal file
44
src/app/subscription/subscription/subscription.component.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-subscription',
|
||||||
|
templateUrl: './subscription.component.html',
|
||||||
|
styleUrls: ['./subscription.component.scss']
|
||||||
|
})
|
||||||
|
export class SubscriptionComponent implements OnInit {
|
||||||
|
|
||||||
|
id = null;
|
||||||
|
subscription = null;
|
||||||
|
files: any[] = null;
|
||||||
|
|
||||||
|
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.route.snapshot.paramMap.get('id')) {
|
||||||
|
this.id = this.route.snapshot.paramMap.get('id');
|
||||||
|
|
||||||
|
this.getSubscription();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.router.navigate(['/subscriptions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscription() {
|
||||||
|
this.postsService.getSubscription(this.id).subscribe(res => {
|
||||||
|
this.subscription = res['subscription'];
|
||||||
|
console.log(res['files']);
|
||||||
|
this.files = res['files'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goToFile(name) {
|
||||||
|
localStorage.setItem('player_navigator', this.router.url);
|
||||||
|
this.router.navigate(['/player', {fileNames: name, type: 'subscription', subscriptionName: this.subscription.name,
|
||||||
|
subPlaylist: this.subscription.isPlaylist}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
54
src/app/subscriptions/subscriptions.component.html
Normal file
54
src/app/subscriptions/subscriptions.component.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<br/>
|
||||||
|
|
||||||
|
<h2 style="text-align: center; margin-bottom: 15px;">Your subscriptions</h2>
|
||||||
|
|
||||||
|
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<h4 style="text-align: center;">Channels</h4>
|
||||||
|
<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 }}</strong>
|
||||||
|
<div *ngIf="!sub.name">
|
||||||
|
<ngx-content-loading [width]="200" [height]="20">
|
||||||
|
<svg:g ngx-rect width="200" height="20" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||||
|
</ngx-content-loading>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<button mat-icon-button (click)="showSubInfo(sub)">
|
||||||
|
<mat-icon>info</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-nav-list>
|
||||||
|
|
||||||
|
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="channel_subscriptions.length === 0 && subscriptions">
|
||||||
|
<p>You have no channel subscriptions.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 style="text-align: center;">Playlists</h4>
|
||||||
|
<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 }}</strong>
|
||||||
|
<div class="content-loading-div" *ngIf="!sub.name">
|
||||||
|
<ngx-content-loading [primaryColor]="postsService.theme.background_color" [secondaryColor]="postsService.theme.alternate_color" [width]="200" [height]="20">
|
||||||
|
<svg:g ngx-rect width="200" height="20" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||||
|
</ngx-content-loading>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<button mat-icon-button (click)="showSubInfo(sub)">
|
||||||
|
<mat-icon>info</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-nav-list>
|
||||||
|
|
||||||
|
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="!playlist_subscriptions && subscriptions">
|
||||||
|
<p>You have no playlist subscriptions.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 0 auto" *ngIf="subscriptions_loading">
|
||||||
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="add-subscription-button" (click)="openSubscribeDialog()" mat-fab><mat-icon>add</mat-icon></button>
|
||||||
27
src/app/subscriptions/subscriptions.component.scss
Normal file
27
src/app/subscriptions/subscriptions.component.scss
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.add-subscription-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-card {
|
||||||
|
height: 200px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-loading-div {
|
||||||
|
position: absolute;
|
||||||
|
width: 200px;
|
||||||
|
height: 50px;
|
||||||
|
bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-list-item {
|
||||||
|
height: 48px;
|
||||||
|
padding-top: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav-list {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
25
src/app/subscriptions/subscriptions.component.spec.ts
Normal file
25
src/app/subscriptions/subscriptions.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SubscriptionsComponent } from './subscriptions.component';
|
||||||
|
|
||||||
|
describe('SubscriptionsComponent', () => {
|
||||||
|
let component: SubscriptionsComponent;
|
||||||
|
let fixture: ComponentFixture<SubscriptionsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ SubscriptionsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
93
src/app/subscriptions/subscriptions.component.ts
Normal file
93
src/app/subscriptions/subscriptions.component.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Component, OnInit, EventEmitter } from '@angular/core';
|
||||||
|
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||||
|
import { SubscribeDialogComponent } from 'app/dialogs/subscribe-dialog/subscribe-dialog.component';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { SubscriptionInfoDialogComponent } from 'app/dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-subscriptions',
|
||||||
|
templateUrl: './subscriptions.component.html',
|
||||||
|
styleUrls: ['./subscriptions.component.scss']
|
||||||
|
})
|
||||||
|
export class SubscriptionsComponent implements OnInit {
|
||||||
|
|
||||||
|
playlist_subscriptions = [];
|
||||||
|
channel_subscriptions = [];
|
||||||
|
subscriptions = null;
|
||||||
|
|
||||||
|
subscriptions_loading = false;
|
||||||
|
|
||||||
|
constructor(private dialog: MatDialog, private postsService: PostsService, private router: Router, private snackBar: MatSnackBar) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscriptions() {
|
||||||
|
this.subscriptions_loading = true;
|
||||||
|
this.subscriptions = [];
|
||||||
|
this.channel_subscriptions = [];
|
||||||
|
this.playlist_subscriptions = [];
|
||||||
|
this.postsService.getAllSubscriptions().subscribe(res => {
|
||||||
|
this.subscriptions_loading = false;
|
||||||
|
this.subscriptions = res['subscriptions'];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||||
|
const sub = this.subscriptions[i];
|
||||||
|
|
||||||
|
// parse subscriptions into channels and playlists
|
||||||
|
if (sub.isPlaylist) {
|
||||||
|
this.playlist_subscriptions.push(sub);
|
||||||
|
} else {
|
||||||
|
this.channel_subscriptions.push(sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goToSubscription(sub) {
|
||||||
|
this.router.navigate(['/subscription', {id: sub.id}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
openSubscribeDialog() {
|
||||||
|
const dialogRef = this.dialog.open(SubscribeDialogComponent, {
|
||||||
|
maxWidth: 500,
|
||||||
|
width: '80vw'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
if (result.isPlaylist) {
|
||||||
|
this.playlist_subscriptions.push(result);
|
||||||
|
} else {
|
||||||
|
this.channel_subscriptions.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showSubInfo(sub) {
|
||||||
|
const unsubbedEmitter = new EventEmitter<any>();
|
||||||
|
const dialogRef = this.dialog.open(SubscriptionInfoDialogComponent, {
|
||||||
|
data: {
|
||||||
|
sub: sub,
|
||||||
|
unsubbedEmitter: unsubbedEmitter
|
||||||
|
}
|
||||||
|
});
|
||||||
|
unsubbedEmitter.subscribe(success => {
|
||||||
|
if (success) {
|
||||||
|
this.openSnackBar(`${sub.name} successfully deleted!`)
|
||||||
|
this.getSubscriptions();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// snackbar helper
|
||||||
|
public openSnackBar(message: string, action = '') {
|
||||||
|
this.snackBar.open(message, action, {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -28,6 +28,12 @@
|
|||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
"allow_theme_change": true
|
"allow_theme_change": true
|
||||||
},
|
},
|
||||||
|
"Subscriptions": {
|
||||||
|
"allow_subscriptions": true,
|
||||||
|
"subscriptions_base_path": "subscriptions/",
|
||||||
|
"subscriptions_check_interval": "300",
|
||||||
|
"subscriptions_use_youtubedl_archive": true
|
||||||
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
"use_default_downloading_agent": true,
|
"use_default_downloading_agent": true,
|
||||||
"custom_downloading_agent": "",
|
"custom_downloading_agent": "",
|
||||||
|
|||||||
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 19 KiB |
@@ -2,12 +2,14 @@ const THEMES_CONFIG = {
|
|||||||
'default': {
|
'default': {
|
||||||
'key': 'default',
|
'key': 'default',
|
||||||
'background_color': 'ghostwhite',
|
'background_color': 'ghostwhite',
|
||||||
|
'alternate_color': 'gray',
|
||||||
'css_label': 'default-theme',
|
'css_label': 'default-theme',
|
||||||
'social_theme': 'material-light'
|
'social_theme': 'material-light'
|
||||||
},
|
},
|
||||||
'dark': {
|
'dark': {
|
||||||
'key': 'dark',
|
'key': 'dark',
|
||||||
'background_color': '#757575',
|
'background_color': '#757575',
|
||||||
|
'alternate_color': '#695959',
|
||||||
'css_label': 'dark-theme',
|
'css_label': 'dark-theme',
|
||||||
'social_theme': 'material-dark'
|
'social_theme': 'material-dark'
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user