Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Next

## 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
* Add SD healthcheck and add system alert if user is blocked

## Version 9.2.15 (2025-09-16)
* Fix for lockouts with SD EPG including better handling when run as service and using clients
* Ensure SD is sent 14 character program IDs
Expand Down
4 changes: 4 additions & 0 deletions i18n/SageTVCoreTranslations.properties
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ LINEUP_SD_ACCOUNT_EXPIRED=Schedules Direct Account Expired
LINEUP_SD_ACCOUNT_EXPIRED_MSG=The Schedules Direct account has expired. Please renew the account subscription to continue to receive EPG updates.
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.
SOFTWARE_UPDATE_AVAILABLE_MSG=Software update available
SOFTWARE_UPDATE_AVAILABLE=New version available on Github: "{0}". Go to: "{1}" to check it out.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a prior definition of SOFTWARE_UPDATE_AVAILABLE in this file which should be removed. The definitions of SOFTWARE_UPDATE_AVAILABLE and SOFTWARE_UPDATE_AVAILABLE_MSG should also be swapped for consistency (here and in the code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Team=Team
Guest_Voice=Guest Voice
Anchor=Anchor
Expand Down
33 changes: 29 additions & 4 deletions java/sage/MetaImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -52,8 +50,8 @@
import java.util.Vector;
import java.util.WeakHashMap;
import sage.epg.sd.SDRipper;
import sage.epg.sd.SDSageSession;
import sage.epg.sd.SDSession;
import sage.epg.sd.SDUtils;

/*
* NOTE: DON'T DO THUMBNAIL GENERATION IN HERE. WE WANT TO STORE THEM IN SEPARATE FILES
Expand Down Expand Up @@ -3218,6 +3216,7 @@ private boolean loadCacheFile()
HttpURLConnection.setFollowRedirects(true);
URL myURL = (URL) src;
URLConnection myURLConn;
boolean isSDURL = false; //identifies an Schedules Direct image that needs special handling
try
{
while (true)
Expand All @@ -3227,6 +3226,7 @@ private boolean loadCacheFile()
myURLConn.setConnectTimeout(30000);
myURLConn.setReadTimeout(30000);
if(src.toString().startsWith(SDSession.URL_VERSIONED)){
isSDURL = true;
//this is an SD supplied image so a token is required to retreive it
if (Sage.DBG) System.out.println("MetaImage.loadCacheFile: Found SD image url. src = '" + src + "'");

Expand All @@ -3245,8 +3245,9 @@ private boolean loadCacheFile()
if (Sage.DBG) System.out.println("MetaImage.loadCacheFile: No token so skipping src = '" + src + "'");
break;
}else{
if (Sage.DBG) System.out.println("MetaImage.loadCacheFile: Adding token to Request. token = '" + sdToken + "'");
if (Sage.DBG && SDSession.debugEnabled()) System.out.println("MetaImage.loadCacheFile: Adding token to Request. token = '" + sdToken + "'");
myURLConn.addRequestProperty("token", sdToken);
myURLConn.setRequestProperty("User-Agent", SDSession.USER_AGENT);
//secret SD debug mode that will send requests to their debug server. Only enable when working with SD Support
if(Sage.getBoolean("debug_sd_support", false)) {
myURLConn.addRequestProperty("RouteTo", "debug");
Expand All @@ -3259,6 +3260,30 @@ private boolean loadCacheFile()
if (myURLConn instanceof HttpURLConnection)
{
HttpURLConnection httpConn = (HttpURLConnection) myURLConn;
if (isSDURL) {
if ("application/json".equals(httpConn.getContentType())){
//SD returned an error which is within the returned json
int imageErrorCode = SDUtils.handleSDJsonErrorFromHttpResponse(httpConn);
if(imageErrorCode==1004){ //no or invalid token
is.close();
is = null;
break;
}else if(imageErrorCode==5000){ //image does not exist - do not ask again
//create a blank image to store in cache so next request returns the blank rather than asking for an image that does not exist in SD
if (Sage.DBG) System.out.println("MetaImage.loadCacheFile: error 5000 - SD image does not exist - creating a blank image for the cache to avoid re-requesting");
is = SDUtils.createBlankImageInputStream(270,360,"jpg");
break;
}else if(imageErrorCode==5002 || imageErrorCode==5003){ //5002-max downloads, 5003-max downloads trial
is.close();
is = null;
break;
}else{
is.close();
is = null;
break;
}
}
}
if (httpConn.getResponseCode() / 100 == 3)
{
if (Sage.DBG) System.out.println("Internally processing HTTP redirect...");
Expand Down
2 changes: 1 addition & 1 deletion java/sage/SageConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ private SageConstants()
// Non-instantiable
}

public static final int BUILD_VERSION = 1037;
public static final int BUILD_VERSION = 1046;
}
2 changes: 1 addition & 1 deletion java/sage/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class Version
{
public static final byte MAJOR_VERSION = 9;
public static final byte MINOR_VERSION = 2;
public static final byte MICRO_VERSION = 15;
public static final byte MICRO_VERSION = 16;

public static final String VERSION = MAJOR_VERSION + "." + MINOR_VERSION + "." + MICRO_VERSION + "." + SageConstants.BUILD_VERSION;

Expand Down
13 changes: 11 additions & 2 deletions java/sage/epg/sd/SDErrors.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sage.epg.sd;

import java.io.IOException;
import sage.Sage;
import sage.SageTV;

public enum SDErrors
{
Expand Down Expand Up @@ -56,6 +56,7 @@ public enum SDErrors
SAGETV_TOKEN_RETURN_MISSING(-2000 /*, "Schedules Direct did not return a valid token."*/),
SAGETV_SERVICE_MISSING(-2001 /*, "The requested service is not currently available from Schedules Direct."*/),
SAGETV_NO_PASSWORD(-2002 /*, "A username and password have not been provided to connect to Schedules Direct."*/),
SD_ACCOUNT_BLOCKED(-2003 /*, "Schedules Direct account is blocked. Contact SD to resolve."*/),
SAGETV_UNKNOWN(-9999 /*, "Unknown error to SageTV."*/);

