248 changes: 248 additions & 0 deletions mythtv/html/backend/src/app/dashboard/videos/videos.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { MenuItem, MessageService } from 'primeng/api';
import { Menu } from 'primeng/menu';
import { PartialObserver } from 'rxjs';
import { UpdateVideoMetadataRequest, VideoMetadataInfo } from 'src/app/services/interfaces/video.interface';
import { VideoService } from 'src/app/services/video.service';

@Component({
selector: 'app-videos',
templateUrl: './videos.component.html',
styleUrls: ['./videos.component.css'],
providers: [MessageService]
})
export class VideosComponent implements OnInit {

@ViewChild("vidsform") currentForm!: NgForm;
@ViewChild("menu") menu!: Menu;

allVideos: VideoMetadataInfo[] = [];
videos: VideoMetadataInfo[] = [];
refreshing = false;
loaded = false;
successCount = 0;
errorCount = 0;
directory: string[] = [];
video: VideoMetadataInfo = <VideoMetadataInfo>{ Title: '' };
editingVideo?: VideoMetadataInfo;
displayMetadataDlg = false;
displayUnsaved = false;

mnu_markwatched: MenuItem = { label: 'dashboard.recordings.mnu_markwatched', command: (event) => this.markwatched(event, true) };
mnu_markunwatched: MenuItem = { label: 'dashboard.recordings.mnu_markunwatched', command: (event) => this.markwatched(event, false) };
mnu_updatemeta: MenuItem = { label: 'dashboard.recordings.mnu_updatemeta', command: (event) => this.updatemeta(event) };

menuToShow: MenuItem[] = [];

msg = {
Success: 'common.success',
Failed: 'common.failed',
NetFail: 'common.networkfail',
}


constructor(private videoService: VideoService, private translate: TranslateService,
private messageService: MessageService) {
this.loadVideos();

// translations
for (const [key, value] of Object.entries(this.msg)) {
this.translate.get(value).subscribe(data => {
Object.defineProperty(this.msg, key, { value: data });
});
}

const mnu_entries = [this.mnu_markwatched, this.mnu_markunwatched, this.mnu_updatemeta];
mnu_entries.forEach(entry => {
if (entry.label)
this.translate.get(entry.label).subscribe(data =>
entry.label = data
);
});

}

ngOnInit(): void {
}


loadVideos() {
this.videoService.GetVideoList({ Sort: "FileName" }).subscribe(data => {
this.allVideos = data.VideoMetadataInfoList.VideoMetadataInfos;
// this.videos = data.VideoMetadataInfoList.VideoMetadataInfos;
this.refreshing = false;
this.filterVideos();
this.loaded = true;
});
}

formatDate(date: string): string {
if (!date)
return '';
if (date.length == 10)
date = date + ' 00:00';
return new Date(date).toLocaleDateString()
}

filterVideos() {
this.videos = [];
let prior = '';
let search = this.directory.join('/');
if (search.length > 0)
search += '/';
this.allVideos.forEach(
video => {
const parts = video.FileName.split('/');
if (video.FileName.startsWith(search)) {
if (parts.length == this.directory.length + 1) {
this.videos.push(video);
prior = '';
}
else {
let dir = parts.slice(0, this.directory.length + 1)
.join('/') + '/';
if (dir != prior) {
// Dummy directory entry
this.videos.push(<VideoMetadataInfo>{
FileName: dir,
Title: parts[this.directory.length],
ContentType: 'D', // indicates directory
Season: 0,
Episode: 0,
Length: 0
});
prior = dir;
}
}
}
}
);
}

onDirectory(subdir: string) {
this.directory.push(subdir);
this.filterVideos();
}

breadCrumb(ix: number) {
this.directory.length = ix;
this.filterVideos();
}

showMenu(video: VideoMetadataInfo, event: any) {
this.video = video;
this.menuToShow.length = 0;
if (video.Watched)
this.menuToShow.push(this.mnu_markunwatched);
else
this.menuToShow.push(this.mnu_markwatched);
this.menuToShow.push(this.mnu_updatemeta);
this.menu.toggle(event);
}


markwatched(event: any, Watched: boolean) {
this.videoService.UpdateVideoWatchedStatus(this.video.Id, Watched).subscribe({
next: (x) => {
if (x.bool) {
this.sendMessage('success', event.item.label, this.msg.Success);
this.video.Watched = Watched;
}
else
this.sendMessage('error', event.item.label, this.msg.Failed);
},
error: (err: any) => this.networkError(err)
});
}

updatemeta(event: any) {
this.editingVideo = this.video;
this.video = Object.assign({}, this.video);
this.video.ReleaseDate = new Date(this.video.ReleaseDate);
this.displayMetadataDlg = true;
}


sendMessage(severity: string, action: string, text: string, extraText?: string) {
if (extraText)
extraText = '\n' + extraText;
else
extraText = '';
this.messageService.add({
severity: severity, summary: text,
detail: action + ' ' + this.video.Title + ' ' + this.video.SubTitle + extraText,
sticky: true,
// contentStyleClass: 'recsmsg'
});
}


saveObserver: PartialObserver<any> = {
next: (x: any) => {
if (x.bool) {
console.log("saveObserver success", x);
this.successCount++;
this.successCount++;
this.currentForm.form.markAsPristine();
if (this.editingVideo)
Object.assign(this.editingVideo, this.video);
}
else {
console.log("saveObserver error", x);
this.errorCount++;
}
},
error: (err: any) => {
console.log("saveObserver error", err);
this.errorCount++;
}
};

saveVideo() {
this.successCount = 0;
this.errorCount = 0;
this.displayUnsaved = false;
let request: UpdateVideoMetadataRequest = {
Id: this.video.Id,
Episode: this.video.Episode,
Inetref: this.video.Inetref,
Plot: this.video.Description,
ReleaseDate: this.video.ReleaseDate,
Season: this.video.Season,
SubTitle: this.video.SubTitle,
Title: this.video.Title
};

this.videoService.UpdateVideoMetadata(request).subscribe(this.saveObserver);
}

networkError(err: any) {
console.log("network error", err);
this.sendMessage('error', '', this.msg.NetFail);
}

closeDialog() {
if (this.currentForm.dirty) {
if (this.displayUnsaved) {
// Close on the unsaved warning
this.displayUnsaved = false;
this.displayMetadataDlg = false;
this.editingVideo = undefined;
this.currentForm.form.markAsPristine();
}
else
// Close on the channel edit dialog
// Open the unsaved warning
this.displayUnsaved = true;
}
else {
// Close on the channel edit dialog
this.displayMetadataDlg = false;
this.displayUnsaved = false;
this.editingVideo = undefined;
};
}

}
2 changes: 1 addition & 1 deletion mythtv/html/backend/src/app/services/dvr.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class DvrService {
}

