Skip to content

Commit

Permalink
Support additional grouping options in program lister and finder.
Browse files Browse the repository at this point in the history
Traditionally, MythTV has grouped programs by channel number and
callsign when displaying guide data in the EPG, program lister and
program finder.  This is done to unclutter the listings when the same
channel is available on multiple videosources.

This change adds support for the new grouping options listed below to
the program lister and program finder.  The EPG is not affected by
this change.  The traditional grouping by channel number and callsign
is called simply "Group By Channel Number".

Group By Call Sign: This option groups solely by callsign.  It is
useful when the same channel is available on multiple videosources but
with different channel numbers.

Group By Program ID: This option groups solely by program ID.  It is
useful when the same program is simulcast on multiple channels.  For
example, at present, many NBA and NHL games are simulcast on TNT and
TruTV.  In lineups that include SD and HD channels and/or Eastern and
Pacific channels, the can result in the same program being on up to 8
channels!  with this grouping option, those programs will only be
displayed once.

Group By None: This option doesn't perform any grouping at all.  It is
mainly included for completeness and for any rare cases where it might
prove useful.

The new grouping options are "sticky" in that they remain in effect
for all future guide displays until it is changed.  The
MythCenter-Wide theme has been updated to display the current grouping
option.  It is hoped that other them authors will also update their
themese.

Finally, this new grouping is deterministic.  This is in contrast to
the previous grouping which relied on MySQL's liberal grouping support
and could return non-deterministic results.  This new grouping chooses
programs using the following preferences: highest channel priority,
lowest videosource, lowest channel number.
  • Loading branch information
gigem committed Jan 15, 2025
1 parent 4954da3 commit eb7b1fc
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 82 deletions.
94 changes: 67 additions & 27 deletions mythtv/libs/libmythbase/programinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5658,7 +5658,8 @@ QStringList ProgramInfo::LoadFromScheduler(
// results returned for any of it's users.
static bool FromProgramQuery(const QString &sql, const MSqlBindings &bindings,
MSqlQuery &query, const uint start,
const uint limit, uint &count)
const uint limit, uint &count,
ProgGroupBy::Type groupBy)
{
count = 0;

Expand All @@ -5677,15 +5678,43 @@ static bool FromProgramQuery(const QString &sql, const MSqlBindings &bindings,
"program.season, program.episode, program.totalepisodes "); // 29-31

QString querystr = QString(
"SELECT %1 "
"FROM program "
"LEFT JOIN channel ON program.chanid = channel.chanid "
"LEFT JOIN oldrecorded AS oldrecstatus ON "
" oldrecstatus.future = 0 AND "
" program.title = oldrecstatus.title AND "
" channel.callsign = oldrecstatus.station AND "
" program.starttime = oldrecstatus.starttime "
) + sql;
"SELECT %1 FROM ( "
" SELECT %2 "
" FROM program "
" LEFT JOIN channel ON program.chanid = channel.chanid "
" LEFT JOIN oldrecorded AS oldrecstatus ON "
" oldrecstatus.future = 0 AND "
" program.title = oldrecstatus.title AND "
" channel.callsign = oldrecstatus.station AND "
" program.starttime = oldrecstatus.starttime "
) + sql +
") groupsq ";

// If a ProgGroupBy option is specified, wrap the query in an outer
// query using row_number() and select only rows with value 1. We
// do this instead of relying on MySQL's liberal support for group
// by on non-aggregated columns because it is deterministic.
if (groupBy != ProgGroupBy::None)
{
columns +=
", row_number() over ( "
" partition by ";
if (groupBy == ProgGroupBy::ChanNum)
columns += "channel.channum, "
" channel.callsign, ";
else if (groupBy == ProgGroupBy::CallSign)
columns += "channel.callsign, ";
else if (groupBy == ProgGroupBy::ProgramId)
columns += "program.programid, ";
columns +=
" program.title, "
" program.starttime "
" order by channel.recpriority desc, "
" channel.sourceid, "
" channel.channum+0 "
") grouprn ";
querystr += "WHERE grouprn = 1 ";
}