public final int CODE;
Expand Down Expand Up @@ -107,15 +108,23 @@ public static SDErrors getErrorForName(String name)
* @param code The code to look up and throw.
* @throws SDException Always thrown by this method.
*/
public static void throwErrorForCode(int code) throws SDException
public static void throwErrorForCode(int code) throws SDException, IOException
{
// We don't know why this is an error when the code is 0, but this method was called, so we will
// throw an error.
if (Sage.DBG) System.out.println("SDErrors: code:" + code + " : " + getErrorForCode(code).name());
if (code != 0)
{
//do an extra check to see if the error is due to the account being blocked
//if(SDUtils.isSDBlocked()){
//this will indicate the account is blocked
// throw new SDException(getErrorForCode(-2003));
//}

for (SDErrors error : SDErrors.values())
{
if (code == error.CODE){
if (Sage.DBG) System.out.println("SDErrors: THROWING code:" + code + " : " + getErrorForCode(code).name());
throw new SDException(error);
}
}
Expand Down
33 changes: 23 additions & 10 deletions java/sage/epg/sd/SDRipper.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ private static SDSession openNewSession() throws IOException, SDException
{
SDSession returnValue = null;
BufferedReader reader = null;

//2025-02-28 jusjoken: switch to using sage properties for user/pass so it can be shared with clients that need it
String propUsername = null;
String propPassword = null;
Expand All @@ -246,7 +246,7 @@ private static SDSession openNewSession() throws IOException, SDException
}
catch (Throwable t)
{
if (Sage.DBG) System.out.println("ERROR executing server API call of:" + t);
if (Sage.DBG) System.out.println("SDRipper:openNewSession: ERROR executing server API call of:" + t);
t.printStackTrace();
propUsername = null;
propPassword = null;
Expand All @@ -270,14 +270,14 @@ private static SDSession openNewSession() throws IOException, SDException
String auth = reader.readLine();
if (auth == null)
{
if (Sage.DBG) System.out.println("SDEPG Error: sdauth file is empty.");
if (Sage.DBG) System.out.println("SDRipper:openNewSession: Error: sdauth file is empty.");
throw new SDException(SDErrors.SAGETV_NO_PASSWORD);
}
int split = auth.indexOf(' ');
// If the file is not formatted correctly, it's as good as not existing.
if (split == -1)
{
if (Sage.DBG) System.out.println("SDEPG Error: sdauth file is missing a space between the username and password.");
if (Sage.DBG) System.out.println("SDRipper:openNewSession: Error: sdauth file is missing a space between the username and password.");
throw new SDException(SDErrors.SAGETV_NO_PASSWORD);
}

Expand Down Expand Up @@ -307,9 +307,9 @@ private static SDSession openNewSession() throws IOException, SDException
// This will throw an exception if there are any issues connecting.
returnValue = new SDSageSession(propUsername, propPassword);
authenticated = true;
if (Sage.DBG) System.out.println("SDEPG authenticated using prop based user/pass PASSED: username:" + propUsername + " password:" + propPassword);
if (Sage.DBG) System.out.println("SDRipper:openNewSession: Authenticated using prop based user/pass PASSED: username:" + propUsername + " password:" + propPassword);
} catch (Exception e) {
if (Sage.DBG) System.out.println("SDEPG checking for prop based user/pass FAILED: username:" + propUsername + " password:" + propPassword);
if (Sage.DBG) System.out.println("SDRipper:openNewSession: Checking for prop based user/pass FAILED: username:" + propUsername + " password:" + propPassword);
}
}

Expand All @@ -321,22 +321,22 @@ private static SDSession openNewSession() throws IOException, SDException
authenticated = true;
Sage.put(PROP_USERNAME, fileUsername);
Sage.put(PROP_PASSWORD, filePassword);
if (Sage.DBG) System.out.println("SDEPG authenticated using file based user/pass PASSED: username:" + fileUsername + " password:" + filePassword);
if (Sage.DBG) System.out.println("SDRipper:openNewSession: Authenticated using file based user/pass PASSED: username:" + fileUsername + " password:" + filePassword);
} catch (Exception e) {
if (Sage.DBG) System.out.println("SDEPG checking for file based user/pass FAILED: username:" + fileUsername + " password:" + filePassword);
if (Sage.DBG) System.out.println("SDRipper:openNewSession: Checking for file based user/pass FAILED: username:" + fileUsername + " password:" + filePassword);
}
}

