Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -1034,3 +1034,5 @@ native/SageWorkspace.VC.VC.opendb
/nbproject/private/
/nbbuild/
/dist/
.github/copilot-instructions.md
.vscode/settings.json
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Next

## Version 9.2.17 (2026-08-04)
* Fixes to better match SD API for error handling
* Fix for running EPG maintenance at specified time

## Version 9.2.16 (2025-11-20)
* Fix for lockouts with SD EPG images bring requested with wrong agent and missing token
* Further fixes to send 14 character program ids
Expand Down
10 changes: 10 additions & 0 deletions i18n/SageTVCoreTranslations.properties
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,16 @@ LINEUP_SD_ACCOUNT_LOCKOUT=Schedules Direct Account Locked
LINEUP_SD_ACCOUNT_LOCKOUT_MSG=The Schedules Direct account is locked. There have been too many login failures. The account will unlock again in 15 minutes.
LINEUP_SD_ACCOUNT_BLOCKED=Schedules Direct Account Blocked
LINEUP_SD_ACCOUNT_BLOCKED_MSG=The Schedules Direct account is blocked. Please contact Schedules Direct to resolve this issue.
LINEUP_SD_TOO_MANY_LOGINS=Schedules Direct Too Many Logins
LINEUP_SD_TOO_MANY_LOGINS_MSG=Exceeded maximum number of Schedules Direct logins in 24 hours. EPG updates will resume in 24 hours. Please contact Schedules Direct support if the problem persists.
LINEUP_SD_APPLICATION_DISABLED=Schedules Direct Application Disabled
LINEUP_SD_APPLICATION_DISABLED_MSG=The Schedules Direct application access has been disabled. This application is not authorized to use the data service. Please contact Schedules Direct support: service@schedulesdirect.org for more information.
LINEUP_SD_ACCOUNT_INACTIVE=Schedules Direct Account Inactive
LINEUP_SD_ACCOUNT_INACTIVE_MSG=The Schedules Direct account is inactive. Please log in to the Schedules Direct website to reactivate your account.
LINEUP_SD_TOO_MANY_UNIQUE_IPS=Schedules Direct Too Many IP Addresses
LINEUP_SD_TOO_MANY_UNIQUE_IPS_MSG=Exceeded maximum number of unique IP addresses for Schedules Direct in 24 hours. EPG updates will resume in 24 hours. Please contact Schedules Direct support if the problem persists.
LINEUP_SD_SERVICE_OFFLINE=Schedules Direct Service Offline
LINEUP_SD_SERVICE_OFFLINE_MSG=The Schedules Direct service is temporarily offline. EPG updates will resume when the service is restored. Please try again later.
SOFTWARE_UPDATE_AVAILABLE=Software update available
SOFTWARE_UPDATE_AVAILABLE_MSG=New version available on Github: "{0}". Go to: "{1}" to check it out.
Team=Team
Expand Down
41 changes: 24 additions & 17 deletions java/sage/EPG.java
Original file line number Diff line number Diff line change
Expand Up @@ -633,20 +633,22 @@ public void run()