public DisableRecordSchedule(recordid: number): Observable<BoolResponse> {
return this.httpClient.post<BoolResponse>('/Dvr/DisableRecordSchedule', recordid);
return this.httpClient.post<BoolResponse>('/Dvr/DisableRecordSchedule', {RecordId: recordid});
}

public DupInToDescription(DupIn: string): Observable<string> {
Expand Down
105 changes: 105 additions & 0 deletions mythtv/html/backend/src/app/services/interfaces/video.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { ArtworkInfoList } from "./artwork.interface";
import { CastMemberList } from "./castmember.interface";

export interface Genre {
Name: string;
}

export interface VideoMetadataInfo {
Id: number;
Title: string;
SubTitle: string;
Tagline: string;
Director: string;
Studio: string;
Description: string;
Certification: string;
Inetref: string;
Collectionref: number;
HomePage: string;
ReleaseDate: Date; // dateTime
AddDate: string; // dateTime
UserRating: number;
ChildID: number;
Length: number;
PlayCount: number;
Season: number;
Episode: number;
ParentalLevel: number;
Visible: boolean;
Watched: boolean;
Processed: boolean;
ContentType: string;
FileName: string;
Hash: string;
HostName: string;
Coverart: string;
Fanart: string;
Banner: string;
Screenshot: string;
Trailer: string;
Artwork: ArtworkInfoList;
Cast: CastMemberList;
Genres: {
GenreList: Genre[];
}
}

export interface VideoMetadataInfoList {
StartIndex: number;
Count: number;
TotalAvailable: number;
AsOf: string; // dateTime
Version: string;
ProtoVer: string;
VideoMetadataInfos: VideoMetadataInfo[];
}

export interface GetVideoListRequest {
Folder?: string;
Sort?: string;
Descending?: boolean;
StartIndex?: number;
Count?: number;
}

export interface UpdateVideoMetadataRequest {
Id: number;
Title?: string;
SubTitle?: string;
TagLine?: string;
Director?: string;
Studio?: string;
Plot?: string;
Rating?: string;
Inetref?: string;
CollectionRef?: number;
HomePage?: string;
Year?: number;
ReleaseDate?: Date;
UserRating?: number;
Length?: number;
PlayCount?: number;
Season?: number;
Episode?: number;
ShowLevel?: number;
FileName?: string;
Hash?: string;
CoverFile?: string;
ChildID?: number;
Browse?: boolean;
Watched?: boolean;
Processed?: boolean;
PlayCommand?: string;
Category?: number;
Trailer?: string;
Host?: string;
Screenshot?: string;
Banner?: string;
Fanart?: string;
InsertDate?: Date;
ContentType?: string;
Genres?: string;
Cast?: string;
Countries?: string;
}
16 changes: 16 additions & 0 deletions mythtv/html/backend/src/app/services/video.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { VideoService } from './video.service';

describe('VideoService', () => {
let service: VideoService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(VideoService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
32 changes: 32 additions & 0 deletions mythtv/html/backend/src/app/services/video.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { GetVideoListRequest, UpdateVideoMetadataRequest, VideoMetadataInfoList } from './interfaces/video.interface';
import { Observable } from 'rxjs';
import { BoolResponse } from './interfaces/common.interface';

@Injectable({
providedIn: 'root'
})
export class VideoService {

constructor(private httpClient: HttpClient) { }

// All parameters are optional
public GetVideoList(request: GetVideoListRequest):
Observable<{ VideoMetadataInfoList: VideoMetadataInfoList }> {
let params = new HttpParams();
for (const [key, value] of Object.entries(request))
params = params.set(key, value);
return this.httpClient.get<{ VideoMetadataInfoList: VideoMetadataInfoList }>
('/Video/GetVideoList', { params });
}

public UpdateVideoWatchedStatus(id: number, watched: boolean) : Observable<BoolResponse> {
return this.httpClient.post<BoolResponse>('/Video/UpdateVideoWatchedStatus', {Id: id, Watched: watched});
}

public UpdateVideoMetadata(request: UpdateVideoMetadataRequest) : Observable<BoolResponse> {
return this.httpClient.post<BoolResponse>('/Video/UpdateVideoMetadata', request);
}

}
1 change: 1 addition & 0 deletions mythtv/html/backend/src/proxy.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const PROXY_CONFIG = [
"/Content",
"/Dvr",
"/Channel",
"/Video",
],
target: "http://localhost:6744",
}
Expand Down