diff --git a/backend/app.js b/backend/app.js index d616e57..a02a6cd 100644 --- a/backend/app.js +++ b/backend/app.js @@ -84,12 +84,17 @@ app.use(bodyParser.json()); // objects -function File(id, title, thumbnailURL, isAudio, duration) { +function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) { this.id = id; this.title = title; this.thumbnailURL = thumbnailURL; this.isAudio = isAudio; this.duration = duration; + this.url = url; + this.uploader = uploader; + this.size = size; + this.path = path; + this.upload_date = upload_date; } // actual functions @@ -353,10 +358,16 @@ function getVideoFormatID(name) } } -async function createPlaylistZipFile(fileNames, type, outputName) { +async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null) { return new Promise(async resolve => { - let zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath); - // let name = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0]; + let zipFolderPath = null; + + if (!fullPathProvided) { + zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath); + } else { + zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path')); + } + let ext = (type === 'audio') ? '.mp3' : '.mp4'; let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip')); @@ -376,7 +387,8 @@ async function createPlaylistZipFile(fileNames, type, outputName) { for (let i = 0; i < fileNames.length; i++) { let fileName = fileNames[i]; - archive.file(zipFolderPath + fileName + ext, {name: fileName + ext}) + let file_path = !fullPathProvided ? zipFolderPath + fileName + ext : fileName; + archive.file(file_path, {name: fileName + ext}) } await archive.finalize(); @@ -951,20 +963,24 @@ app.post('/api/getMp3s', function(req, res) { for (let i = 0; i < files.length; i++) { let file = files[i]; var file_path = file.substring(audioFolderPath.length, file.length); + + var stats = fs.statSync(file); + var id = file_path.substring(0, file_path.length-4); var jsonobj = getJSONMp3(id); if (!jsonobj) continue; var title = jsonobj.title; + var url = jsonobj.webpage_url; + var uploader = jsonobj.uploader; + var upload_date = jsonobj.upload_date; + upload_date = `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}`; - if (title.length > 14) // edits title if it's too long - { - title = title.substring(0,12) + "..."; - } + var size = stats.size; var thumbnail = jsonobj.thumbnail; var duration = jsonobj.duration; var isaudio = true; - var file_obj = new File(id, title, thumbnail, isaudio, duration); + var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date); mp3s.push(file_obj); } @@ -984,20 +1000,24 @@ app.post('/api/getMp4s', function(req, res) { for (let i = 0; i < files.length; i++) { let file = files[i]; var file_path = file.substring(videoFolderPath.length, file.length); + + var stats = fs.statSync(file); + var id = file_path.substring(0, file_path.length-4); var jsonobj = getJSONMp4(id); if (!jsonobj) continue; var title = jsonobj.title; - - if (title.length > 14) // edits title if it's too long - { - title = title.substring(0,12) + "..."; - } - + var url = jsonobj.webpage_url; + var uploader = jsonobj.uploader; + var upload_date = jsonobj.upload_date; + upload_date = `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}`; var thumbnail = jsonobj.thumbnail; var duration = jsonobj.duration; + + var size = stats.size; + var isaudio = false; - var file_obj = new File(id, title, thumbnail, isaudio, duration); + var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date); mp4s.push(file_obj); } @@ -1101,6 +1121,8 @@ app.post('/api/getSubscription', async (req, res) => { for (let i = 0; i < files.length; i++) { let file = files[i]; var file_path = file.substring(appended_base_path.length, file.length); + var stats = fs.statSync(file); + var id = file_path.substring(0, file_path.length-4); var jsonobj = getJSONMp4(id, appended_base_path); if (!jsonobj) continue; @@ -1108,8 +1130,14 @@ app.post('/api/getSubscription', async (req, res) => { var thumbnail = jsonobj.thumbnail; var duration = jsonobj.duration; + var url = jsonobj.webpage_url; + var uploader = jsonobj.uploader; + var upload_date = jsonobj.upload_date; + upload_date = `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}`; + var size = stats.size; + var isaudio = false; - var file_obj = new File(id, title, thumbnail, isaudio, duration); + var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date); parsed_files.push(file_obj); } @@ -1120,9 +1148,6 @@ app.post('/api/getSubscription', async (req, res) => { } else { res.sendStatus(500); } - - - }); app.post('/api/downloadVideosForSubscription', async (req, res) => { @@ -1257,11 +1282,12 @@ app.post('/api/deleteMp4', async (req, res) => { app.post('/api/downloadFile', async (req, res) => { let fileNames = req.body.fileNames; - let is_playlist = req.body.is_playlist; + let zip_mode = req.body.zip_mode; let type = req.body.type; let outputName = req.body.outputName; + let fullPathProvided = req.body.fullPathProvided; let file = null; - if (!is_playlist) { + if (!zip_mode) { fileNames = decodeURIComponent(fileNames); if (type === 'audio') { file = __dirname + '/' + audioFolderPath + fileNames + '.mp3'; @@ -1272,10 +1298,20 @@ app.post('/api/downloadFile', async (req, res) => { for (let i = 0; i < fileNames.length; i++) { fileNames[i] = decodeURIComponent(fileNames[i]); } - file = await createPlaylistZipFile(fileNames, type, outputName); + file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided); } - res.sendFile(file); + res.sendFile(file, function (err) { + if (err) { + next(err); + } else if (fullPathProvided) { + try { + fs.unlinkSync(file); + } catch(e) { + console.log("ERROR: Failed to remove file", file); + } + } + }); }); app.post('/api/deleteFile', async (req, res) => { diff --git a/package.json b/package.json index dcbef0f..64c1aec 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@locl/core": "0.0.1-beta.2", "core-js": "^2.4.1", "file-saver": "^2.0.2", + "filesize": "^6.1.0", "ng-lazyload-image": "^7.0.1", "ng4-configure": "^0.1.7", "ngx-content-loading": "^0.1.3", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a430fff..e88d6fb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -49,6 +49,7 @@ import { SettingsComponent } from './settings/settings.component'; import es from '@angular/common/locales/es'; import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component'; +import { VideoInfoDialogComponent } from './dialogs/video-info-dialog/video-info-dialog.component'; registerLocaleData(es, 'es'); export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps) { @@ -70,7 +71,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible SubscriptionFileCardComponent, SubscriptionInfoDialogComponent, SettingsComponent, - AboutDialogComponent + AboutDialogComponent, + VideoInfoDialogComponent ], imports: [ BrowserModule, diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.html b/src/app/dialogs/video-info-dialog/video-info-dialog.component.html new file mode 100644 index 0000000..0c0c105 --- /dev/null +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.html @@ -0,0 +1,32 @@ +