//we have now have tried both prop and file based user/pass
if(!authenticated){
if (Sage.DBG) System.out.println("SDEPG ERROR: checking for BOTH user/pass FAILED: throwing SAGETV_NO_PASSWORD");
if (Sage.DBG) System.out.println("SDRipper:openNewSession: ERROR: checking for BOTH user/pass FAILED: throwing SAGETV_NO_PASSWORD");
throw new SDException(SDErrors.SAGETV_NO_PASSWORD);
}

// We have just successfully authenticated, so this needs to be cleared so that updates can
// start immediately.
SDRipper.retryWait = 0;
if (Sage.DBG) System.out.println("SDEPG Successfully got token: " + returnValue.token);
if (Sage.DBG) System.out.println("SDRipper:openNewSession: Successfully got token: " + returnValue.token);
return returnValue;
}

Expand Down Expand Up @@ -2953,6 +2953,19 @@ public static boolean isAvailable()
if (Sage.DBG) System.out.println("SDEPG Unable to use the Schedules Direct service at this time.");
return false;
}

try {
//2025-11-14 add SD Health check to avoid running if SD has the account blocked
if(SDUtils.isSDBlocked()){
if (Sage.DBG) System.out.println("SDEPG Account is blocked: Unable to use the Schedules Direct service at this time.");
return false;
} } catch (IOException ex) {
if (Sage.DBG) System.out.println("SDEPG Account is blocked: Unable to use the Schedules Direct service at this time. " + ex.getMessage());
return false;
} catch (SDException ex) {
if (Sage.DBG) System.out.println("SDEPG Account is blocked: Unable to use the Schedules Direct service at this time. " + ex.getMessage());
return false;
}