try{

if(Sage.getBoolean("wizard/" + SCHEDULED_EPG_UPDATE, false)){
if (Sage.DBG) System.out.println("EPG next scheduled maintenance offset is " + Sage.getInt("wizard/" + SCHEDULED_EPG_UPDATE_OFFSET, 0));

nextScheduledEPGUpdateTime = getNextScheduledEPGUpdateTime();
boolean scheduledUpdateEnabled = Sage.getBoolean("wizard/" + SCHEDULED_EPG_UPDATE, false);
boolean scheduledExpandDue = false;
if (scheduledUpdateEnabled)
{
int scheduledOffset = Sage.getInt("wizard/" + SCHEDULED_EPG_UPDATE_OFFSET, 0);
if (Sage.DBG) System.out.println("EPG next scheduled maintenance offset is " + scheduledOffset);
nextScheduledEPGUpdateTime = getNextScheduledEPGUpdateTime();
long scheduledWindowStart = nextScheduledEPGUpdateTime - MAINTENANCE_FREQ;
scheduledExpandDue = (scheduledWindowStart < Sage.time()) && (getLastRun() < scheduledWindowStart);
if (scheduledExpandDue && Sage.DBG)
System.out.println("EPG scheduled update window is due; running EPG expand");
}

if ((Sage.time() - wiz.getLastMaintenance() > MAINTENANCE_FREQ) || ((nextScheduledEPGUpdateTime - MAINTENANCE_FREQ) < Sage.time())){
if (Sage.DBG) System.out.println("EPG next scheduled update is ready to run as we are past the maintenance window and/or scheduled time");
reqMaintenanceType = MaintenanceType.FULL;
}
}else{
if (Sage.time() - wiz.getLastMaintenance() > MAINTENANCE_FREQ){
if (Sage.DBG) System.out.println("EPG next maintenance update is ready to run based on 24 hour frequency");
reqMaintenanceType = MaintenanceType.FULL;
}
if (Sage.time() - wiz.getLastMaintenance() > MAINTENANCE_FREQ){
if (Sage.DBG) System.out.println("EPG next maintenance update is ready to run based on 24 hour frequency");
reqMaintenanceType = MaintenanceType.FULL;
}

if (reqMaintenanceType != MaintenanceType.NONE)
Expand Down Expand Up @@ -674,11 +676,16 @@ public void run()
{
for (int i = 0; (i < sources.size()) && alive; i++)
{
if(Sage.getBoolean("wizard/" + SCHEDULED_EPG_UPDATE, false)){
if (scheduledUpdateEnabled){
//determine the wait until the next scheduled update time
//calc the next time as the user may have changed the schedule settings
nextScheduledEPGUpdateTime = getNextScheduledEPGUpdateTime();
minWait = nextScheduledEPGUpdateTime - Sage.time();
if (scheduledExpandDue)
minWait = 0;
else
{
nextScheduledEPGUpdateTime = getNextScheduledEPGUpdateTime();
minWait = nextScheduledEPGUpdateTime - Sage.time();
}
//if (Sage.DBG) System.out.println(sources.elementAt(i) + " needs a scheduled update in " + Sage.durFormat(minWait));
}else{
long currWait = sources.elementAt(i).getTimeTillUpdate();
Expand Down Expand Up @@ -879,7 +886,7 @@ private long getNextScheduledEPGUpdateTime(){
long calcNextScheduledEPGUpdateTime = cal.getTimeInMillis();

//determine if we need to add 1 to the date if we are AFTER the scheduledMaintenance time for today
if(Sage.time()>nextScheduledEPGUpdateTime){
if(Sage.time()>calcNextScheduledEPGUpdateTime){
cal.add(java.util.Calendar.DATE,1);
calcNextScheduledEPGUpdateTime = cal.getTimeInMillis();
}
Expand Down
146 changes: 124 additions & 22 deletions java/sage/EPGDataSource.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Context for the first change
// This is the beginning of the processEPGDataMsg method
// The following lines are part of the method's logic
/*
* Copyright 2015 The SageTV Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
if (!toker.hasMoreTokens()) return; // Defensive guard
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
Expand Down Expand Up @@ -162,7 +165,7 @@ else if (cdis[i].isActive() && cdis[i].getCaptureDevice().isDataScanning())
SeekerSelector.getInstance().kick();
if (dataScanAllowed && cdi != null && cdi.isActive() && cdi.getCaptureDevice().isDataScanning())
{
if (Sage.DBG) System.out.println("EPGDS " + name + " found a capture device to start data scanning with:" + cdi);
if (Sage.DBG) System.out.println("EPGDS sourceId=" + epgSourceID + " " + name + " found a capture device to start data scanning with:" + cdi);
// Now we need to find the actual stations we want to scan for and go to it!
int[] allStations = EPG.getInstance().getAllStations(providerID);
long newScannedUntil = Long.MAX_VALUE;
Expand Down Expand Up @@ -266,44 +269,74 @@ public void processEPGDataMsg(sage.msg.SageMsg msg)
* EPG-0|major-minor AN/DT|startTimeGPS|durationSeconds|language|title|description|rating|
*/

byte[] msgData = (byte[]) msg.getData();
String msgString;
try
{
msgString = new String((byte[])msg.getData(), Sage.BYTE_CHARSET);
msgString = new String(msgData, Sage.BYTE_CHARSET);
}
catch (java.io.UnsupportedEncodingException e)
{
msgString = new String((byte[])msg.getData());
msgString = new String(msgData);
}
if (((byte[])msg.getData()).length != msgString.length())
if (msgData.length != msgString.length())
throw new InternalError("Byte array length is not the same length as string and we used a byte charset!!!");
if (msgString.length() == 0) return;
try
{
int offset = 0;
java.util.StringTokenizer toker = new java.util.StringTokenizer(msgString, "|", true);
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing message type token", msgString);
return;
}
offset += toker.nextToken().length(); // First token is "EPG-0"
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing delimiter after message type", msgString);
return;
}
offset += toker.nextToken().length(); // delimiter
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing channel token", msgString);
return;
}
String chanInfo = toker.nextToken(); // Channel number and DT or AN
offset += chanInfo.length();
String chanNum = chanInfo.substring(0, chanInfo.indexOf(' '));
int chanInfoIdx = chanInfo.indexOf(' ');
if (chanInfoIdx <= 0)
{
logMalformedEPGDataMessage("invalid channel token format", msgString);
return;
}
String chanNum = chanInfo.substring(0, chanInfoIdx);
int stationID = sage.EPG.getInstance().guessStationIDFromPhysicalChannel(providerID, chanNum, chanNum.indexOf('-') != -1);
if (stationID == 0)
stationID = sage.EPG.getInstance().guessStationID(providerID, chanNum);

if (stationID > 10000)
{
// It has TMS EPG data, so do NOT overwrite it with what we have found here
// For TVTV they're station IDs overlap with the generated ones so don't take anything if using their data
//if (sage.Sage.DBG) System.out.println("Skipping EPG data message because we have that channel's EPG data from a better source");
return;
}
if (stationID == 0)
{
//if (sage.Sage.DBG) System.out.println("Skipping EPG data message because we don't have a station ID for this channel");
// if we don't have a station ID for this channel, ignore the message.
return;
}
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing delimiter before start time", msgString);
return;
}
offset += toker.nextToken().length(); // delimiter
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing start time token", msgString);
return;
}
String timeStr = toker.nextToken();
offset += timeStr.length();
long startTime;
Expand Down Expand Up @@ -333,15 +366,42 @@ else if (timeStr.startsWith("LOCAL:"))
System.out.println("ERROR parsing EPG message start time of:" + e);
return;
}
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing delimiter before duration", msgString);
return;
}
offset += toker.nextToken().length(); // delimiter
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing duration token", msgString);
return;
}
String durStr = toker.nextToken();
offset += durStr.length();
int duration = Integer.parseInt(durStr); // duration
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing delimiter before language", msgString);
return;
}
offset += toker.nextToken().length(); // delimiter
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing language token", msgString);
return;
}
String language = toker.nextToken();
offset += language.length();
if (!"|".equals(language))
{
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing delimiter after language", msgString);
return;
}
offset += toker.nextToken().length(); // delimiter
}
else
language = "";
if (language.length() > 0)
Expand All @@ -357,35 +417,51 @@ else if ("swe".equalsIgnoreCase(language))
else if ("fra".equalsIgnoreCase(language))
language = "French";
}
// Now we need to check for alternate character sets which means we'd need to switch to the byte arrays
String title="", description="";