{{file.title}}

+ + +
+
Name: 
+
{{file.title}}
+
+
+
URL: 
+ +
+
+
Uploader: 
+
{{file.uploader ? file.uploader : 'N/A'}}
+
+
+
File size: 
+
{{filesize(file.size)}}
+
+
+
Path: 
+
{{file.path}}
+
+
+
Upload Date: 
+
{{file.upload_date}}
+
+
+ + + + \ No newline at end of file diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss b/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss new file mode 100644 index 0000000..6e7c4f9 --- /dev/null +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss @@ -0,0 +1,18 @@ +.info-item { + margin-bottom: 12px; + width: 100%; +} + +.info-item-value { + font-size: 13px; + display: inline-block; + width: 70%; +} + +.spacer {flex: 1 1 auto;} + +.info-item-label { + display: inline-block; + width: 30%; + vertical-align: top; +} diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.spec.ts b/src/app/dialogs/video-info-dialog/video-info-dialog.component.spec.ts new file mode 100644 index 0000000..126ea43 --- /dev/null +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VideoInfoDialogComponent } from './video-info-dialog.component'; + +describe('VideoInfoDialogComponent', () => { + let component: VideoInfoDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ VideoInfoDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(VideoInfoDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts b/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts new file mode 100644 index 0000000..4bfc8e3 --- /dev/null +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import filesize from 'filesize'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +@Component({ + selector: 'app-video-info-dialog', + templateUrl: './video-info-dialog.component.html', + styleUrls: ['./video-info-dialog.component.scss'] +}) +export class VideoInfoDialogComponent implements OnInit { + file: any; + filesize; + constructor(@Inject(MAT_DIALOG_DATA) public data: any) { } + + ngOnInit(): void { + this.filesize = filesize; + if (this.data) { + this.file = this.data.file; + } + } + +} diff --git a/src/app/file-card/file-card.component.css b/src/app/file-card/file-card.component.css index 342885f..a581bca 100644 --- a/src/app/file-card/file-card.component.css +++ b/src/app/file-card/file-card.component.css @@ -51,6 +51,14 @@ -webkit-line-clamp: 2; } +.file-link { + width: 80%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +} + @media (max-width: 576px){ .example-card { diff --git a/src/app/file-card/file-card.component.html b/src/app/file-card/file-card.component.html index 1c796c5..ef79cf6 100644 --- a/src/app/file-card/file-card.component.html +++ b/src/app/file-card/file-card.component.html @@ -1,8 +1,9 @@
- {{title}} -
+
+ {{title}} +
ID: {{name}}
Count: {{count}}
@@ -15,10 +16,11 @@
- - + + + - +
diff --git a/src/app/file-card/file-card.component.ts b/src/app/file-card/file-card.component.ts index 770976e..16f33ed 100644 --- a/src/app/file-card/file-card.component.ts +++ b/src/app/file-card/file-card.component.ts @@ -5,6 +5,8 @@ import {EventEmitter} from '@angular/core'; import { MainComponent } from 'app/main/main.component'; import { Subject, Observable } from 'rxjs'; import 'rxjs/add/observable/merge'; +import { MatDialog } from '@angular/material/dialog'; +import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component'; @Component({ selector: 'app-file-card', @@ -12,7 +14,7 @@ import 'rxjs/add/observable/merge'; styleUrls: ['./file-card.component.css'] }) export class FileCardComponent implements OnInit { - + @Input() file: any; @Input() title: string; @Input() length: string; @Input() name: string; @@ -29,8 +31,10 @@ export class FileCardComponent implements OnInit { scrollSubject; scrollAndLoad; - constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { - this.scrollSubject = new Subject(); + constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent, + private dialog: MatDialog) { + + this.scrollSubject = new Subject(); this.scrollAndLoad = Observable.merge( Observable.fromEvent(window, 'scroll'), this.scrollSubject @@ -57,6 +61,15 @@ export class FileCardComponent implements OnInit { } + openVideoInfoDialog() { + const dialogRef = this.dialog.open(VideoInfoDialogComponent, { + data: { + file: this.file, + }, + minWidth: '50vw' + }); + } + onImgError(event) { this.image_errored = true; } diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index dc11d79..982a917 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -203,7 +203,7 @@
- @@ -244,7 +244,7 @@
- diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 6ea4133..97b63de 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -114,11 +114,12 @@ export class PostsService { return this.http.post(this.path + 'getMp4s', {}); } - downloadFileFromServer(fileName, type, outputName = null) { + downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null) { return this.http.post(this.path + 'downloadFile', {fileNames: fileName, type: type, - is_playlist: Array.isArray(fileName), - outputName: outputName}, + zip_mode: Array.isArray(fileName), + outputName: outputName, + fullPathProvided: fullPathProvided}, {responseType: 'blob'}); } 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 index a2972a9..81897db 100644 --- a/src/app/subscription/subscription-file-card/subscription-file-card.component.html +++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.html @@ -4,6 +4,7 @@
+ diff --git a/src/app/subscription/subscription-file-card/subscription-file-card.component.ts b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts index c2c23e2..093eb94 100644 --- a/src/app/subscription/subscription-file-card/subscription-file-card.component.ts +++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts @@ -3,6 +3,8 @@ import { Observable, Subject } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { PostsService } from 'app/posts.services'; +import { MatDialog } from '@angular/material/dialog'; +import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component'; @Component({ selector: 'app-subscription-file-card', @@ -25,7 +27,7 @@ export class SubscriptionFileCardComponent implements OnInit { @Output() goToFileEmit = new EventEmitter(); @Output() reloadSubscription = new EventEmitter(); - constructor(private snackBar: MatSnackBar, private postsService: PostsService) { + constructor(private snackBar: MatSnackBar, private postsService: PostsService, private dialog: MatDialog) { this.scrollSubject = new Subject(); this.scrollAndLoad = Observable.merge( Observable.fromEvent(window, 'scroll'), @@ -55,6 +57,15 @@ export class SubscriptionFileCardComponent implements OnInit { this.goToFileEmit.emit(this.file.id); } + openSubscriptionInfoDialog() { + const dialogRef = this.dialog.open(VideoInfoDialogComponent, { + data: { + file: this.file, + }, + minWidth: '50vw' + }); + } + deleteAndRedownload() { this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false).subscribe(res => { this.reloadSubscription.emit(true); @@ -77,8 +88,7 @@ export class SubscriptionFileCardComponent implements OnInit { } -function fancyTimeFormat(time) -{ +function fancyTimeFormat(time) { // Hours, minutes and seconds const hrs = ~~(time / 3600); const mins = ~~((time % 3600) / 60); diff --git a/src/app/subscription/subscription/subscription.component.html b/src/app/subscription/subscription/subscription.component.html index d94a0a4..44ccb41 100644 --- a/src/app/subscription/subscription/subscription.component.html +++ b/src/app/subscription/subscription/subscription.component.html @@ -1,31 +1,46 @@ -
- -
-

- {{subscription.name}} -

-
- -
+
+ +
+

+ {{subscription.name}} +

+
+ +
-
-
-
-
-

Videos

-
-
- - - search - +
+
+
+
+ + + {{filterOption['value']['label']}} + + +
+
+ +
+
+
+
+
+

Videos

+
+
+ + + search + +
-
-
-
-
- +
+
+
+ +
+
\ No newline at end of file diff --git a/src/app/subscription/subscription/subscription.component.scss b/src/app/subscription/subscription/subscription.component.scss index 10557a0..7734644 100644 --- a/src/app/subscription/subscription/subscription.component.scss +++ b/src/app/subscription/subscription/subscription.component.scss @@ -8,6 +8,13 @@ left: 15px; } +.filter-select-parent { + position: absolute; + top: 0px; + left: 20px; + display: block; +} + .search-bar { transition: all .5s ease; position: relative; @@ -29,8 +36,29 @@ .flex-grid { width: 100%; display: block; + position: relative; } + .col { width: 33%; display: inline-block; +} + +.spinner { + width: 50px; + height: 50px; + bottom: 3px; + left: 3px; + position: absolute; +} + +.save-button { + right: 25px; + position: absolute; + bottom: 25px; +} + +.save-icon { + bottom: 1px; + position: relative; } \ No newline at end of file diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index 0bcf008..496f2d1 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -17,6 +17,31 @@ export class SubscriptionComponent implements OnInit { search_mode = false; search_text = ''; searchIsFocused = false; + descendingMode = true; + filterProperties = { + 'upload_date': { + 'key': 'upload_date', + 'label': 'Upload Date', + 'property': 'upload_date' + }, + 'name': { + 'key': 'name', + 'label': 'Name', + 'property': 'title' + }, + 'file_size': { + 'key': 'file_size', + 'label': 'File Size', + 'property': 'size' + }, + 'duration': { + 'key': 'duration', + 'label': 'Duration', + 'property': 'duration' + } + }; + filterProperty = this.filterProperties['upload_date']; + downloading = false; constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router) { } @@ -27,6 +52,12 @@ export class SubscriptionComponent implements OnInit { this.getSubscription(); this.getConfig(); } + + // set filter property to cached + const cached_filter_property = localStorage.getItem('filter_property'); + if (cached_filter_property && this.filterProperties[cached_filter_property]) { + this.filterProperty = this.filterProperties[cached_filter_property]; + } } goBack() { @@ -42,6 +73,7 @@ export class SubscriptionComponent implements OnInit { } else { this.filtered_files = this.files; } + this.filterByProperty(this.filterProperty['property']); }); } @@ -72,4 +104,40 @@ export class SubscriptionComponent implements OnInit { this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue)); } + filterByProperty(prop) { + if (this.descendingMode) { + this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1)); + } else { + this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1)); + } + } + + filterOptionChanged(value) { + // this.filterProperty = value; + this.filterByProperty(value['property']); + localStorage.setItem('filter_property', value['key']); + } + + toggleModeChange() { + this.descendingMode = !this.descendingMode; + this.filterByProperty(this.filterProperty['property']); + } + + downloadContent() { + const fileNames = []; + for (let i = 0; i < this.files.length; i++) { + fileNames.push(this.files[i].path); + } + + this.downloading = true; + this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { + this.downloading = false; + const blob: Blob = res; + saveAs(blob, this.subscription.name + '.zip'); + }, err => { + console.log(err); + this.downloading = false; + }); + } + } diff --git a/src/app/subscriptions/subscriptions.component.ts b/src/app/subscriptions/subscriptions.component.ts index 9614e87..0f89c61 100644 --- a/src/app/subscriptions/subscriptions.component.ts +++ b/src/app/subscriptions/subscriptions.component.ts @@ -33,6 +33,9 @@ export class SubscriptionsComponent implements OnInit { this.postsService.getAllSubscriptions().subscribe(res => { this.subscriptions_loading = false; this.subscriptions = res['subscriptions']; + if (!this.subscriptions) { + return; + } for (let i = 0; i < this.subscriptions.length; i++) { const sub = this.subscriptions[i];