try
{
Expand Down
1 change: 0 additions & 1 deletion java/sage/epg/sd/SDSageSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.util.Random;

public class SDSageSession extends SDSession
{
Expand Down
34 changes: 19 additions & 15 deletions java/sage/epg/sd/SDSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

public abstract class SDSession
{
Expand All @@ -85,7 +83,7 @@ public abstract class SDSession
// The character set to be used for outgoing communications.
protected static final Charset OUT_CHARSET = StandardCharsets.UTF_8;
// The expected character set to be used for incoming communications.
protected static final Charset IN_CHARSET = StandardCharsets.ISO_8859_1;
public static final Charset IN_CHARSET = StandardCharsets.ISO_8859_1;

// These are set in the static constructor because they can throw format exceptions.
// Returns a token if the credentials are valid.
Expand All @@ -95,7 +93,9 @@ public abstract class SDSession
// Get the current account status/saved lineups.
private static final URL GET_STATUS;
// Get a list of available services.
private static final URL GET_AVAILABLE;
public static final URL GET_AVAILABLE;
// Get weather the accounts ip is blocked.
public static final URL GET_IS_BLOCKED;
// Get the lineups associated with the authenticated account.
private static final URL GET_LINEUPS;
// Get requested guide data for specific programs.
Expand All @@ -116,6 +116,7 @@ public abstract class SDSession
URL newGetTokenCurrent;
URL newGetStatus;
URL newGetAvailable;
URL newGetIsBlocked;
URL newGetLineups;
URL newGetPrograms;
URL newGetSeriesDesc;
Expand All @@ -129,6 +130,7 @@ public abstract class SDSession
newGetTokenCurrent = new URL(URL_VERSIONED + "/token/current");
newGetStatus = new URL(URL_VERSIONED + "/status");
newGetAvailable = new URL(URL_VERSIONED + "/available");
newGetIsBlocked = new URL(URL_VERSIONED + "/ip_isblocked");
newGetLineups = new URL(URL_VERSIONED + "/lineups");
newGetPrograms = new URL(URL_VERSIONED + "/programs");
newGetSeriesDesc = new URL(URL_VERSIONED + "/metadata/description/");
Expand All @@ -146,6 +148,7 @@ public abstract class SDSession
newGetTokenCurrent = null;
newGetStatus = null;
newGetAvailable = null;
newGetIsBlocked = null;
newGetLineups = null;
newGetPrograms = null;
newGetSeriesDesc = null;
Expand All @@ -158,6 +161,7 @@ public abstract class SDSession
GET_TOKEN_CURRENT = newGetTokenCurrent;
GET_STATUS = newGetStatus;
GET_AVAILABLE = newGetAvailable;
GET_IS_BLOCKED = newGetIsBlocked;
GET_LINEUPS = newGetLineups;
GET_PROGRAMS = newGetPrograms;
GET_SERIES_DESC = newGetSeriesDesc;
Expand Down Expand Up @@ -384,8 +388,6 @@ private static String debugDateTime(){
*/
public synchronized void authenticate() throws IOException, SDException
{

//if(Sage.DBG) System.out.println("SDSession/authenticate: checking existing token:" + token + " with expiry:" + tokenExpiration + " against System:" + (System.currentTimeMillis()/1000));

// The token is still valid.
if (System.currentTimeMillis()/1000 < tokenExpiration && token != null)
Expand All @@ -397,6 +399,10 @@ public synchronized void authenticate() throws IOException, SDException
// Set the token to null so if we are getting a new token, it doesn't send the old token along
// for the authentication request.
token = null;
if(SDUtils.isSDBlocked()){
if(Sage.DBG) System.out.println("SDSession/authenticate: Account is blocked. Contact SD to resolve.");
return;
}

JsonObject authRequest = new JsonObject();
authRequest.addProperty("username", username);
Expand Down Expand Up @@ -952,7 +958,9 @@ public SDProgram[] getPrograms(Collection<String> programs) throws IOException,
JsonArray submit = new JsonArray();
for (String program : programs)
{
submit.add(SDUtils.fromSageTVtoProgram(program));
if(SDUtils.isValidProgramID(program)){
submit.add(SDUtils.fromSageTVtoProgram(program));
}
}

SDProgram[] returnValues = postAuthJson(GET_PROGRAMS, SDProgram[].class, submit);
Expand Down Expand Up @@ -1104,16 +1112,12 @@ public SDProgramImages[] getProgramImages(String[] programs) throws IOException,
JsonArray submit = new JsonArray();
for (String program : programs)
{
//08-12-2025 jusjoken - convert program to 14 chars needed by SD
program = SDUtils.fromSageTVtoProgram(program);
//08-12-2025 jusjoken - the below should no longer be required - remove after testing
//03-01-2025 jusjoken: added validation for program ids
//first check if its already formated correctly
if(SDUtils.isValidShortProgramID(program)){
if(SDUtils.isValidProgramID(program)){
//08-12-2025 jusjoken - convert program to 14 chars needed by SD
program = SDUtils.fromSageTVtoProgram(program);
submit.add(program);
}else if(SDUtils.isValidProgramID(program)){ //valid BUT is not shortend to 10
submit.add(program); //submit as is as SD now handles the 14 character program ids as well
//submit.add(program.substring(0, 10));
}else{
if (Sage.DBG) System.out.println("getProgramImages: INVALID program ID - SKIPPING:" + program);
}
Expand Down Expand Up @@ -1264,7 +1268,7 @@ public SDInProgressSport getInProgressSport(String programId) throws IOException
if (programId == null || programId.length() == 0)
return null;

if (programId.length() == 12)
if (SDUtils.isValidProgramID(programId))
programId = SDUtils.fromSageTVtoProgram(programId);

// A token is now required to perform this lookup.
Expand Down
Loading