// If a limit arg was given then append the LIMIT, otherwise set a hard
// limit of 20000.
Expand Down Expand Up @@ -5713,7 +5742,8 @@ static bool FromProgramQuery(const QString &sql, const MSqlBindings &bindings,
// Therefore two queries is 1.4 seconds faster than one query.
if (start > 0 || limit > 0)
{
QString countStr = querystr.arg("SQL_CALC_FOUND_ROWS program.chanid");
QString countStr = querystr
.arg("SQL_CALC_FOUND_ROWS chanid", columns);
query.prepare(countStr);
for (it = bindings.begin(); it != bindings.end(); ++it)
{
Expand All @@ -5734,7 +5764,7 @@ static bool FromProgramQuery(const QString &sql, const MSqlBindings &bindings,
if (start > 0)
querystr += QString("OFFSET %1 ").arg(start);

querystr = querystr.arg(columns);
querystr = querystr.arg("*", columns);
query.prepare(querystr);
for (it = bindings.begin(); it != bindings.end(); ++it)
{
Expand Down Expand Up @@ -5770,12 +5800,13 @@ bool LoadFromProgram(ProgramList &destination, const QString &where,

// ------------------------------------------------------------------------

return LoadFromProgram(destination, queryStr, bindings, schedList, 0, 0, count);
return LoadFromProgram(destination, queryStr, bindings, schedList, 0, 0,
count);
}

bool LoadFromProgram(ProgramList &destination,
const QString &sql, const MSqlBindings &bindings,
const ProgramList &schedList)
const ProgramList &schedList, ProgGroupBy::Type groupBy)
{
uint count = 0;

Expand All @@ -5796,16 +5827,6 @@ bool LoadFromProgram(ProgramList &destination,
if (!queryStr.contains("WHERE"))
queryStr += " WHERE deleted IS NULL AND visible > 0 ";

// NOTE: Any GROUP BY clause with a LIMIT is slow, adding at least
// a couple of seconds to the query execution time

// TODO: This one seems to be dealing with eliminating duplicate channels (same
// programming, different source), but using GROUP BY for that isn't very
// efficient so another approach is required
if (!queryStr.contains("GROUP BY"))
queryStr += " GROUP BY program.starttime, channel.channum, "
" channel.callsign, program.title ";

if (!queryStr.contains("ORDER BY"))
{
queryStr += " ORDER BY program.starttime, ";
Expand All @@ -5819,13 +5840,15 @@ bool LoadFromProgram(ProgramList &destination,

// ------------------------------------------------------------------------

return LoadFromProgram(destination, queryStr, bindings, schedList, 0, 0, count);
return LoadFromProgram(destination, queryStr, bindings, schedList, 0, 0,
count, groupBy);
}

bool LoadFromProgram( ProgramList &destination,
const QString &sql, const MSqlBindings &bindings,
const ProgramList &schedList,
const uint start, const uint limit, uint &count)
const uint start, const uint limit, uint &count,
ProgGroupBy::Type groupBy)
{
destination.clear();

Expand All @@ -5845,7 +5868,7 @@ bool LoadFromProgram( ProgramList &destination,

MSqlQuery query(MSqlQuery::InitCon());
query.setForwardOnly(true);
if (!FromProgramQuery(sql, bindings, query, start, limit, count))
if (!FromProgramQuery(sql, bindings, query, start, limit, count, groupBy))
return false;

if (count == 0)
Expand Down Expand Up @@ -6570,5 +6593,22 @@ void ProgramInfo::CalculateProgress(uint64_t pos)
CalculateWatchedProgress(pos);
}

QString ProgGroupBy::toString(ProgGroupBy::Type groupBy)
{
switch (groupBy)
{
case ProgGroupBy::None:
return tr("None");
case ProgGroupBy::ChanNum:
return tr("Channel Number");
case ProgGroupBy::CallSign:
return tr("CallSign");
case ProgGroupBy::ProgramId:
return tr("ProgramId");
default:
return tr("Unknown");
}
}


/* vim: set expandtab tabstop=4 shiftwidth=4: */
24 changes: 22 additions & 2 deletions mythtv/libs/libmythbase/programinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,24 @@ class MBASE_PUBLIC ProgramInfo
QDateTime m_previewUpdate;
};

// Class for specifying the desired grouping behavior when querying
// for program data. Note that title and starttime are always used in
// all cases but None and callsign is also used in the ChanNum case.
class MBASE_PUBLIC ProgGroupBy : public QObject
{
Q_OBJECT

public:
enum Type {
None, // Don't group programs
ChanNum, // Group by number and callsign
CallSign, // Group by call sign
ProgramId // Group by program ID
};
static QString toString(ProgGroupBy::Type groupBy);
Q_ENUM(Type)
};

MBASE_PUBLIC bool LoadFromProgram(
ProgramList &destination,
const QString &where,
Expand All @@ -875,7 +893,8 @@ MBASE_PUBLIC bool LoadFromProgram(
ProgramList &destination,
const QString &sql,
const MSqlBindings &bindings,
const ProgramList &schedList);
const ProgramList &schedList,
ProgGroupBy::Type groupBy = ProgGroupBy::None);

MBASE_PUBLIC bool LoadFromProgram(
ProgramList &destination,
Expand All @@ -884,7 +903,8 @@ MBASE_PUBLIC bool LoadFromProgram(
const ProgramList &schedList,
uint start,
uint limit,
uint &count);
uint &count,
ProgGroupBy::Type groupBy = ProgGroupBy::None);

MBASE_PUBLIC ProgramInfo* LoadProgramFromProgram(
uint chanid, const QDateTime &starttime);
Expand Down
17 changes: 15 additions & 2 deletions mythtv/programs/mythbackend/servicesv2/v2guide.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ V2ProgramList* V2Guide::GetProgramList(int nStartIndex,
const QString &sSort,
bool bDescending,
bool bWithInvisible,
const QString& sCatType)
const QString& sCatType,
const QString& sGroupBy)
{
if (!rawStartTime.isNull() && !rawStartTime.isValid())
throw QString( "StartTime is invalid" );
Expand All @@ -231,6 +232,17 @@ V2ProgramList* V2Guide::GetProgramList(int nStartIndex,
if (!rawEndTime.isNull() && dtEndTime < dtStartTime)
throw QString( "EndTime is before StartTime");

ProgGroupBy::Type nGroupBy = ProgGroupBy::ChanNum;
if (!sGroupBy.isEmpty())
{
// Handle ProgGroupBy enum name
auto meta = QMetaEnum::fromType<ProgGroupBy::Type>();
bool ok = false;
nGroupBy = ProgGroupBy::Type(meta.keyToValue(sGroupBy.toLocal8Bit(), &ok));
if (!ok)
throw QString( "GroupBy is invalid" );
}

MSqlQuery query(MSqlQuery::InitCon());


Expand Down Expand Up @@ -352,7 +364,8 @@ V2ProgramList* V2Guide::GetProgramList(int nStartIndex,

uint nTotalAvailable = 0;
LoadFromProgram( progList, sSQL, bindings, schedList,
(uint)nStartIndex, (uint)nCount, nTotalAvailable);
(uint)nStartIndex, (uint)nCount, nTotalAvailable,
nGroupBy);

// ----------------------------------------------------------------------
// Build Response
Expand Down
3 changes: 2 additions & 1 deletion mythtv/programs/mythbackend/servicesv2/v2guide.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ class V2Guide : public MythHTTPService
const QString &Sort,
bool Descending,
bool WithInvisible,
const QString &CatType);
const QString &CatType,
const QString &GroupBy);

static V2Program* GetProgramDetails ( int ChanId,
const QDateTime &StartTime );
Expand Down
5 changes: 3 additions & 2 deletions mythtv/programs/mythfrontend/guidegrid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1228,7 +1228,7 @@ ProgramList GuideGrid::GetProgramList(uint chanid) const
bindings[":CHANID"] = chanid;

ProgramList dummy;
LoadFromProgram(proglist, querystr, bindings, dummy);
LoadFromProgram(proglist, querystr, bindings, dummy, ProgGroupBy::ChanNum);

return proglist;
}
Expand Down Expand Up @@ -1584,7 +1584,8 @@ ProgramList *GuideGrid::getProgramListFromProgram(int chanNum)
bindings[":STARTLIMITTS"] = starttime.addDays(-1);
bindings[":ENDTS"] = m_currentEndTime.addSecs(0 - m_currentEndTime.time().second());

LoadFromProgram(*proglist, querystr, bindings, m_recList);
LoadFromProgram(*proglist, querystr, bindings, m_recList,
ProgGroupBy::ChanNum);
}

return proglist;
Expand Down
55 changes: 30 additions & 25 deletions mythtv/programs/mythfrontend/progfind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ bool ProgFinder::Create()
UIUtilW::Assign(this, m_help1Text, "help1text");
UIUtilW::Assign(this, m_help2Text, "help2text");
UIUtilW::Assign(this, m_searchText, "search");
UIUtilW::Assign(this, m_groupByText, "groupby");

if (err)
{
Expand Down Expand Up @@ -221,35 +222,36 @@ bool ProgFinder::keyPressEvent(QKeyEvent *event)

void ProgFinder::ShowMenu(void)
{
QString label = tr("Options");
auto *menu = new MythMenu(tr("Options"), this, "menu");

MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
auto *menuPopup = new MythDialogBox(label, popupStack, "menuPopup");

if (menuPopup->Create())
if (!m_searchStr.isEmpty())
menu->AddItem(tr("Clear Search"));
menu->AddItem(tr("Edit Search"));
if (GetFocusWidget() == m_timesList && m_timesList->GetCount() > 0)
{
menuPopup->SetReturnEvent(this, "menu");
auto *sortGroupMenu = new MythMenu(tr("Sort/Group Options"), this,
"sortgroupmenu");
AddGroupMenuItems(sortGroupMenu);
menu->AddItem(tr("Sort/Group"), nullptr, sortGroupMenu);
menu->AddItem(tr("Toggle Record"));
menu->AddItem(tr("Program Details"));
menu->AddItem(tr("Upcoming"));
menu->AddItem(tr("Previously Recorded"));
menu->AddItem(tr("Custom Edit"));
menu->AddItem(tr("Program Guide"));
menu->AddItem(tr("Channel Search"));
}

if (!m_searchStr.isEmpty())
menuPopup->AddButton(tr("Clear Search"));
menuPopup->AddButton(tr("Edit Search"));
if (GetFocusWidget() == m_timesList && m_timesList->GetCount() > 0)
{
menuPopup->AddButton(tr("Toggle Record"));
menuPopup->AddButton(tr("Program Details"));
menuPopup->AddButton(tr("Upcoming"));
menuPopup->AddButton(tr("Previously Recorded"));
menuPopup->AddButton(tr("Custom Edit"));
menuPopup->AddButton(tr("Program Guide"));
menuPopup->AddButton(tr("Channel Search"));
}
MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
auto *menuPopup = new MythDialogBox(menu, popupStack, "menuPopup");

popupStack->AddScreen(menuPopup);
}
else
if (!menuPopup->Create())
{
delete menuPopup;
return;
}

popupStack->AddScreen(menuPopup);
}

void ProgFinder::customEvent(QEvent *event)
Expand All @@ -262,7 +264,8 @@ void ProgFinder::customEvent(QEvent *event)

const QString& message = me->Message();

if (message == "SCHEDULE_CHANGE")
if (message == "SCHEDULE_CHANGE"
|| message == "GROUPBY_CHANGE")
{
if (GetFocusWidget() == m_timesList)
{
Expand Down Expand Up @@ -520,8 +523,10 @@ void ProgFinder::updateShowList()
void ProgFinder::selectShowData(QString progTitle, int newCurShow)
{
progTitle = m_showList->GetValue();

QDateTime progStart = MythDate::current();
ProgGroupBy::Type groupBy = GetProgramListGroupBy();
if (m_groupByText)
m_groupByText->SetText(ProgGroupBy::toString(groupBy));

MSqlBindings bindings;
QString querystr = "WHERE program.title = :TITLE "
Expand All @@ -532,7 +537,7 @@ void ProgFinder::selectShowData(QString progTitle, int newCurShow)
bindings[":ENDTIME"] = progStart.addSecs(50 - progStart.time().second());

LoadFromScheduler(m_schedList);
LoadFromProgram(m_showData, querystr, bindings, m_schedList);
LoadFromProgram(m_showData, querystr, bindings, m_schedList, groupBy);

updateTimesList();

Expand Down
1 change: 1 addition & 0 deletions mythtv/programs/mythfrontend/progfind.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class ProgFinder : public ScheduleCommon
MythUIButtonList *m_timesList {nullptr};

MythUIText *m_searchText {nullptr};
MythUIText *m_groupByText {nullptr};
MythUIText *m_help1Text {nullptr};
MythUIText *m_help2Text {nullptr};
};
Expand Down
Loading

0 comments on commit eb7b1fc

Please sign in to comment.