diff --git a/backend/app.js b/backend/app.js
index 690edbc..abe36ac 100644
--- a/backend/app.js
+++ b/backend/app.js
@@ -1,4 +1,5 @@
var async = require('async');
+const { uuid } = require('uuidv4');
var fs = require('fs');
var path = require('path');
var youtubedl = require('youtube-dl');
@@ -10,7 +11,9 @@ var archiver = require('archiver');
const low = require('lowdb')
var URL = require('url').URL;
const shortid = require('shortid')
+const url_api = require('url');
var config_api = require('./config.js');
+var subscriptions_api = require('./subscriptions')
var app = express();
@@ -25,7 +28,8 @@ db.defaults(
audio: [],
video: []
},
- configWriteFlag: false
+ configWriteFlag: false,
+ subscriptions: []
}).write();
// config values
@@ -39,6 +43,8 @@ var videoFolderPath = null;
var downloadOnlyMode = null;
var useDefaultDownloadingAgent = null;
var customDownloadingAgent = null;
+var allowSubscriptions = null;
+var subscriptionsCheckInterval = null;
// other needed values
var options = null; // encryption options
@@ -129,6 +135,9 @@ async function loadConfig() {
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_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 ) {
console.log(`INFO: Using non-default downloading agent \'${customDownloadingAgent}\'`)
}
@@ -149,6 +158,11 @@ async function loadConfig() {
url_domain = new URL(url);
+ // get subscriptions
+ if (allowSubscriptions) {
+ watchSubscriptions();
+ }
+
// start the server here
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() {
return url_domain.origin;
}
@@ -239,9 +281,14 @@ function getJSONMp3(name)
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))
{
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
@@ -802,6 +849,109 @@ app.post('/api/getMp4s', function(req, res) {
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) => {
let playlistName = req.body.playlistName;
let fileNames = req.body.fileNames;
@@ -948,8 +1098,15 @@ app.post('/api/deleteFile', async (req, res) => {
app.get('/api/video/:id', function(req , res){
var head;
+ let optionalParams = url_api.parse(req.url,true).query;
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 fileSize = stat.size
const range = req.headers.range
diff --git a/backend/config.js b/backend/config.js
index ee31843..4a5334b 100644
--- a/backend/config.js
+++ b/backend/config.js
@@ -56,7 +56,10 @@ function setConfigFile(config) {
function getConfigItem(key) {
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'];
return Object.byString(config_json, path);
};
diff --git a/backend/config/default.json b/backend/config/default.json
index 051c68c..df8fd5b 100644
--- a/backend/config/default.json
+++ b/backend/config/default.json
@@ -28,6 +28,12 @@
"default_theme": "default",
"allow_theme_change": true
},
+ "Subscriptions": {
+ "allow_subscriptions": true,
+ "subscriptions_base_path": "subscriptions/",
+ "subscriptions_check_interval": "300",
+ "subscriptions_use_youtubedl_archive": true
+ },
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
diff --git a/backend/config/encrypted.json b/backend/config/encrypted.json
index 0b1ce37..fd2833f 100644
--- a/backend/config/encrypted.json
+++ b/backend/config/encrypted.json
@@ -28,6 +28,12 @@
"default_theme": "default",
"allow_theme_change": true
},
+ "Subscriptions": {
+ "allow_subscriptions": true,
+ "subscriptions_base_path": "subscriptions/",
+ "subscriptions_check_interval": "300",
+ "subscriptions_use_youtubedl_archive": true
+ },
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
diff --git a/backend/consts.js b/backend/consts.js
index 19590e6..3c87c41 100644
--- a/backend/consts.js
+++ b/backend/consts.js
@@ -56,7 +56,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_allow_multi_download_mode',
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
},
-
// API
'ytdl_use_youtube_api': {
@@ -78,6 +77,28 @@ let CONFIG_ITEMS = {
'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
'ytdl_use_default_downloading_agent': {
'key': 'ytdl_use_default_downloading_agent',
diff --git a/backend/package.json b/backend/package.json
index ad85567..6da5631 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -26,6 +26,7 @@
"express": "^4.17.1",
"lowdb": "^1.0.0",
"shortid": "^2.2.15",
+ "uuidv4": "^6.0.6",
"youtube-dl": "^3.0.2"
}
}
diff --git a/backend/subscriptions.js b/backend/subscriptions.js
new file mode 100644
index 0000000..65a2b22
--- /dev/null
+++ b/backend/subscriptions.js
@@ -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
+}
diff --git a/backend/youtube-dl.exe b/backend/youtube-dl.exe
index 0ccb93a..3e4ebeb 100644
Binary files a/backend/youtube-dl.exe and b/backend/youtube-dl.exe differ
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index c9d3181..c4cce48 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -2,9 +2,13 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MainComponent } from './main/main.component';
import { PlayerComponent } from './player/player.component';
+import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
+import { SubscriptionComponent } from './subscription/subscription/subscription.component';
const routes: Routes = [
{ path: 'home', component: MainComponent },
{ path: 'player', component: PlayerComponent},
+ { path: 'subscriptions', component: SubscriptionsComponent },
+ { path: 'subscription', component: SubscriptionComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
];
diff --git a/src/app/app.component.html b/src/app/app.component.html
index fc7da91..426a303 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,17 +1,32 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
+
\ No newline at end of file
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 91a16c6..add0194 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -4,7 +4,7 @@ import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable';
import {FormControl, Validators} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
-import {MatSnackBar} from '@angular/material';
+import {MatSnackBar, MatSidenav} from '@angular/material';
import { saveAs } from 'file-saver';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/mapTo';
@@ -15,7 +15,7 @@ import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/do'
import 'rxjs/add/operator/switch'
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 { THEMES_CONFIG } from '../themes';
@@ -34,11 +34,18 @@ export class AppComponent implements OnInit {
defaultTheme = 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,
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
this.postsService.loadNavItems().subscribe(res => { // loads settings
const result = !this.postsService.debugMode ? res['config_file'] : res;
@@ -57,6 +64,10 @@ export class AppComponent implements OnInit {
}
+ toggleSidenav() {
+ this.sidenav.toggle();
+ }
+
// theme stuff
setTheme(theme) {
@@ -115,7 +126,11 @@ onSetTheme(theme, old_theme) {
goBack() {
- this.router.navigate(['/home']);
+ if (!this.navigator) {
+ this.router.navigate(['/home']);
+ } else {
+ this.router.navigateByUrl(this.navigator);
+ }
}
}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 6ffecfe..928b4d4 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -5,7 +5,9 @@ import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, Ma
MatProgressBarModule, MatExpansionModule,
MatProgressSpinnerModule,
MatButtonToggleModule,
- MatDialogModule} from '@angular/material';
+ MatDialogModule,
+ MatRippleModule,
+ MatMenuModule} from '@angular/material';
import {DragDropModule} from '@angular/cdk/drag-drop';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
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 { CreatePlaylistComponent } from './create-playlist/create-playlist.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
) {
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
@@ -41,7 +48,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
PlayerComponent,
InputDialogComponent,
CreatePlaylistComponent,
- DownloadItemComponent
+ DownloadItemComponent,
+ SubscriptionsComponent,
+ SubscribeDialogComponent,
+ SubscriptionComponent,
+ SubscriptionFileCardComponent,
+ SubscriptionInfoDialogComponent
],
imports: [
BrowserModule,
@@ -67,6 +79,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatProgressBarModule,
MatProgressSpinnerModule,
MatButtonToggleModule,
+ MatRippleModule,
+ MatMenuModule,
MatDialogModule,
DragDropModule,
VgCoreModule,
@@ -80,7 +94,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
],
entryComponents: [
InputDialogComponent,
- CreatePlaylistComponent
+ CreatePlaylistComponent,
+ SubscribeDialogComponent,
+ SubscriptionInfoDialogComponent
],
providers: [PostsService],
bootstrap: [AppComponent]
diff --git a/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html
new file mode 100644
index 0000000..7e8d004
--- /dev/null
+++ b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html
@@ -0,0 +1,43 @@
+Subscribe to playlist or channel
+
+
+
+
+
+
+
+ The playlist or channel URL
+
+
+
+
+
+ This is optional
+
+
+
+ Download all uploads
+
+
+ Download videos uploaded in the last
+
+
+
+
+
+ {{time_unit + (timerange_amount === 1 ? '' : 's')}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.scss b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.scss
new file mode 100644
index 0000000..809e4ac
--- /dev/null
+++ b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.scss
@@ -0,0 +1,8 @@
+.unit-select {
+ width: 75px;
+ margin-left: 20px;
+}
+
+.mat-spinner {
+ margin-left: 5%;
+}
diff --git a/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.spec.ts b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.spec.ts
new file mode 100644
index 0000000..94d1fe1
--- /dev/null
+++ b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SubscribeDialogComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubscribeDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts
new file mode 100644
index 0000000..129da4f
--- /dev/null
+++ b/src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts
@@ -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) { }
+
+ 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,
+ });
+ }
+
+}
diff --git a/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html
new file mode 100644
index 0000000..b0ccf22
--- /dev/null
+++ b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html
@@ -0,0 +1,10 @@
+{{sub.name}}
+
+
+ Type: {{(sub.isPlaylist ? 'Playlist' : 'Channel')}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.scss b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.spec.ts b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.spec.ts
new file mode 100644
index 0000000..45fa822
--- /dev/null
+++ b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SubscriptionInfoDialogComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubscriptionInfoDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts
new file mode 100644
index 0000000..ee004d3
--- /dev/null
+++ b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts
@@ -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,
+ @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();
+ });
+ }
+
+}
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts
index 3b3e640..4a8d950 100644
--- a/src/app/main/main.component.ts
+++ b/src/app/main/main.component.ts
@@ -372,6 +372,7 @@ export class MainComponent implements OnInit {
this.downloading_content[type][playlistID] = true;
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
} else {
+ localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
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));
}
} else {
+ localStorage.setItem('player_navigator', this.router.url);
if (is_playlist) {
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
} else {
@@ -481,6 +483,7 @@ export class MainComponent implements OnInit {
this.downloadVideoFile(decodeURI(name));
}
} else {
+ localStorage.setItem('player_navigator', this.router.url);
if (is_playlist) {
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
} else {
diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts
index d8e3466..41dccb5 100644
--- a/src/app/player/player.component.ts
+++ b/src/app/player/player.component.ts
@@ -31,16 +31,19 @@ export class PlayerComponent implements OnInit {
// params
fileNames: string[];
type: string;
+ id = null; // used for playlists (not subscription)
+ subscriptionName = null;
+ subPlaylist = null;
baseStreamPath = null;
audioFolderPath = null;
videoFolderPath = null;
+ subscriptionFolderPath = null;
+
innerWidth: number;
downloading = false;
- id = null;
-
@HostListener('window:resize', ['$event'])
onResize(event) {
this.innerWidth = window.innerWidth;
@@ -52,6 +55,8 @@ export class PlayerComponent implements OnInit {
this.fileNames = this.route.snapshot.paramMap.get('fileNames').split('|nvr|');
this.type = this.route.snapshot.paramMap.get('type');
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
this.postsService.loadNavItems().subscribe(res => { // loads settings
@@ -59,6 +64,7 @@ export class PlayerComponent implements OnInit {
this.baseStreamPath = this.postsService.path;
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
+ this.subscriptionFolderPath = result['YoutubeDLMaterial']['Subscriptions']['subscriptions_base_path'];
let fileType = null;
@@ -66,15 +72,27 @@ export class PlayerComponent implements OnInit {
fileType = 'audio/mp3';
} else if (this.type === 'video') {
fileType = 'video/mp4';
+ } else if (this.type === 'subscription') {
+ // only supports mp4 for now
+ fileType = 'video/mp4';
} else {
// 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++) {
const fileName = this.fileNames[i];
- const baseLocation = (this.type === 'audio') ? this.audioFolderPath : this.videoFolderPath;
- const fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
+ let baseLocation = null;
+ 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
let label = null;
const decodedName = decodeURIComponent(fileName);
diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts
index 08fa9a0..2b3e31e 100644
--- a/src/app/posts.services.ts
+++ b/src/app/posts.services.ts
@@ -136,6 +136,22 @@ export class PostsService {
removePlaylist(playlistID, 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', {});
+ }
}
diff --git a/src/app/subscription/subscription-file-card/subscription-file-card.component.html b/src/app/subscription/subscription-file-card/subscription-file-card.component.html
new file mode 100644
index 0000000..da0a6ca
--- /dev/null
+++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
![Thumbnail]()
+
+
+
{{file.title}}
+
+
+
diff --git a/src/app/subscription/subscription-file-card/subscription-file-card.component.scss b/src/app/subscription/subscription-file-card/subscription-file-card.component.scss
new file mode 100644
index 0000000..a0e5f36
--- /dev/null
+++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.scss
@@ -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 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;
+ }
+
+ }
\ No newline at end of file
diff --git a/src/app/subscription/subscription-file-card/subscription-file-card.component.spec.ts b/src/app/subscription/subscription-file-card/subscription-file-card.component.spec.ts
new file mode 100644
index 0000000..ccb2763
--- /dev/null
+++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SubscriptionFileCardComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubscriptionFileCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/subscription/subscription-file-card/subscription-file-card.component.ts b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts
new file mode 100644
index 0000000..d17baa2
--- /dev/null
+++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts
@@ -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();
+
+ 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,
+ });
+ }
+
+}
diff --git a/src/app/subscription/subscription/subscription.component.html b/src/app/subscription/subscription/subscription.component.html
new file mode 100644
index 0000000..9fa386b
--- /dev/null
+++ b/src/app/subscription/subscription/subscription.component.html
@@ -0,0 +1,20 @@
+
+
+
+
+ {{subscription.name}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/subscription/subscription/subscription.component.scss b/src/app/subscription/subscription/subscription.component.scss
new file mode 100644
index 0000000..0f2927c
--- /dev/null
+++ b/src/app/subscription/subscription/subscription.component.scss
@@ -0,0 +1,9 @@
+.sub-file-col {
+ max-width: 240px;
+}
+
+.back-button {
+ float: left;
+ position: absolute;
+ left: 15px;
+}
\ No newline at end of file
diff --git a/src/app/subscription/subscription/subscription.component.spec.ts b/src/app/subscription/subscription/subscription.component.spec.ts
new file mode 100644
index 0000000..a4a2203
--- /dev/null
+++ b/src/app/subscription/subscription/subscription.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SubscriptionComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubscriptionComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts
new file mode 100644
index 0000000..50c2b35
--- /dev/null
+++ b/src/app/subscription/subscription/subscription.component.ts
@@ -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}]);
+ }
+
+}
diff --git a/src/app/subscriptions/subscriptions.component.html b/src/app/subscriptions/subscriptions.component.html
new file mode 100644
index 0000000..f4bb1ca
--- /dev/null
+++ b/src/app/subscriptions/subscriptions.component.html
@@ -0,0 +1,54 @@
+
+
+Your subscriptions
+
+
+
+
+Channels
+
+
+
+ {{ sub.name }}
+
+
+
+
+
+
+
+
+
+
+
+
You have no channel subscriptions.
+
+
+Playlists
+
+
+
+ {{ sub.name }}
+
+
+
+
+
+
+
+
+
+
+
+
You have no playlist subscriptions.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/subscriptions/subscriptions.component.scss b/src/app/subscriptions/subscriptions.component.scss
new file mode 100644
index 0000000..9ed3fd7
--- /dev/null
+++ b/src/app/subscriptions/subscriptions.component.scss
@@ -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%;
+}
\ No newline at end of file
diff --git a/src/app/subscriptions/subscriptions.component.spec.ts b/src/app/subscriptions/subscriptions.component.spec.ts
new file mode 100644
index 0000000..205dcf4
--- /dev/null
+++ b/src/app/subscriptions/subscriptions.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SubscriptionsComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubscriptionsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/subscriptions/subscriptions.component.ts b/src/app/subscriptions/subscriptions.component.ts
new file mode 100644
index 0000000..f489239
--- /dev/null
+++ b/src/app/subscriptions/subscriptions.component.ts
@@ -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();
+ 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,
+ });
+ }
+
+}
diff --git a/src/assets/default.json b/src/assets/default.json
index 6d9ab14..3b0e85a 100644
--- a/src/assets/default.json
+++ b/src/assets/default.json
@@ -28,6 +28,12 @@
"default_theme": "default",
"allow_theme_change": true
},
+ "Subscriptions": {
+ "allow_subscriptions": true,
+ "subscriptions_base_path": "subscriptions/",
+ "subscriptions_check_interval": "300",
+ "subscriptions_use_youtubedl_archive": true
+ },
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
diff --git a/src/favicon.ico b/src/favicon.ico
index 8b74b2c..c3aeeba 100644
Binary files a/src/favicon.ico and b/src/favicon.ico differ
diff --git a/src/themes.ts b/src/themes.ts
index 8fea9ee..cf9aa99 100644
--- a/src/themes.ts
+++ b/src/themes.ts
@@ -2,12 +2,14 @@ const THEMES_CONFIG = {
'default': {
'key': 'default',
'background_color': 'ghostwhite',
+ 'alternate_color': 'gray',
'css_label': 'default-theme',
'social_theme': 'material-light'
},
'dark': {
'key': 'dark',
'background_color': '#757575',
+ 'alternate_color': '#695959',
'css_label': 'dark-theme',
'social_theme': 'material-dark'
},