mirror of
https://github.com/fergalmoran/podnoms.git
synced 2025-12-25 18:58:12 +00:00
Merge branch 'feature/storage_reporting' into develop
This commit is contained in:
@@ -31,6 +31,8 @@
|
|||||||
"node_modules/popper.js/dist/umd/popper.min.js",
|
"node_modules/popper.js/dist/umd/popper.min.js",
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.js",
|
"node_modules/bootstrap/dist/js/bootstrap.js",
|
||||||
"node_modules/quill/dist/quill.js",
|
"node_modules/quill/dist/quill.js",
|
||||||
|
"node_modules/easy-pie-chart/dist/easypiechart.js",
|
||||||
|
"node_modules/easy-pie-chart/dist/jquery.easypiechart.js",
|
||||||
"node_modules/howler/dist/howler.js"
|
"node_modules/howler/dist/howler.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -73,32 +75,6 @@
|
|||||||
"browserTarget": "podnoms-web:build"
|
"browserTarget": "podnoms-web:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
|
||||||
"options": {
|
|
||||||
"main": "src/test.ts",
|
|
||||||
"karmaConfig": "./karma.conf.js",
|
|
||||||
"polyfills": "src/polyfills.ts",
|
|
||||||
"tsConfig": "src/tsconfig.spec.json",
|
|
||||||
"scripts": [
|
|
||||||
"node_modules/jquery/dist/jquery.js",
|
|
||||||
"node_modules/cookieconsent/build/cookieconsent.min.js",
|
|
||||||
"node_modules/tether/dist/js/tether.js",
|
|
||||||
"node_modules/popper.js/dist/umd/popper.min.js",
|
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.js",
|
|
||||||
"node_modules/quill/dist/quill.js",
|
|
||||||
"node_modules/howler/dist/howler.js"
|
|
||||||
],
|
|
||||||
"styles": ["src/styles.css"],
|
|
||||||
"assets": [
|
|
||||||
"src/assets",
|
|
||||||
"src/favicon.ico",
|
|
||||||
"src/firebase-messaging-sw.js",
|
|
||||||
"src/facebook-auth.html",
|
|
||||||
"src/manifest.json"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
"lint": {
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
"options": {
|
"options": {
|
||||||
@@ -110,27 +86,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"podnoms-web-e2e": {
|
|
||||||
"root": "",
|
|
||||||
"sourceRoot": "",
|
|
||||||
"projectType": "application",
|
|
||||||
"architect": {
|
|
||||||
"e2e": {
|
|
||||||
"builder": "@angular-devkit/build-angular:protractor",
|
|
||||||
"options": {
|
|
||||||
"protractorConfig": "./protractor.conf.js",
|
|
||||||
"devServerTarget": "podnoms-web:serve"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
|
||||||
"options": {
|
|
||||||
"tsConfig": ["e2e/tsconfig.e2e.json"],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "podnoms-web",
|
"defaultProject": "podnoms-web",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve --aot",
|
"start": "ng serve --aot",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"test": "ng test",
|
|
||||||
"lint": "ng lint"
|
"lint": "ng lint"
|
||||||
},
|
},
|
||||||
"ngrxGen": {
|
"ngrxGen": {
|
||||||
@@ -29,6 +28,7 @@
|
|||||||
"@ngrx/store": "^5.1.0",
|
"@ngrx/store": "^5.1.0",
|
||||||
"@ngrx/store-devtools": "^5.1.0",
|
"@ngrx/store-devtools": "^5.1.0",
|
||||||
"@qontu/ngx-inline-editor": "^0.2.0-alpha.12",
|
"@qontu/ngx-inline-editor": "^0.2.0-alpha.12",
|
||||||
|
"@types/jquery": "^3.3.1",
|
||||||
"angular2-jwt": "^0.2.3",
|
"angular2-jwt": "^0.2.3",
|
||||||
"angular2-moment": "^1.8.0",
|
"angular2-moment": "^1.8.0",
|
||||||
"angularfire2": "^5.0.0-rc.7",
|
"angularfire2": "^5.0.0-rc.7",
|
||||||
@@ -38,12 +38,14 @@
|
|||||||
"cookieconsent": "^3.0.6",
|
"cookieconsent": "^3.0.6",
|
||||||
"core-js": "^2.5.3",
|
"core-js": "^2.5.3",
|
||||||
"dropzone": "^5.3.0",
|
"dropzone": "^5.3.0",
|
||||||
|
"easy-pie-chart": "^2.1.7",
|
||||||
"firebase": "^4.12.1",
|
"firebase": "^4.12.1",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"howler": "^2.0.9",
|
"howler": "^2.0.9",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
"lodash": "^4.17.5",
|
"lodash": "^4.17.5",
|
||||||
"ng2-toasty": "^4.0.3",
|
"ng2-toasty": "^4.0.3",
|
||||||
|
"ng2modules-easypiechart": "0.0.4",
|
||||||
"ngx-bootstrap": "^2.0.4",
|
"ngx-bootstrap": "^2.0.4",
|
||||||
"ngx-clipboard": "^10.0.0",
|
"ngx-clipboard": "^10.0.0",
|
||||||
"ngx-cookieconsent": "^1.0.1",
|
"ngx-cookieconsent": "^1.0.1",
|
||||||
@@ -70,12 +72,6 @@
|
|||||||
"codelyzer": "~4.2.1",
|
"codelyzer": "~4.2.1",
|
||||||
"jasmine-core": "~2.99.1",
|
"jasmine-core": "~2.99.1",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
"karma": "~1.7.1",
|
|
||||||
"karma-chrome-launcher": "~2.2.0",
|
|
||||||
"karma-coverage-istanbul-reporter": "~1.4.2",
|
|
||||||
"karma-jasmine": "~1.1.1",
|
|
||||||
"karma-jasmine-html-reporter": "^0.2.2",
|
|
||||||
"protractor": "~5.3.0",
|
|
||||||
"ts-node": "~5.0.1",
|
"ts-node": "~5.0.1",
|
||||||
"tslint": "~5.9.1",
|
"tslint": "~5.9.1",
|
||||||
"@types/applicationinsights-js": "^1.0.5"
|
"@types/applicationinsights-js": "^1.0.5"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { AngularFireAuthModule } from 'angularfire2/auth';
|
|||||||
import { AngularFireModule } from 'angularfire2';
|
import { AngularFireModule } from 'angularfire2';
|
||||||
import { AngularFirestoreModule } from 'angularfire2/firestore';
|
import { AngularFirestoreModule } from 'angularfire2/firestore';
|
||||||
import { QuillModule } from 'ngx-quill';
|
import { QuillModule } from 'ngx-quill';
|
||||||
|
import { EasyPieChartModule } from 'ng2modules-easypiechart';
|
||||||
|
|
||||||
import { SocialLoginModule, AuthServiceConfig } from 'angularx-social-login';
|
import { SocialLoginModule, AuthServiceConfig } from 'angularx-social-login';
|
||||||
import {
|
import {
|
||||||
@@ -81,6 +82,7 @@ import { BoilerplateComponent } from './components/boilerplate/boilerplate.compo
|
|||||||
import { BasePageComponent } from './components/base-page/base-page.component';
|
import { BasePageComponent } from './components/base-page/base-page.component';
|
||||||
import { ChatWidgetComponent } from './components/chat-widget/chat-widget.component';
|
import { ChatWidgetComponent } from './components/chat-widget/chat-widget.component';
|
||||||
import { ChatService } from 'app/services/chat.service';
|
import { ChatService } from 'app/services/chat.service';
|
||||||
|
import { BytesToHumanPipe } from './pipes/bytes-to-human.pipe';
|
||||||
|
|
||||||
const cookieConfig: NgcCookieConsentConfig = {
|
const cookieConfig: NgcCookieConsentConfig = {
|
||||||
cookie: {
|
cookie: {
|
||||||
@@ -130,6 +132,7 @@ export function provideConfig() {
|
|||||||
NavbarComponent,
|
NavbarComponent,
|
||||||
|
|
||||||
FilterEntryPipe,
|
FilterEntryPipe,
|
||||||
|
BytesToHumanPipe,
|
||||||
OrderByPipe,
|
OrderByPipe,
|
||||||
PrettyPrintPipe,
|
PrettyPrintPipe,
|
||||||
EntryListItemComponent,
|
EntryListItemComponent,
|
||||||
@@ -148,7 +151,8 @@ export function provideConfig() {
|
|||||||
SideOverlayComponent,
|
SideOverlayComponent,
|
||||||
BoilerplateComponent,
|
BoilerplateComponent,
|
||||||
BasePageComponent,
|
BasePageComponent,
|
||||||
ChatWidgetComponent
|
ChatWidgetComponent,
|
||||||
|
BytesToHumanPipe
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -172,6 +176,7 @@ export function provideConfig() {
|
|||||||
InlineEditorModule,
|
InlineEditorModule,
|
||||||
MomentModule,
|
MomentModule,
|
||||||
QuillModule,
|
QuillModule,
|
||||||
|
EasyPieChartModule,
|
||||||
ModalModule.forRoot(),
|
ModalModule.forRoot(),
|
||||||
ProgressbarModule.forRoot(),
|
ProgressbarModule.forRoot(),
|
||||||
ToastyModule.forRoot(),
|
ToastyModule.forRoot(),
|
||||||
|
|||||||
@@ -2,10 +2,17 @@
|
|||||||
<h2 class="content-heading text-black">Personal Details</h2>
|
<h2 class="content-heading text-black">Personal Details</h2>
|
||||||
<div class="row items-push">
|
<div class="row items-push">
|
||||||
<div class="col-lg-3">
|
<div class="col-lg-3">
|
||||||
<p class="text-muted">
|
<a class="block block-link-shadow text-right" href="javascript:void(0)">
|
||||||
Tell us as much or as little as you like, nobody sees this anyway
|
<div class="block-content block-content-full clearfix">
|
||||||
</p>
|
<div class="chart js-pie-chart pie-chart js-pie-chart-enabled" data-percent="60" #usageChart *ngIf="limit">
|
||||||
<h2>Avatar Image</h2>
|
<span>{{limit.storageUsed | bytesToHuman}}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">of {{limit.storageQuota| bytesToHuman}}</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="font-w600 text-center">Storage space used</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
<div class="mb-15 animated fadeIn">
|
<div class="mb-15 animated fadeIn">
|
||||||
<div class="col-sm-6 col-xl-4">
|
<div class="col-sm-6 col-xl-4">
|
||||||
<div class="options-container text-center">
|
<div class="options-container text-center">
|
||||||
@@ -44,7 +51,7 @@
|
|||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label for="crypto-settings-street-1">Slug</label>
|
<label for="crypto-settings-street-1">Slug</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="slug" name="slug" required="" [(ngModel)]="profile.slug" (input)="onSlugChanged(profile.slug)">
|
<input type="text" class="form-control" id="slug-box" name="slug-box" autocomplete="off" required="" [(ngModel)]="profile.slug" (input)="onSlugChanged(profile.slug)">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<i class="fa fa-asterisk"></i>
|
<i class="fa fa-asterisk"></i>
|
||||||
|
|||||||
@@ -2,20 +2,35 @@ import { ProfileService } from 'app/services/profile.service';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { ApplicationState } from './../../store/index';
|
import { ApplicationState } from './../../store/index';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ElementRef,
|
||||||
|
AfterViewInit,
|
||||||
|
NgZone,
|
||||||
|
ViewChildren,
|
||||||
|
ViewContainerRef
|
||||||
|
} from '@angular/core';
|
||||||
import { ProfileModel } from 'app/models/profile.model';
|
import { ProfileModel } from 'app/models/profile.model';
|
||||||
import * as fromProfile from 'app/reducers';
|
import * as fromProfile from 'app/reducers';
|
||||||
import * as fromProfileActions from 'app/actions/profile.actions';
|
import * as fromProfileActions from 'app/actions/profile.actions';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { ImageService } from 'app/services/image.service';
|
import { ImageService } from 'app/services/image.service';
|
||||||
import { BasePageComponent } from '../base-page/base-page.component';
|
import { BasePageComponent } from '../base-page/base-page.component';
|
||||||
|
import { ProfileLimitsModel } from 'app/models/profile.limits';
|
||||||
|
import { fromEvent } from 'rxjs';
|
||||||
|
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
declare let jQuery: any;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-profile',
|
selector: 'app-profile',
|
||||||
templateUrl: './profile.component.html',
|
templateUrl: './profile.component.html',
|
||||||
styleUrls: ['./profile.component.css']
|
styleUrls: ['./profile.component.css']
|
||||||
})
|
})
|
||||||
export class ProfileComponent extends BasePageComponent implements OnInit {
|
export class ProfileComponent extends BasePageComponent
|
||||||
|
implements AfterViewInit {
|
||||||
profile$: Observable<ProfileModel>;
|
profile$: Observable<ProfileModel>;
|
||||||
|
|
||||||
originalSlug: string;
|
originalSlug: string;
|
||||||
@@ -27,12 +42,19 @@ export class ProfileComponent extends BasePageComponent implements OnInit {
|
|||||||
_imageFileBuffer: File;
|
_imageFileBuffer: File;
|
||||||
|
|
||||||
@ViewChild('fileInput') fileInput: ElementRef;
|
@ViewChild('fileInput') fileInput: ElementRef;
|
||||||
|
limits$: Observable<ProfileLimitsModel>;
|
||||||
|
private usageChart: ElementRef;
|
||||||
|
|
||||||
|
@ViewChildren('usageChart', { read: ViewContainerRef })
|
||||||
|
viewContainerRefs;
|
||||||
|
limit: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _store: Store<ApplicationState>,
|
private _store: Store<ApplicationState>,
|
||||||
private _service: ProfileService,
|
private _service: ProfileService,
|
||||||
private _imageService: ImageService,
|
private _imageService: ImageService,
|
||||||
private _router: Router
|
private _router: Router,
|
||||||
|
private _zone: NgZone
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.profile$ = _store.select(fromProfile.getProfile);
|
this.profile$ = _store.select(fromProfile.getProfile);
|
||||||
@@ -41,7 +63,46 @@ export class ProfileComponent extends BasePageComponent implements OnInit {
|
|||||||
this.image.src = p.profileImage;
|
this.image.src = p.profileImage;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ngOnInit() {}
|
ngAfterViewInit() {
|
||||||
|
this._service.getLimits().subscribe((l) => {
|
||||||
|
this.limit = l;
|
||||||
|
this.viewContainerRefs.changes.subscribe((r) => {
|
||||||
|
if (this.viewContainerRefs.length !== 0) {
|
||||||
|
const el = r.first.element.nativeElement;
|
||||||
|
this._zone.runOutsideAngular(() => {
|
||||||
|
jQuery(el).easyPieChart({
|
||||||
|
easing: 'easeOutBounce',
|
||||||
|
onStep: function(from, to, percent) {
|
||||||
|
jQuery(el)
|
||||||
|
.find('.percent')
|
||||||
|
.text(Math.round(percent));
|
||||||
|
},
|
||||||
|
barColor: jQuery(this).attr('data-rel'),
|
||||||
|
trackColor: 'rgba(0,0,0,0)',
|
||||||
|
size: 84,
|
||||||
|
scaleLength: 0,
|
||||||
|
animation: 2000,
|
||||||
|
lineWidth: 9,
|
||||||
|
lineCap: 'round'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchBox = document.getElementById('slug-box');
|
||||||
|
|
||||||
|
const typeahead = fromEvent(searchBox, 'input').pipe(
|
||||||
|
map((e: KeyboardEvent) => e.target.value),
|
||||||
|
filter((text) => text.length > 2),
|
||||||
|
debounceTime(10),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((v) => this.onSlugChanged(v))
|
||||||
|
);
|
||||||
|
typeahead.subscribe((data) => {
|
||||||
|
// Handle the data from the API
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _parseImageData(file: File) {
|
private _parseImageData(file: File) {
|
||||||
const myReader: FileReader = new FileReader();
|
const myReader: FileReader = new FileReader();
|
||||||
@@ -52,10 +113,10 @@ export class ProfileComponent extends BasePageComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
myReader.readAsDataURL(file);
|
myReader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
onSlugChanged(slug: string) {
|
onSlugChanged(slug: string) : boolean {
|
||||||
this._service.checkSlug(slug).subscribe((v) => {
|
this._service.checkSlug(slug).subscribe((v) => {
|
||||||
console.log('profile.component.ts', 'onSlugChanged', v);
|
console.log('profile.component.ts', 'onSlugChanged', v);
|
||||||
if (v.status == 404) this.slugError = '';
|
if (v) this.slugError = '';
|
||||||
else this.slugError = 'Slug already exists';
|
else this.slugError = 'Slug already exists';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
7
client/src/app/models/profile.limits.ts
Normal file
7
client/src/app/models/profile.limits.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { ProfileModel } from 'app/models/profile.model';
|
||||||
|
|
||||||
|
export class ProfileLimitsModel {
|
||||||
|
user: ProfileModel;
|
||||||
|
storageQuota: number;
|
||||||
|
storageUsed: number;
|
||||||
|
}
|
||||||
19
client/src/app/pipes/bytes-to-human.pipe.ts
Normal file
19
client/src/app/pipes/bytes-to-human.pipe.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'bytesToHuman'
|
||||||
|
})
|
||||||
|
export class BytesToHumanPipe implements PipeTransform {
|
||||||
|
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
|
||||||
|
transform(bytes: number, args?: any): any {
|
||||||
|
if (bytes == 0) return '0 Bytes';
|
||||||
|
const k = 1024,
|
||||||
|
dm = 0,
|
||||||
|
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||||
|
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return (
|
||||||
|
parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ProfileModel } from 'app/models/profile.model';
|
import { ProfileModel } from 'app/models/profile.model';
|
||||||
import 'rxjs/add/operator/map';
|
import 'rxjs/add/operator/map';
|
||||||
import { Profile } from 'selenium-webdriver/firefox';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { PodnomsAuthService } from './podnoms-auth.service';
|
import { PodnomsAuthService } from './podnoms-auth.service';
|
||||||
|
import { ProfileLimitsModel } from 'app/models/profile.limits';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProfileService {
|
export class ProfileService {
|
||||||
@@ -38,9 +38,9 @@ export class ProfileService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkSlug(slug): Observable<Response> {
|
checkSlug(slug): Observable<boolean> {
|
||||||
console.log('profile.service.ts', 'checkSlug', slug);
|
console.log('profile.service.ts', 'checkSlug', slug);
|
||||||
return this._http.get<Response>(
|
return this._http.get<boolean>(
|
||||||
environment.API_HOST + '/profile/checkslug/' + slug
|
environment.API_HOST + '/profile/checkslug/' + slug
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,4 +50,9 @@ export class ProfileService {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
getLimits(): Observable<ProfileLimitsModel> {
|
||||||
|
return this._http.get<ProfileLimitsModel>(
|
||||||
|
environment.API_HOST + '/profile/limits'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"typeRoots": ["node_modules/@types"],
|
||||||
// "target": "es2015",
|
// "target": "es2015",
|
||||||
"lib": ["es2017", "dom"]
|
"lib": ["es2017", "dom"],
|
||||||
|
"plugins": [{ "name": "tslint-language-service" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ namespace PodNoms.Api.Controllers {
|
|||||||
public class ProfileController : BaseAuthController {
|
public class ProfileController : BaseAuthController {
|
||||||
|
|
||||||
public IUnitOfWork _unitOfWork { get; }
|
public IUnitOfWork _unitOfWork { get; }
|
||||||
|
|
||||||
|
|
||||||
public IMapper _mapper { get; }
|
public IMapper _mapper { get; }
|
||||||
|
private readonly IEntryRepository _entryRepository;
|
||||||
|
|
||||||
public ProfileController(IMapper mapper, IUnitOfWork unitOfWork,
|
public ProfileController(IMapper mapper, IUnitOfWork unitOfWork,
|
||||||
|
IEntryRepository entryRepository,
|
||||||
UserManager<ApplicationUser> userManager, IHttpContextAccessor contextAccessor)
|
UserManager<ApplicationUser> userManager, IHttpContextAccessor contextAccessor)
|
||||||
: base(contextAccessor, userManager) {
|
: base(contextAccessor, userManager) {
|
||||||
|
this._entryRepository = entryRepository;
|
||||||
this._mapper = mapper;
|
this._mapper = mapper;
|
||||||
this._unitOfWork = unitOfWork;
|
this._unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
@@ -45,13 +46,23 @@ namespace PodNoms.Api.Controllers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("checkslug/{slug}")]
|
[HttpGet("checkslug/{slug}")]
|
||||||
public async Task<IActionResult> CheckSlug(string slug) {
|
public async Task<ActionResult<bool>> CheckSlug(string slug) {
|
||||||
var slugValid = await _userManager.CheckSlug(slug);
|
var slugValid = await _userManager.CheckSlug(slug);
|
||||||
|
return Ok(slugValid);
|
||||||
|
}
|
||||||
|
|
||||||
if (slugValid)
|
[HttpGet("limits")]
|
||||||
return NotFound();
|
public async Task<ActionResult<ProfileLimitsViewModel>> GetProfileLimits() {
|
||||||
|
var entries = await _entryRepository.GetAllForUserAsync(_applicationUser.Id);
|
||||||
return Ok();
|
var user = _mapper.Map<ApplicationUser, ProfileViewModel>(_applicationUser);
|
||||||
|
var sum = entries.Select(x => x.AudioFileSize)
|
||||||
|
.Sum();
|
||||||
|
var vm = new ProfileLimitsViewModel {
|
||||||
|
StorageQuota = 5368709120, //5Gb
|
||||||
|
StorageUsed = sum,
|
||||||
|
User = user
|
||||||
|
};
|
||||||
|
return Ok(vm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,6 @@ namespace PodNoms.Api.Models.ViewModels {
|
|||||||
public string ImageUrl { get; set; }
|
public string ImageUrl { get; set; }
|
||||||
public string ThumbnailUrl { get; set; }
|
public string ThumbnailUrl { get; set; }
|
||||||
public string RssUrl { get; set; }
|
public string RssUrl { get; set; }
|
||||||
// public ICollection<PodcastEntryViewModel> PodcastEntries { get; set; }
|
public ICollection<PodcastEntryViewModel> PodcastEntries { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
10
server/Models/ViewModels/Resources/ProfileLimitsViewModel.cs
Normal file
10
server/Models/ViewModels/Resources/ProfileLimitsViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace PodNoms.Api.Models.ViewModels {
|
||||||
|
public class ProfileLimitsViewModel {
|
||||||
|
public decimal StorageQuota { get; set; }
|
||||||
|
public decimal StorageUsed { get; set; }
|
||||||
|
public ProfileViewModel User { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
this._logger = logger.CreateLogger<ClearOrphanAudioJob>();
|
this._logger = logger.CreateLogger<ClearOrphanAudioJob>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute() {
|
public async Task<bool> Execute() {
|
||||||
try {
|
try {
|
||||||
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_storageSettings.ConnectionString);
|
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_storageSettings.ConnectionString);
|
||||||
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
|
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
|
||||||
@@ -49,9 +49,11 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this._mailSender.SendEmailAsync("fergal.moran@gmail.com", $"ClearOrphanAudioJob: Complete {blobCount}", string.Empty);
|
await this._mailSender.SendEmailAsync("fergal.moran@gmail.com", $"ClearOrphanAudioJob: Complete {blobCount}", string.Empty);
|
||||||
|
return true;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
_logger.LogError($"Error clearing orphans\n{ex.Message}");
|
_logger.LogError($"Error clearing orphans\n{ex.Message}");
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,6 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace PodNoms.Api.Services.Jobs {
|
namespace PodNoms.Api.Services.Jobs {
|
||||||
public interface IJob {
|
public interface IJob {
|
||||||
Task Execute();
|
Task<bool> Execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,10 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
RecurringJob.AddOrUpdate<UpdateYouTubeDlJob>(x => x.Execute(), Cron.Daily(1, 30));
|
RecurringJob.AddOrUpdate<UpdateYouTubeDlJob>(x => x.Execute(), Cron.Daily(1, 30));
|
||||||
BackgroundJob.Schedule<ProcessPlaylistsJob>(x => x.Execute(3), TimeSpan.FromSeconds(1));
|
BackgroundJob.Schedule<ProcessPlaylistsJob>(x => x.Execute(3), TimeSpan.FromSeconds(1));
|
||||||
RecurringJob.AddOrUpdate<ProcessPlaylistsJob>(x => x.Execute(), Cron.Daily(2));
|
RecurringJob.AddOrUpdate<ProcessPlaylistsJob>(x => x.Execute(), Cron.Daily(2));
|
||||||
|
|
||||||
|
BackgroundJob.Schedule<ProcessRemoteAudioFileAttributesJob>(
|
||||||
|
x => x.Execute(),
|
||||||
|
TimeSpan.FromSeconds(Int16.MaxValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
this._logger = logger;
|
this._logger = logger;
|
||||||
}
|
}
|
||||||
[Mutex("ProcessPlaylistItemJob")]
|
[Mutex("ProcessPlaylistItemJob")]
|
||||||
public async Task Execute() {
|
public async Task<bool> Execute() {
|
||||||
var items = await _playlistRepository.GetUnprocessedItems();
|
var items = await _playlistRepository.GetUnprocessedItems();
|
||||||
foreach (var item in items) {
|
foreach (var item in items) {
|
||||||
await ExecuteForItem(item.VideoId, item.Playlist.Id);
|
await ExecuteForItem(item.VideoId, item.Playlist.Id);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
[Mutex("ProcessPlaylistItemJob")]
|
[Mutex("ProcessPlaylistItemJob")]
|
||||||
public async Task ExecuteForItem(string itemId, int playlistId) {
|
public async Task<bool> ExecuteForItem(string itemId, int playlistId) {
|
||||||
var item = await _playlistRepository.GetParsedItem(itemId, playlistId);
|
var item = await _playlistRepository.GetParsedItem(itemId, playlistId);
|
||||||
if (item != null && !string.IsNullOrEmpty(item.VideoType) &&
|
if (item != null && !string.IsNullOrEmpty(item.VideoType) &&
|
||||||
(item.VideoType.Equals("youtube") || item.VideoType.Equals("mixcloud"))) {
|
(item.VideoType.Equals("youtube") || item.VideoType.Equals("mixcloud"))) {
|
||||||
@@ -80,9 +81,11 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_logger.LogError($"Processing playlist item {itemId} failed");
|
_logger.LogError($"Processing playlist item {itemId} failed");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,17 +37,20 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
this._logger = logger.CreateLogger<ProcessPlaylistsJob>();
|
this._logger = logger.CreateLogger<ProcessPlaylistsJob>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute() {
|
public async Task<bool> Execute() {
|
||||||
var playlists = _playlistRepository.GetAll()
|
var playlists = _playlistRepository.GetAll()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var playlist in playlists) {
|
foreach (var playlist in playlists) {
|
||||||
await Execute(playlist.Id);
|
await Execute(playlist.Id);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
public async Task Execute(int playlistId) {
|
public async Task<bool> Execute(int playlistId) {
|
||||||
try {
|
try {
|
||||||
var playlist = await _playlistRepository.GetAsync(playlistId);
|
var playlist = await _playlistRepository.GetAsync(playlistId);
|
||||||
|
if (playlist == null)
|
||||||
|
return false;
|
||||||
var resultList = new List<ParsedItemResult>();
|
var resultList = new List<ParsedItemResult>();
|
||||||
|
|
||||||
var downloader = new AudioDownloader(playlist.SourceUrl, _helpersSettings.Downloader);
|
var downloader = new AudioDownloader(playlist.SourceUrl, _helpersSettings.Downloader);
|
||||||
@@ -62,7 +65,7 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
} else if (MixcloudParser.ValidateUrl(playlist.SourceUrl)) {
|
} else if (MixcloudParser.ValidateUrl(playlist.SourceUrl)) {
|
||||||
resultList = await _mixcloudParser.GetEntries(playlist.SourceUrl);
|
resultList = await _mixcloudParser.GetEntries(playlist.SourceUrl);
|
||||||
}
|
}
|
||||||
if (resultList != null) {
|
if (resultList != null) {
|
||||||
//order in reverse so the newest item is added first
|
//order in reverse so the newest item is added first
|
||||||
foreach (var item in resultList?.OrderBy(r => r.UploadDate)) {
|
foreach (var item in resultList?.OrderBy(r => r.UploadDate)) {
|
||||||
if (!playlist.ParsedPlaylistItems.Any(p => p.VideoId == item.Id)) {
|
if (!playlist.ParsedPlaylistItems.Any(p => p.VideoId == item.Id)) {
|
||||||
@@ -76,9 +79,11 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
_logger.LogError(ex.Message);
|
_logger.LogError(ex.Message);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
43
server/Services/Jobs/ProcessRemoteAudioFileAttributesJob.cs
Normal file
43
server/Services/Jobs/ProcessRemoteAudioFileAttributesJob.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using PodNoms.Api.Models.Settings;
|
||||||
|
using PodNoms.Api.Persistence;
|
||||||
|
using PodNoms.Api.Services.Storage;
|
||||||
|
|
||||||
|
namespace PodNoms.Api.Services.Jobs {
|
||||||
|
public class ProcessRemoteAudioFileAttributesJob : IJob {
|
||||||
|
private readonly IEntryRepository _entryRepository;
|
||||||
|
private readonly IFileUtilities _fileUtilities;
|
||||||
|
private readonly ILogger<ProcessRemoteAudioFileAttributesJob> _logger;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public ProcessRemoteAudioFileAttributesJob(IEntryRepository entryRepository,
|
||||||
|
IFileUtilities fileUtilities, ILogger<ProcessRemoteAudioFileAttributesJob> logger,
|
||||||
|
IUnitOfWork unitOfWork) {
|
||||||
|
this._logger = logger;
|
||||||
|
this._entryRepository = entryRepository;
|
||||||
|
this._fileUtilities = fileUtilities;
|
||||||
|
this._unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
public async Task<bool> Execute() {
|
||||||
|
var entries = await _entryRepository.GetAll()
|
||||||
|
.ToListAsync();
|
||||||
|
foreach (var entry in entries) {
|
||||||
|
string[] parts = entry.AudioUrl.Split("/");
|
||||||
|
|
||||||
|
if (parts.Length == 2) {
|
||||||
|
_logger.LogInformation($"Processing remote: {entry.AudioUrl}");
|
||||||
|
var size = await _fileUtilities.GetRemoteFileSize(
|
||||||
|
parts[0], parts[1]);
|
||||||
|
if (size != -1) {
|
||||||
|
entry.AudioFileSize = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _unitOfWork.CompleteAsync();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,12 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
private readonly IMailSender _sender;
|
private readonly IMailSender _sender;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public UpdateYouTubeDlJob(IMailSender sender, ILogger<UpdateYouTubeDlJob> logger){
|
public UpdateYouTubeDlJob(IMailSender sender, ILogger<UpdateYouTubeDlJob> logger) {
|
||||||
this._sender = sender;
|
this._sender = sender;
|
||||||
this._logger = logger;
|
this._logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute() {
|
public async Task<bool> Execute() {
|
||||||
_logger.LogInformation("Updating YoutubeDL");
|
_logger.LogInformation("Updating YoutubeDL");
|
||||||
|
|
||||||
var yt = new YoutubeDL();
|
var yt = new YoutubeDL();
|
||||||
@@ -24,6 +24,7 @@ namespace PodNoms.Api.Services.Jobs {
|
|||||||
|
|
||||||
var results = await _sender.SendEmailAsync("fergal.moran@gmail.com", "PodNoms: UpdateYouTubeDlJob completed", "As you were");
|
var results = await _sender.SendEmailAsync("fergal.moran@gmail.com", "PodNoms: UpdateYouTubeDlJob completed", "As you were");
|
||||||
_logger.LogInformation($"{results}");
|
_logger.LogInformation($"{results}");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ namespace PodNoms.Api.Services.Processor {
|
|||||||
localFile = entry.AudioUrl;
|
localFile = entry.AudioUrl;
|
||||||
|
|
||||||
if (File.Exists(localFile)) {
|
if (File.Exists(localFile)) {
|
||||||
var fileName = new FileInfo(localFile).Name;
|
FileInfo fileInfo = new FileInfo(localFile);
|
||||||
|
var fileName = fileInfo.Name;
|
||||||
await _fileUploader.UploadFile(localFile, _audioStorageSettings.ContainerName, fileName,
|
await _fileUploader.UploadFile(localFile, _audioStorageSettings.ContainerName, fileName,
|
||||||
"application/mpeg",
|
"application/mpeg",
|
||||||
async (p, t) => {
|
async (p, t) => {
|
||||||
@@ -60,6 +61,7 @@ namespace PodNoms.Api.Services.Processor {
|
|||||||
entry.Processed = true;
|
entry.Processed = true;
|
||||||
entry.ProcessingStatus = ProcessingStatus.Processed;
|
entry.ProcessingStatus = ProcessingStatus.Processed;
|
||||||
entry.AudioUrl = $"{_audioStorageSettings.ContainerName}/{fileName}";
|
entry.AudioUrl = $"{_audioStorageSettings.ContainerName}/{fileName}";
|
||||||
|
entry.AudioFileSize = fileInfo.Length;
|
||||||
await _unitOfWork.CompleteAsync();
|
await _unitOfWork.CompleteAsync();
|
||||||
await _sendProcessCompleteMessage(entry);
|
await _sendProcessCompleteMessage(entry);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using PodNoms.Api.Services.Realtime;
|
|||||||
using PodNoms.Api.Utils.Extensions;
|
using PodNoms.Api.Utils.Extensions;
|
||||||
|
|
||||||
namespace PodNoms.Api.Services.Storage {
|
namespace PodNoms.Api.Services.Storage {
|
||||||
|
|
||||||
internal class AzureFileUploader : IFileUploader {
|
internal class AzureFileUploader : IFileUploader {
|
||||||
private readonly StorageSettings _settings;
|
private readonly StorageSettings _settings;
|
||||||
public AzureFileUploader(IOptions<StorageSettings> settings, ILoggerFactory logger) {
|
public AzureFileUploader(IOptions<StorageSettings> settings, ILoggerFactory logger) {
|
||||||
|
|||||||
30
server/Services/Storage/AzureFileUtilities.cs
Normal file
30
server/Services/Storage/AzureFileUtilities.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.WindowsAzure.Storage;
|
||||||
|
using Microsoft.WindowsAzure.Storage.Blob;
|
||||||
|
using PodNoms.Api.Models.Settings;
|
||||||
|
|
||||||
|
namespace PodNoms.Api.Services.Storage {
|
||||||
|
internal class AzureFileUtilities : IFileUtilities {
|
||||||
|
private readonly StorageSettings _settings;
|
||||||
|
public AzureFileUtilities(IOptions<StorageSettings> settings, ILoggerFactory logger) {
|
||||||
|
this._settings = settings.Value;
|
||||||
|
}
|
||||||
|
public async Task<long> GetRemoteFileSize(string containerName, string fileName) {
|
||||||
|
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_settings.ConnectionString);
|
||||||
|
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
|
||||||
|
CloudBlobContainer container = blobClient.GetContainerReference(containerName);
|
||||||
|
var exists = await container.ExistsAsync();
|
||||||
|
if (exists) {
|
||||||
|
CloudBlockBlob blob = container.GetBlockBlobReference(fileName);
|
||||||
|
exists = await blob.ExistsAsync();
|
||||||
|
if (exists) {
|
||||||
|
await blob.FetchAttributesAsync();
|
||||||
|
return blob.Properties.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
server/Services/Storage/IFileUtilities.cs
Normal file
7
server/Services/Storage/IFileUtilities.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace PodNoms.Api.Services.Storage {
|
||||||
|
public interface IFileUtilities {
|
||||||
|
Task<long> GetRemoteFileSize(string containerName, string fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -255,6 +255,7 @@ namespace PodNoms.Api {
|
|||||||
services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService>();
|
services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService>();
|
||||||
services.AddScoped<ISupportChatService, SupportChatService>();
|
services.AddScoped<ISupportChatService, SupportChatService>();
|
||||||
services.AddScoped<IMailSender, MailgunSender>();
|
services.AddScoped<IMailSender, MailgunSender>();
|
||||||
|
services.AddScoped<IFileUtilities, AzureFileUtilities>();
|
||||||
services.AddScoped<YouTubeParser>();
|
services.AddScoped<YouTubeParser>();
|
||||||
services.AddScoped<MixcloudParser>();
|
services.AddScoped<MixcloudParser>();
|
||||||
services.AddScoped<SlackSupportClient>();
|
services.AddScoped<SlackSupportClient>();
|
||||||
|
|||||||
Reference in New Issue
Block a user