187 changes: 168 additions & 19 deletions mythtv/html/backend/src/app/schedule/schedule.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { NgForm } from '@angular/forms';
import { RecordScheduleRequest } from '../services/interfaces/dvr.interface';
import { Observable, of } from 'rxjs';
import { UtilityService } from '../services/utility.service';
import { ChannelService } from '../services/channel.service';

export interface SchedulerSummary {
refresh(): void;
Expand All @@ -23,12 +24,16 @@ export interface ScheduleLink {
interface ListEntry {
prompt: string;
value: string;
inactive?: boolean;
}

interface BoolAssociativeArray {
[key: string]: boolean
}

interface MyChannel extends Channel {
Description?: string;
}

@Component({
selector: 'app-schedule',
Expand All @@ -44,13 +49,21 @@ export class ScheduleComponent implements OnInit {
loadCount = 0;
successCount = 0;
errorCount = 0;
errortext = '';
htmlRegex = new RegExp("<TITLE>|</TITLE>");
srchTypeDisabled = true;
titleRows = 1;
subTitleRows = 1;
descriptionRows = 5;
program?: ScheduleOrProgram;
channel?: Channel;
recRule?: RecRule;
defaultTemplate?: RecRule;
reqProgram?: ScheduleOrProgram
reqChannel?: Channel
reqRecRule?: RecRule
reqDate = new Date();
reqDuration = 60;

recRules: RecRule[] = [];
playGroups: string[] = [];
Expand All @@ -63,6 +76,16 @@ export class ScheduleComponent implements OnInit {
selectedFilters: number[] = [];
templates: RecRule[] = [];
typeList: ListEntry[] = [];
allChannels: MyChannel[] = [];

srchTypeList: ListEntry[] = [
{ prompt: this.translate.instant('recrule.srch_None'), value: 'None' },
{ prompt: this.translate.instant('recrule.srch_PowerSearch'), value: 'Power Search' },
{ prompt: this.translate.instant('recrule.srch_TitleSearch'), value: 'Title Search' },
{ prompt: this.translate.instant('recrule.srch_KeywordSearch'), value: 'Keyword Search' },
{ prompt: this.translate.instant('recrule.srch_PeopleSearch'), value: 'People Search' },
{ prompt: this.translate.instant('recrule.srch_ManualSearch'), value: 'Manual Search' },
]

dupMethodList: ListEntry[] = [
{ prompt: this.translate.instant('dashboard.sched.dupmethod.none'), value: 'None' },
Expand Down Expand Up @@ -107,7 +130,7 @@ export class ScheduleComponent implements OnInit {
templateId = 0;

constructor(private dvrService: DvrService, private translate: TranslateService,
private mythService: MythService, public utility: UtilityService) {
private mythService: MythService, public utility: UtilityService, private channelService: ChannelService) {
}

ngOnInit(): void {
Expand Down Expand Up @@ -188,6 +211,11 @@ export class ScheduleComponent implements OnInit {
error: () => this.loadFail()
});
}
this.channelService.GetChannelInfoList({ Details: true }).subscribe(data => {
this.allChannels = data.ChannelInfoList.ChannelInfos;
this.allChannels.forEach(channel => channel.Description
= channel.ChanNum + ' ' + channel.ChannelName + ' (' + channel.CallSign + ')')
});
}

loadSuccess() {
Expand All @@ -208,6 +236,15 @@ export class ScheduleComponent implements OnInit {
this.reqProgram = program;
this.reqChannel = channel;
this.reqRecRule = recRule;
this.titleRows = 1;
if (program || recRule) {
this.srchTypeDisabled = true;
this.srchTypeList[0].inactive = false;
}
else {
this.srchTypeDisabled = false;
this.srchTypeList[0].inactive = true;
}
this.loadLists();
}

Expand Down Expand Up @@ -239,39 +276,62 @@ export class ScheduleComponent implements OnInit {
}
if (entry.Type == 'Recording Template') {
this.templates.push(entry);
if (entry.Title == 'Default (Template)')
if (entry.Category == 'Default')
this.defaultTemplate = entry;
}
});
if (!this.recRule) {
this.recRule = <RecRule>{ Id: 0 };
}
if (!this.recRule.Title) {
this.recRule.Title = ''
this.recRule.SubTitle = ''
this.recRule.Description = ''
if (this.defaultTemplate)
this.mergeTemplate(this.recRule, this.defaultTemplate)
ruleType = 'Not Recording';
}
if (!this.recRule.SearchType)
this.recRule.SearchType = 'None';

// if (this.program && this.channel && this.recRule.Id == 0)
if (this.program && this.channel && this.recRule.SearchType == 'None')
this.mergeProgram(this.recRule, this.program, this.channel);

if (!ruleType)
ruleType = 'Not Recording';
if (!this.recRule.StartTime)
this.recRule.StartTime = (new Date()).toISOString();

if (!this.recRule.StartTime) {
let date = new Date();
this.recRule.StartTime = date.toISOString();
this.recRule.FindDay = (date.getDay() + 1) % 7;
this.recRule.FindTime = date.toTimeString().slice(0, 8);
}

this.filterFromRec(this.recRule);
this.postProcFromRec(this.recRule);

this.recRule.Type = ruleType;
this.setupTypeList(this.recRule);

if (!this.srchTypeDisabled)
this.onSearchTypeChange();
setTimeout(() => {
if (this.recRule)
this.recRule.Type = ruleType;
this.currentForm.form.markAsPristine();
}, 10);
}

setupTypeList(recRule: RecRule) {
let ruleType = recRule.Type;
this.typeList.length = 0;
if (ruleType == 'Recording Template') {
this.typeList.push(
{
prompt: this.translate.instant('dashboard.sched.type.mod_template'),
value: 'Recording Template'
}
);
if (this.recRule.Category != 'Default') {
if (recRule.Category != 'Default' && recRule.Id > 0) {
this.typeList.push(
{
prompt: this.translate.instant('dashboard.sched.type.del_template'),
Expand All @@ -296,15 +356,15 @@ export class ScheduleComponent implements OnInit {
}
);
else {
const isManual = (this.recRule.SearchType == 'Manual Search');
const isSearch = (this.recRule.SearchType != 'None');
const isManual = (recRule.SearchType == 'Manual Search');
const isSearch = (recRule.SearchType != 'None');
this.typeList.push(
{
prompt: this.translate.instant('dashboard.sched.type.not'),
value: 'Not Recording'
}
);
if (this.recRule.CallSign && !isSearch)
if (recRule.CallSign && !isSearch || isManual)
this.typeList.push(
{
prompt: this.translate.instant('dashboard.sched.type.this'),
Expand All @@ -318,7 +378,7 @@ export class ScheduleComponent implements OnInit {
value: 'Record One'
}
);
if (!this.recRule.CallSign || isSearch)
if (!recRule.CallSign || isSearch)
this.typeList.push(
{
prompt: this.translate.instant('dashboard.sched.type.weekly'),
Expand All @@ -337,11 +397,11 @@ export class ScheduleComponent implements OnInit {
}
);
}
setTimeout(() => {
if (this.recRule)
this.recRule.Type = ruleType;
this.currentForm.form.markAsPristine();
}, 10);
// If the selected entry is no longet in the available list, set it to the
// first item in the list
if (this.typeList.findIndex((x) => x.value == recRule.Type) == -1) {
recRule.Type = this.typeList[0].value;
}
}


Expand Down Expand Up @@ -417,6 +477,92 @@ export class ScheduleComponent implements OnInit {
this.templateId = 0;
}

onSearchTypeChange() {
if (this.recRule) {
this.recRule.Title = '';
this.reqDate = new Date();
this.reqDate.setMinutes(0);
this.reqDate.setSeconds(0, 0);
this.reqDuration = 60;
this.onDateChange();
if (this.recRule.SearchType == "Manual Search") {
this.recRule.Description = '';
setTimeout(() => this.onChannelChange(), 100);
}
if (this.recRule.SearchType == "Power Search") {
this.subTitleRows = 5;
}
else {
this.subTitleRows = 1;
}
if (['None', 'Power Search'].indexOf(this.recRule.SearchType) > -1)
this.descriptionRows = 5;
else
this.descriptionRows = 1;
this.recRule.SubTitle = '';
this.recRule.Description = '';
this.setupTypeList(this.recRule);
}
this.onTitleBlur();
this.onDescriptionBlur();
}

onTitleBlur() {
if (this.recRule) {
this.recRule.Title = this.recRule.Title.trim();
if (this.recRule.Title.length > 0) {
let desc;
if (['Manual Search', 'Power Search'].indexOf(this.recRule.SearchType) > -1 && this.recRule.Title.length > 0)
desc = '(' + this.trSearch(this.recRule.SearchType) + ')'
if (this.recRule.Type == 'Recording Template' && this.recRule.Title.length > 0)
desc = '(' + this.translate.instant('recrule.template') + ')'
if (desc && this.recRule.Title.indexOf(desc) == -1)
this.recRule.Title = this.recRule.Title + ' ' + desc;
}
if (this.recRule.SearchType == 'Manual Search')
this.recRule.Description = this.recRule.Title;
}
}

onDescriptionBlur() {
if (this.recRule) {
this.recRule.Description = this.recRule.Description.trim();
if (['Title Search', 'Keyword Search', 'People Search'].indexOf(this.recRule.SearchType) > -1)
if (this.recRule.Description.length > 0)
this.recRule.Title = this.recRule.Description + ' (' + this.trSearch(this.recRule.SearchType) + ')';
else
this.recRule.Title = '';
}
}

onChannelChange() {
if (this.recRule && this.channel) {
this.recRule.ChanId = this.channel.ChanId;
this.recRule.CallSign = this.channel.CallSign;
}
}

onDateChange() {
if (this.recRule) {
this.recRule.StartTime = this.reqDate.toISOString();
this.recRule.FindDay = (this.reqDate.getDay() + 1) % 7;
this.recRule.FindTime = this.reqDate.toTimeString().slice(0, 8);
this.onDurationChange();
}
}

onDurationChange() {
if (this.recRule) {
let start = new Date(this.recRule.StartTime);
let end = new Date(start.getTime() + this.reqDuration * 60000);
this.recRule.EndTime = end.toISOString();
}
}

trSearch(value: string): string {
return this.translate.instant('recrule.srch_' + value.replace(' ', ''));
}

mergeTemplate(recRule: RecRule, template: RecRule) {
recRule.Inactive = template.Inactive;
recRule.RecPriority = template.RecPriority;
Expand Down Expand Up @@ -446,7 +592,6 @@ export class ScheduleComponent implements OnInit {
this.filterFromRec(recRule);
}


close() {

if (!this.recRule) {
Expand Down Expand Up @@ -508,12 +653,18 @@ export class ScheduleComponent implements OnInit {
},
error: (err: any) => {
console.error(err);
if (err.status == 400) {
let parts = (<string>err.error).split(this.htmlRegex);
if (parts.length > 1)
this.errortext = parts[1];
}
this.errorCount++;
this.currentForm.form.markAsDirty();
},
};

save() {
this.errortext = '';
if (!this.recRule)
return;
if (this.recRule.Id == 0 && this.recRule.Type == 'Not Recording')
Expand Down Expand Up @@ -609,6 +760,4 @@ export class ScheduleComponent implements OnInit {
}
}



}
13 changes: 9 additions & 4 deletions mythtv/libs/libmythtv/recordingrule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -921,13 +921,18 @@ bool RecordingRule::IsValid(QString &msg) const

if (m_searchType == kPowerSearch)
{
if (m_description.contains(';') || m_subtitle.contains(';'))
{
msg = QString("Invalid SQL, contains semicolon");
return false;
}
MSqlQuery query(MSqlQuery::InitCon());
query.prepare(QString("SELECT NULL FROM (program, channel) "
"%1 WHERE %2")
query.prepare(QString("SELECT NULL FROM (program, channel, oldrecorded AS oldrecstatus) "
"%1 WHERE %2 LIMIT 5")
.arg(m_subtitle, m_description));
if (m_description.contains(';') || !query.exec())
if (!query.exec())
{
msg = QString("Invalid custom search values.");
msg = QString("Invalid SQL Where clause." + query.lastError().databaseText());
return false;
}
}
Expand Down
2 changes: 2 additions & 0 deletions mythtv/libs/libmythtv/tv_rec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3337,6 +3337,8 @@ void TVRec::RingBufferChanged(MythMediaBuffer *Buffer, RecordingInfo *pginfo, Re
{
LOG(VB_GENERAL, LOG_INFO, LOC + "RingBufferChanged()");

QMutexLocker lock(&m_stateChangeLock);

if (pginfo)
{
if (m_curRecording)
Expand Down
4 changes: 2 additions & 2 deletions mythtv/programs/mythfrontend/customedit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -787,8 +787,8 @@ bool CustomEdit::checkSyntax(void)
else
{
MSqlQuery query(MSqlQuery::InitCon());
query.prepare(QString("SELECT NULL FROM (program,channel) "
"%1 WHERE\n%2").arg(from, desc));
query.prepare(QString("SELECT NULL FROM (program, channel, oldrecorded AS oldrecstatus) "
"%1 WHERE %2 LIMIT 5").arg(from, desc));

if (query.exec())
{
Expand Down