diff --git a/backend/app.js b/backend/app.js index 2c0a05d..69c489a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -32,6 +32,9 @@ const FileSync = require('lowdb/adapters/FileSync') const adapter = new FileSync('./appdata/db.json'); const db = low(adapter) +// check if debug mode +let debugMode = process.env.YTDL_MODE === 'debug'; + // logging setup // console format @@ -39,7 +42,7 @@ const defaultFormat = winston.format.printf(({ level, message, label, timestamp return `${timestamp} ${level.toUpperCase()}: ${message}`; }); const logger = winston.createLogger({ - level: 'info', + level: !debugMode ? 'info' : 'debug', format: winston.format.combine(winston.format.timestamp(), defaultFormat), defaultMeta: {}, transports: [ @@ -65,9 +68,14 @@ db.defaults( audio: [], video: [] }, + files: { + audio: [], + video: [] + }, configWriteFlag: false, subscriptions: [], - pin_md5: '' + pin_md5: '', + files_to_db_migration_complete: false }).write(); // config values @@ -90,9 +98,6 @@ var options = null; // encryption options var url_domain = null; var updaterStatus = null; -// check if debug mode -let debugMode = process.env.YTDL_MODE === 'debug'; - if (debugMode) logger.info('YTDL-Material in debug mode!'); // check if just updated @@ -154,6 +159,56 @@ function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, p // actual functions +async function checkMigrations() { + return new Promise(async resolve => { + // 3.5->3.6 migration + const files_to_db_migration_complete = db.get('files_to_db_migration_complete').value(); + + if (!files_to_db_migration_complete) { + logger.info('Beginning migration: 3.5->3.6+') + runFilesToDBMigration().then(success => { + if (success) { logger.info('3.5->3.6+ migration complete!'); } + else { logger.error('Migration failed: 3.5->3.6+'); } + }); + } + + resolve(true); + }); +} + +async function runFilesToDBMigration() { + return new Promise(async resolve => { + try { + let mp3s = getMp3s(); + let mp4s = getMp4s(); + + for (let i = 0; i < mp3s.length; i++) { + let file_obj = mp3s[i]; + const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value(); + if (!file_already_in_db) { + logger.verbose(`Migrating file ${file_obj.id}`); + registerFileDB(file_obj.id + '.mp3', 'audio'); + } + } + + for (let i = 0; i < mp4s.length; i++) { + let file_obj = mp4s[i]; + const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value(); + if (!file_already_in_db) { + logger.verbose(`Migrating file ${file_obj.id}`); + registerFileDB(file_obj.id + '.mp4', 'video'); + } + } + + // sets migration to complete + db.set('files_to_db_migration_complete', true).write(); + resolve(true); + } catch(err) { + resolve(false); + } + }); +} + async function startServer() { if (process.env.USING_HEROKU && process.env.PORT) { // default to heroku port if using heroku @@ -435,7 +490,7 @@ async function setConfigFromEnv() { } async function loadConfig() { - return new Promise(resolve => { + return new Promise(async resolve => { url = !debugMode ? config_api.getConfigItem('ytdl_url') : 'http://localhost:4200'; backendPort = config_api.getConfigItem('ytdl_port'); usingEncryption = config_api.getConfigItem('ytdl_use_encryption'); @@ -483,6 +538,9 @@ async function loadConfig() { }, subscriptionsCheckInterval * 1000); } + // check migrations + await checkMigrations(); + // start the server here startServer(); @@ -544,6 +602,64 @@ function generateEnvVarConfigItem(key) { return {key: key, value: process['env'][key]}; } +function getMp3s() { + let mp3s = []; + var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath); + for (let i = 0; i < files.length; i++) { + let file = files[i]; + var file_path = file.substring(audioFolderPath.length, file.length); + + var stats = 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)}`; + + 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, url, uploader, size, file, upload_date); + mp3s.push(file_obj); + } + return mp3s; +} + +function getMp4s(relative_path = true) { + let mp4s = []; + var files = recFindByExt(videoFolderPath, 'mp4'); + for (let i = 0; i < files.length; i++) { + let file = files[i]; + var file_path = file.substring(videoFolderPath.length, file.length); + + var stats = fs.statSync(file); + + var id = file_path.substring(0, file_path.length-4); + var jsonobj = getJSONMp4(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)}`; + 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, url, uploader, size, file, upload_date); + mp4s.push(file_obj); + } + return mp4s; +} + function getThumbnailMp3(name) { var obj = getJSONMp3(name); @@ -856,6 +972,63 @@ function recFindByExt(base,ext,files,result) return result } +function registerFileDB(full_file_path, type) { + const file_id = full_file_path.substring(0, full_file_path.length-4); + const file_object = generateFileObject(file_id, type); + if (!file_object) { + logger.error(`Could not find associated JSON file for ${type} file ${file_id}`); + return false; + } + + file_object['uid'] = uuid(); + path_object = path.parse(file_object['path']); + file_object['path'] = path.format(path_object); + db.get(`files.${type}`) + .push(file_object) + .write(); + return file_object['uid']; +} + +function generateFileObject(id, type) { + var jsonobj = (type === 'audio') ? getJSONMp3(id) : getJSONMp4(id); + if (!jsonobj) { + return null; + } + const ext = (type === 'audio') ? '.mp3' : '.mp4' + const file_path = getTrueFileName(jsonobj['_filename'], type); // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext); + var stats = fs.statSync(path.join(__dirname, file_path)); + + 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)}`; + + var size = stats.size; + + var thumbnail = jsonobj.thumbnail; + var duration = jsonobj.duration; + var isaudio = type === 'audio'; + var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date); + return file_obj; +} + +// replaces .webm with appropriate extension +function getTrueFileName(unfixed_path, type) { + let fixed_path = unfixed_path; + + const new_ext = (type === 'audio' ? 'mp3' : 'mp4'); + let unfixed_parts = unfixed_path.split('.'); + const old_ext = unfixed_parts[unfixed_parts.length-1]; + + + if (old_ext !== new_ext) { + unfixed_parts[unfixed_parts.length-1] = new_ext; + fixed_path = unfixed_parts.join('.'); + } + return fixed_path; +} + function getAudioInfos(fileNames) { let result = []; for (let i = 0; i < fileNames.length; i++) { @@ -1156,6 +1329,7 @@ app.post('/api/tomp3', async function(req, res) { } youtubedl.exec(url, downloadConfig, {}, function(err, output) { + var uid = null; let new_date = Date.now(); let difference = (new_date - date)/1000; logger.debug(`Audio download delay: ${difference} seconds.`); @@ -1182,9 +1356,10 @@ app.post('/api/tomp3', async function(req, res) { continue; } - const filename_no_extension = removeFileExtension(output_json['_filename']); + const filepath_no_extension = removeFileExtension(output_json['_filename']); - var full_file_path = filename_no_extension + '.mp3'; + var full_file_path = filepath_no_extension + '.mp3'; + var file_name = filepath_no_extension.substring(audioFolderPath.length, filepath_no_extension.length); if (fs.existsSync(full_file_path)) { let tags = { title: output_json['title'], @@ -1193,12 +1368,14 @@ app.post('/api/tomp3', async function(req, res) { // NodeID3.create(tags, function(frame) { }) let success = NodeID3.write(tags, full_file_path); if (!success) logger.error('Failed to apply ID3 tag to audio file ' + full_file_path); + + // registers file in DB + uid = registerFileDB(full_file_path.substring(audioFolderPath.length, full_file_path.length), 'audio'); } else { - logger.info('Output mp3 does not exist'); + logger.error('Download failed: Output mp3 does not exist'); } - var file_path = filename_no_extension.substring(audioFolderPath.length, filename_no_extension.length); - if (file_path) file_names.push(file_path); + if (file_name) file_names.push(file_name); } let is_playlist = file_names.length > 1; @@ -1214,7 +1391,8 @@ app.post('/api/tomp3', async function(req, res) { var audiopathEncoded = encodeURIComponent(file_names[0]); res.send({ audiopathEncoded: audiopathEncoded, - file_names: is_playlist ? file_names : null + file_names: is_playlist ? file_names : null, + uid: uid }); } }); @@ -1293,6 +1471,7 @@ app.post('/api/tomp4', async function(req, res) { } youtubedl.exec(url, downloadConfig, {}, function(err, output) { + var uid = null; let new_date = Date.now(); let difference = (new_date - date)/1000; logger.debug(`Video download delay: ${difference} seconds.`); @@ -1318,7 +1497,12 @@ app.post('/api/tomp4', async function(req, res) { if (!output_json) { continue; } - var file_name = output_json['_filename'].replace(/^.*[\\\/]/, ''); + + // get filepath with no extension + const filepath_no_extension = removeFileExtension(output_json['_filename']); + + var full_file_path = filepath_no_extension + '.mp4'; + var file_name = filepath_no_extension.substring(audioFolderPath.length, filepath_no_extension.length); // renames file if necessary due to bug if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) { @@ -1328,11 +1512,11 @@ app.post('/api/tomp4', async function(req, res) { } catch(e) { } } - var alternate_file_name = file_name.substring(0, file_name.length-4); - var file_path = output_json['_filename'].substring(audioFolderPath.length, output_json['_filename'].length); - // remove extension from file path - var alternate_file_path = file_path.replace(/\.[^/.]+$/, "") - if (alternate_file_name) file_names.push(alternate_file_path); + + // registers file in DB + uid = registerFileDB(full_file_path.substring(videoFolderPath.length, full_file_path.length), 'video'); + + if (file_name) file_names.push(file_name); } let is_playlist = file_names.length > 1; @@ -1348,7 +1532,8 @@ app.post('/api/tomp4', async function(req, res) { var videopathEncoded = encodeURIComponent(file_names[0]); res.send({ videopathEncoded: videopathEncoded, - file_names: is_playlist ? file_names : null + file_names: is_playlist ? file_names : null, + uid: uid }); res.end("yes"); } @@ -1399,32 +1584,8 @@ app.post('/api/fileStatusMp4', function(req, res) { // gets all download mp3s app.post('/api/getMp3s', function(req, res) { - var mp3s = []; + var mp3s = db.get('files.audio').value(); // getMp3s(); var playlists = db.get('playlists.audio').value(); - var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath); - for (let i = 0; i < files.length; i++) { - let file = files[i]; - var file_path = file.substring(audioFolderPath.length, file.length); - - var stats = 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)}`; - - 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, url, uploader, size, file, upload_date); - mp3s.push(file_obj); - } res.send({ mp3s: mp3s, @@ -1435,39 +1596,111 @@ app.post('/api/getMp3s', function(req, res) { // gets all download mp4s app.post('/api/getMp4s', function(req, res) { - var mp4s = []; + var mp4s = db.get('files.video').value(); // getMp4s(); var playlists = db.get('playlists.video').value(); - var fullpath = videoFolderPath; - var files = recFindByExt(videoFolderPath, 'mp4'); - for (let i = 0; i < files.length; i++) { - let file = files[i]; - var file_path = file.substring(videoFolderPath.length, file.length); - var stats = fs.statSync(file); + res.send({ + mp4s: mp4s, + playlists: playlists + }); + res.end("yes"); +}); + +app.post('/api/getFile', function (req, res) { + var uid = req.body.uid; + var type = req.body.type; + + var file = null; + + if (!type) { + file = db.get('files.audio').find({uid: uid}).value(); + if (!file) { + file = db.get('files.video').find({uid: uid}).value(); + if (file) type = 'video'; + } else { + type = 'audio'; + } + } + + if (!file && type) db.get(`files.${type}`).find({uid: uid}).value(); + + if (file) { + res.send({ + success: true, + file: file + }); + } else { + res.send({ + success: false + }); + } +}); + +// video sharing +app.post('/api/enableSharing', function(req, res) { + var type = req.body.type; + var uid = req.body.uid; + var is_playlist = req.body.is_playlist; + try { + success = true; + if (!is_playlist && type !== 'subscription') { + db.get(`files.${type}`) + .find({uid: uid}) + .assign({sharingEnabled: true}) + .write(); + } else if (is_playlist) { + db.get(`playlists.${type}`) + .find({id: uid}) + .assign({sharingEnabled: true}) + .write(); + } else if (type === 'subscription') { + // TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every + // time they are requested from the subscription directory. + } else { + // error + success = false; + } - var id = file_path.substring(0, file_path.length-4); - var jsonobj = getJSONMp4(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)}`; - var thumbnail = jsonobj.thumbnail; - var duration = jsonobj.duration; + } catch(err) { + success = false; + } - var size = stats.size; + res.send({ + success: success + }); +}); - var isaudio = false; - var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date); - mp4s.push(file_obj); +app.post('/api/disableSharing', function(req, res) { + var type = req.body.type; + var uid = req.body.uid; + var is_playlist = req.body.is_playlist; + try { + success = true; + if (!is_playlist && type !== 'subscription') { + db.get(`files.${type}`) + .find({uid: uid}) + .assign({sharingEnabled: false}) + .write(); + } else if (is_playlist) { + db.get(`playlists.${type}`) + .find({id: uid}) + .assign({sharingEnabled: false}) + .write(); + } else if (type === 'subscription') { + // TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every + // time they are requested from the subscription directory. + } else { + // error + success = false; + } + + } catch(err) { + success = false; } res.send({ - mp4s: mp4s, - playlists: playlists + success: success }); - res.end("yes"); }); app.post('/api/subscribe', async (req, res) => { @@ -1620,7 +1853,8 @@ app.post('/api/createPlaylist', async (req, res) => { 'name': playlistName, fileNames: fileNames, id: shortid.generate(), - thumbnailURL: thumbnailURL + thumbnailURL: thumbnailURL, + type: type }; db.get(`playlists.${type}`) @@ -1633,6 +1867,31 @@ app.post('/api/createPlaylist', async (req, res) => { }) }); +app.post('/api/getPlaylist', async (req, res) => { + let playlistID = req.body.playlistID; + let type = req.body.type; + + let playlist = null; + + if (!type) { + playlist = db.get('playlists.audio').find({id: playlistID}).value(); + if (!playlist) { + playlist = db.get('playlists.video').find({id: playlistID}).value(); + if (playlist) type = 'video'; + } else { + type = 'audio'; + } + } + + if (!playlist) playlist = db.get(`playlists.${type}`).find({id: playlistID}).value(); + + res.send({ + playlist: playlist, + type: type, + success: !!playlist + }); +}); + app.post('/api/updatePlaylist', async (req, res) => { let playlistID = req.body.playlistID; let fileNames = req.body.fileNames; @@ -1682,40 +1941,50 @@ app.post('/api/deletePlaylist', async (req, res) => { // deletes mp3 file app.post('/api/deleteMp3', async (req, res) => { - var name = req.body.name; + // var name = req.body.name; + var uid = req.body.uid; + var audio_obj = db.get('files.audio').find({uid: uid}).value(); + var name = audio_obj.id; var blacklistMode = req.body.blacklistMode; var fullpath = audioFolderPath + name + ".mp3"; var wasDeleted = false; if (fs.existsSync(fullpath)) { deleteAudioFile(name, blacklistMode); + db.get('files.audio').remove({uid: uid}).write(); wasDeleted = true; res.send(wasDeleted); res.end("yes"); - } - else - { + } else if (audio_obj) { + db.get('files.audio').remove({uid: uid}).write(); + wasDeleted = true; + res.send(wasDeleted); + } else { wasDeleted = false; res.send(wasDeleted); - res.end("yes"); } }); // deletes mp4 file app.post('/api/deleteMp4', async (req, res) => { - var name = req.body.name; + var uid = req.body.uid; + var video_obj = db.get('files.video').find({uid: uid}).value(); + var name = video_obj.id; var blacklistMode = req.body.blacklistMode; var fullpath = videoFolderPath + name + ".mp4"; var wasDeleted = false; if (fs.existsSync(fullpath)) { wasDeleted = await deleteVideoFile(name, null, blacklistMode); + db.get('files.video').remove({uid: uid}).write(); // wasDeleted = true; res.send(wasDeleted); res.end("yes"); - } - else - { + } else if (video_obj) { + db.get('files.video').remove({uid: uid}).write(); + wasDeleted = true; + res.send(wasDeleted); + } else { wasDeleted = false; res.send(wasDeleted); res.end("yes"); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3784230..6f03650 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -25,8 +25,9 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTabsModule } from '@angular/material/tabs'; - import {DragDropModule} from '@angular/cdk/drag-drop'; - import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {DragDropModule} from '@angular/cdk/drag-drop'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import { AppComponent } from './app.component'; import { HttpClientModule, HttpClient } from '@angular/common/http'; import { PostsService } from 'app/posts.services'; @@ -55,6 +56,7 @@ import { VideoInfoDialogComponent } from './dialogs/video-info-dialog/video-info import { ArgModifierDialogComponent, HighlightPipe } from './dialogs/arg-modifier-dialog/arg-modifier-dialog.component'; import { UpdaterComponent } from './updater/updater.component'; import { UpdateProgressDialogComponent } from './dialogs/update-progress-dialog/update-progress-dialog.component'; +import { ShareMediaDialogComponent } from './dialogs/share-media-dialog/share-media-dialog.component'; registerLocaleData(es, 'es'); export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps) { @@ -82,7 +84,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible ArgModifierDialogComponent, HighlightPipe, UpdaterComponent, - UpdateProgressDialogComponent + UpdateProgressDialogComponent, + ShareMediaDialogComponent ], imports: [ CommonModule, @@ -117,6 +120,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible MatTabsModule, MatTooltipModule, DragDropModule, + ClipboardModule, VgCoreModule, VgControlsModule, VgOverlayPlayModule, diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.html b/src/app/dialogs/share-media-dialog/share-media-dialog.component.html new file mode 100644 index 0000000..02dff40 --- /dev/null +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.html @@ -0,0 +1,25 @@ +