String title = "", description = "";
for (int i = 0; i < 2 && offset < msgString.length(); i++)
{
if (msgString.charAt(offset) != '[')
{
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("missing text token", msgString);
break;
}
if (i == 0)
{
title = toker.nextToken(); // title
title = toker.nextToken();
offset += title.length();
if ("|".equals(title))
title = "";
else if (toker.hasMoreTokens())
offset += toker.nextToken().length(); // delimiter
offset += toker.nextToken().length();
}
else
{
description = toker.nextToken(); // description
description = toker.nextToken();
offset += description.length();
if ("|".equals(description))
description = "";
else if (toker.hasMoreTokens())
offset += toker.nextToken().length(); // delimiter
offset += toker.nextToken().length();
}
}
else
{
String charset = Sage.BYTE_CHARSET;
int len = msgString.indexOf('|', offset + 1) - offset;
int delimiterIdx = msgString.indexOf('|', offset + 1);
if (delimiterIdx == -1)
{
logMalformedEPGDataMessage("missing text delimiter after charset/length block", msgString);
break;
}
int len = delimiterIdx - offset;
if (len < 0)
{
logMalformedEPGDataMessage("negative text block length", msgString);
break;
}
int fullLen = len;
int baseOffset = offset;
do
Expand All @@ -408,8 +484,11 @@ else if ("len".equals(attName))
try
{
len = Integer.parseInt(attValue);
if (len < 0)
len = 0;
}
catch (NumberFormatException e){
catch (NumberFormatException e)
{
if (Sage.DBG) System.out.println("Formatting error with EPG data:" + e);
}
}
Expand All @@ -418,24 +497,33 @@ else if ("len".equals(attName))
} while (brack1 != -1 && brack1 < offset + len);
try
{
if (offset < 0 || len < 0 || offset + len > msgData.length)
{
logMalformedEPGDataMessage("text block bounds exceeded message size", msgString);
break;
}
if (i == 0)
title += new String((byte[])msg.getData(), offset, len, charset);
title += new String(msgData, offset, len, charset);
else
description += new String((byte[])msg.getData(), offset, len, charset);
description += new String(msgData, offset, len, charset);
}
catch (java.io.UnsupportedEncodingException e)
{
if (Sage.DBG) System.out.println("Unsupported encoding for EPG data of:" + charset + " err=" + e);
if (i == 0)
title += new String((byte[])msg.getData(), offset, len);
title += new String(msgData, offset, len);
else
description += new String((byte[])msg.getData(), offset, len);
description += new String(msgData, offset, len);
}
//if (Sage.DBG) System.out.println("Parsing EPG data w/ charset=" + charset + " len=" + len + ((i == 0) ? (" title=" + title) : (" desc=" + description)));
offset += len + 1;
} while (baseOffset + fullLen > offset);
do
{
if (!toker.hasMoreTokens())
{
logMalformedEPGDataMessage("token stream ended while resyncing text offset", msgString);
break;
}
baseOffset += toker.nextToken().length();
} while (baseOffset < offset);
}
Expand Down Expand Up @@ -563,6 +651,19 @@ public void setName(String s)
Sage.put(prefsRoot + EPG_NAME, name);
}

private void logMalformedEPGDataMessage(String reason, String msgString)
{
if (!Sage.DBG)
return;

int maxLen = 220;
String sample = msgString == null ? "" : msgString;
if (sample.length() > maxLen)
sample = sample.substring(0, maxLen) + "...";

System.out.println("Skipping malformed EPG data message: sourceId=" + epgSourceID + " reason=" + reason + " msg=\"" + sample + "\"");
}

public final long getLastRun()
{
return lastRun;
Expand Down Expand Up @@ -733,6 +834,7 @@ protected long getDesiredExpand()
{
return 0;
}

public final long getTimeTillUpdate()
{
return getTimeTillExpand();
Expand Down
Loading
Loading