+ Share playlist + Share video + Share audio +

+ + +
+
+ Enable sharing +
+
+ + + +
+
+ +
+
+
+ + + + diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.scss b/src/app/dialogs/share-media-dialog/share-media-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.spec.ts b/src/app/dialogs/share-media-dialog/share-media-dialog.component.spec.ts new file mode 100644 index 0000000..fddbc1a --- /dev/null +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShareMediaDialogComponent } from './share-media-dialog.component'; + +describe('ShareMediaDialogComponent', () => { + let component: ShareMediaDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ShareMediaDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ShareMediaDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts new file mode 100644 index 0000000..950a6cc --- /dev/null +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { PostsService } from 'app/posts.services'; + +@Component({ + selector: 'app-share-media-dialog', + templateUrl: './share-media-dialog.component.html', + styleUrls: ['./share-media-dialog.component.scss'] +}) +export class ShareMediaDialogComponent implements OnInit { + + type = null; + uid = null; + share_url = null; + sharing_enabled = null; + is_playlist = null; + + constructor(@Inject(MAT_DIALOG_DATA) public data: any, public router: Router, private snackBar: MatSnackBar, + private postsService: PostsService) { } + + ngOnInit(): void { + if (this.data) { + this.type = this.data.type; + this.uid = this.data.uid; + this.sharing_enabled = this.data.sharing_enabled; + this.is_playlist = this.data.is_playlist; + + const arg = (this.is_playlist ? ';id=' : ';uid='); + this.share_url = window.location.href.split(';')[0] + arg + this.uid; + } + } + + copiedToClipboard() { + this.openSnackBar('Copied to clipboard!'); + } + + sharingChanged(event) { + if (event.checked) { + this.postsService.enableSharing(this.uid, this.type, this.is_playlist).subscribe(res => { + if (res['success']) { + this.openSnackBar('Sharing enabled.'); + this.sharing_enabled = true; + } else { + this.openSnackBar('Failed to enable sharing.'); + } + }, err => { + this.openSnackBar('Failed to enable sharing - server error.'); + }); + } else { + this.postsService.disableSharing(this.uid, this.type, this.is_playlist).subscribe(res => { + if (res['success']) { + this.openSnackBar('Sharing disabled.'); + this.sharing_enabled = false; + } else { + this.openSnackBar('Failed to disable sharing.'); + } + }, err => { + this.openSnackBar('Failed to disable sharing - server error.'); + }); + } + } + + public openSnackBar(message: string, action: string = '') { + this.snackBar.open(message, action, { + duration: 2000, + }); + } + +} diff --git a/src/app/file-card/file-card.component.html b/src/app/file-card/file-card.component.html index 92116dc..f9f62a3 100644 --- a/src/app/file-card/file-card.component.html +++ b/src/app/file-card/file-card.component.html @@ -2,7 +2,7 @@
- {{title}} + {{title}}
ID: {{name}}
Count: {{count}}
diff --git a/src/app/file-card/file-card.component.ts b/src/app/file-card/file-card.component.ts index 16f33ed..7c3548b 100644 --- a/src/app/file-card/file-card.component.ts +++ b/src/app/file-card/file-card.component.ts @@ -18,6 +18,7 @@ export class FileCardComponent implements OnInit { @Input() title: string; @Input() length: string; @Input() name: string; + @Input() uid: string; @Input() thumbnailURL: string; @Input() isAudio = true; @Output() removeFile: EventEmitter = new EventEmitter(); @@ -47,7 +48,7 @@ export class FileCardComponent implements OnInit { deleteFile(blacklistMode = false) { if (!this.isPlaylist) { - this.postsService.deleteFile(this.name, this.isAudio, blacklistMode).subscribe(result => { + this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => { if (result === true) { this.openSnackBar('Delete success!', 'OK.'); this.removeFile.emit(this.name); diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index d7d981f..84fe86e 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -204,7 +204,7 @@
- @@ -245,7 +245,7 @@
- diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 3d0a66f..568b1ce 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -390,11 +390,11 @@ export class MainComponent implements OnInit { } } - public goToFile(name, isAudio) { + public goToFile(name, isAudio, uid) { if (isAudio) { - this.downloadHelperMp3(name, false, false); + this.downloadHelperMp3(name, uid, false, false); } else { - this.downloadHelperMp4(name, false, false); + this.downloadHelperMp4(name, uid, false, false); } } @@ -407,7 +407,7 @@ export class MainComponent implements OnInit { } else { localStorage.setItem('player_navigator', this.router.url); const fileNames = playlist.fileNames; - this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]); + this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]); } } else { // playlist not found @@ -463,7 +463,7 @@ export class MainComponent implements OnInit { // download helpers - downloadHelperMp3(name, is_playlist = false, forceView = false, new_download = null) { + downloadHelperMp3(name, uid, is_playlist = false, forceView = false, new_download = null) { this.downloadingfile = false; if (this.multiDownloadMode && !this.downloadOnlyMode) { @@ -482,7 +482,7 @@ export class MainComponent implements OnInit { if (is_playlist) { this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]); } else { - this.router.navigate(['/player', {fileNames: name, type: 'audio'}]); + this.router.navigate(['/player', {fileNames: name, type: 'audio', uid: uid}]); } } } @@ -501,7 +501,7 @@ export class MainComponent implements OnInit { } } - downloadHelperMp4(name, is_playlist = false, forceView = false, new_download = null) { + downloadHelperMp4(name, uid, is_playlist = false, forceView = false, new_download = null) { this.downloadingfile = false; if (this.multiDownloadMode && !this.downloadOnlyMode) { // do nothing @@ -519,7 +519,7 @@ export class MainComponent implements OnInit { if (is_playlist) { this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]); } else { - this.router.navigate(['/player', {fileNames: name, type: 'video'}]); + this.router.navigate(['/player', {fileNames: name, type: 'video', uid: uid}]); } } } @@ -592,7 +592,7 @@ export class MainComponent implements OnInit { this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded']; if (this.path !== '-1') { - this.downloadHelperMp3(this.path, is_playlist, false, new_download); + this.downloadHelperMp3(this.path, posts['uid'], is_playlist, false, new_download); } }, error => { // can't access server or failed to download for other reasons this.downloadingfile = false; @@ -631,7 +631,7 @@ export class MainComponent implements OnInit { this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded']; if (this.path !== '-1') { - this.downloadHelperMp4(this.path, is_playlist, false, new_download); + this.downloadHelperMp4(this.path, posts['uid'], is_playlist, false, new_download); } }, error => { // can't access server this.downloadingfile = false; diff --git a/src/app/player/player.component.css b/src/app/player/player.component.css index 68ccbb3..bc27f1f 100644 --- a/src/app/player/player.component.css +++ b/src/app/player/player.component.css @@ -45,13 +45,19 @@ .save-button { right: 25px; - position: absolute; + position: fixed; + bottom: 25px; +} + +.share-button { + left: 25px; + position: fixed; bottom: 25px; } .favorite-button { left: 25px; - position: absolute; + position: fixed; bottom: 25px; } diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 10b9f56..41ce210 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -1,4 +1,4 @@ -
+
@@ -26,8 +26,10 @@
+
+
\ No newline at end of file diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 61f2fe9..a43bfad 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -6,6 +6,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { InputDialogComponent } from 'app/input-dialog/input-dialog.component'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { ShareMediaDialogComponent } from 'app/dialogs/share-media-dialog/share-media-dialog.component'; export interface IMedia { title: string; @@ -25,6 +26,8 @@ export class PlayerComponent implements OnInit { original_playlist: string = null; playlist_updating = false; + show_player = false; + currentIndex = 0; currentItem: IMedia = null; api: VgAPI; @@ -33,9 +36,15 @@ export class PlayerComponent implements OnInit { fileNames: string[]; type: string; id = null; // used for playlists (not subscription) + uid = null; // used for non-subscription files (audio, video, playlist) subscriptionName = null; subPlaylist = null; + is_shared = false; + + db_playlist = null; + db_file = null; + baseStreamPath = null; audioFolderPath = null; videoFolderPath = null; @@ -53,9 +62,9 @@ export class PlayerComponent implements OnInit { ngOnInit(): void { this.innerWidth = window.innerWidth; - 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.uid = this.route.snapshot.paramMap.get('uid'); this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName'); this.subPlaylist = this.route.snapshot.paramMap.get('subPlaylist'); @@ -66,53 +75,22 @@ export class PlayerComponent implements OnInit { this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio']; this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video']; this.subscriptionFolderPath = result['YoutubeDLMaterial']['Subscriptions']['subscriptions_base_path']; + this.fileNames = this.route.snapshot.paramMap.get('fileNames') ? this.route.snapshot.paramMap.get('fileNames').split('|nvr|') : null; + if (!this.fileNames) { + this.is_shared = true; + } - let fileType = null; - if (this.type === 'audio') { - 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\', \'video\', or \'subscription\'.'); + if (this.uid && !this.id) { + this.getFile(); + } else if (this.id) { + this.getPlaylistFiles(); } - for (let i = 0; i < this.fileNames.length; i++) { - const fileName = this.fileNames[i]; - 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); - const hasSlash = decodedName.includes('/') || decodedName.includes('\\'); - if (hasSlash) { - label = decodedName.replace(/^.*[\\\/]/, ''); - } else { - label = decodedName; - } - const mediaObject: IMedia = { - title: fileName, - src: fullLocation, - type: fileType, - label: label - } - this.playlist.push(mediaObject); + if (this.type === 'subscription' || this.fileNames) { + this.show_player = true; + this.parseFileNames(); } - this.currentItem = this.playlist[this.currentIndex]; - this.original_playlist = JSON.stringify(this.playlist); }); // this.getFileInfos(); @@ -124,6 +102,85 @@ export class PlayerComponent implements OnInit { } + getFile() { + const already_has_filenames = !!this.fileNames; + this.postsService.getFile(this.uid, null).subscribe(res => { + this.db_file = res['file']; + if (!this.fileNames) { + // means it's a shared video + if (!this.id) { + // regular video/audio file (not playlist) + this.fileNames = [this.db_file['id']]; + this.type = this.db_file['isAudio'] ? 'audio' : 'video'; + if (!already_has_filenames) { this.parseFileNames(); } + } + } + if (this.db_file['sharingEnabled']) { + this.show_player = true; + } else if (!already_has_filenames) { + this.openSnackBar('Error: Sharing has been disabled for this video!', 'Dismiss'); + } + }); + } + + getPlaylistFiles() { + this.postsService.getPlaylist(this.id, null).subscribe(res => { + this.db_playlist = res['playlist']; + this.fileNames = this.db_playlist['fileNames']; + this.type = res['type']; + this.show_player = true; + this.parseFileNames(); + }); + } + + parseFileNames() { + let fileType = null; + if (this.type === 'audio') { + 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\', \'video\', or \'subscription\'.'); + } + this.playlist = []; + for (let i = 0; i < this.fileNames.length; i++) { + const fileName = this.fileNames[i]; + 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); + const hasSlash = decodedName.includes('/') || decodedName.includes('\\'); + if (hasSlash) { + label = decodedName.replace(/^.*[\\\/]/, ''); + } else { + label = decodedName; + } + const mediaObject: IMedia = { + title: fileName, + src: fullLocation, + type: fileType, + label: label + } + this.playlist.push(mediaObject); + } + this.currentItem = this.playlist[this.currentIndex]; + this.original_playlist = JSON.stringify(this.playlist); + } + onPlayerReady(api: VgAPI) { this.api = api; @@ -273,6 +330,26 @@ export class PlayerComponent implements OnInit { }) } + openShareDialog() { + const dialogRef = this.dialog.open(ShareMediaDialogComponent, { + data: { + uid: this.id ? this.id : this.uid, + type: this.type, + sharing_enabled: this.id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled, + is_playlist: !!this.id + }, + width: '60vw' + }); + + dialogRef.afterClosed().subscribe(res => { + if (!this.id) { + this.getFile(); + } else { + this.getPlaylistFiles(); + } + }); + } + // snackbar helper public openSnackBar(message: string, action: string) { this.snackBar.open(message, action, { diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 752cc9b..e17b508 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -102,11 +102,11 @@ export class PostsService { return this.http.post(this.path + 'setConfig', {new_config_file: config}); } - deleteFile(name: string, isAudio: boolean, blacklistMode = false) { + deleteFile(uid: string, isAudio: boolean, blacklistMode = false) { if (isAudio) { - return this.http.post(this.path + 'deleteMp3', {name: name, blacklistMode: blacklistMode}); + return this.http.post(this.path + 'deleteMp3', {uid: uid, blacklistMode: blacklistMode}); } else { - return this.http.post(this.path + 'deleteMp4', {name: name, blacklistMode: blacklistMode}); + return this.http.post(this.path + 'deleteMp4', {uid: uid, blacklistMode: blacklistMode}); } } @@ -118,6 +118,10 @@ export class PostsService { return this.http.post(this.path + 'getMp4s', {}); } + getFile(uid, type) { + return this.http.post(this.path + 'getFile', {uid: uid, type: type}); + } + downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null) { return this.http.post(this.path + 'downloadFile', {fileNames: fileName, type: type, @@ -147,6 +151,14 @@ export class PostsService { return this.http.post(this.path + 'checkPin', {input_pin: unhashed_pin}); } + enableSharing(uid, type, is_playlist) { + return this.http.post(this.path + 'enableSharing', {uid: uid, type: type, is_playlist: is_playlist}); + } + + disableSharing(uid, type, is_playlist) { + return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}); + } + createPlaylist(playlistName, fileNames, type, thumbnailURL) { return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName, fileNames: fileNames, @@ -154,6 +166,11 @@ export class PostsService { thumbnailURL: thumbnailURL}); } + getPlaylist(playlistID, type) { + return this.http.post(this.path + 'getPlaylist', {playlistID: playlistID, + type: type}); + } + updatePlaylist(playlistID, fileNames, type) { return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID, fileNames: fileNames,