From 4b022f26c69c9875f537e2feb4b4050bf913c5e3 Mon Sep 17 00:00:00 2001 From: jusjoken Date: Sat, 8 Nov 2025 11:06:08 -0700 Subject: [PATCH 1/2] SD image requests - fixes for error handling, add health check and alert if blocked --- CHANGELOG.md | 5 + i18n/SageTVCoreTranslations.properties | 4 + java/sage/MetaImage.java | 33 ++++++- java/sage/SageConstants.java | 2 +- java/sage/Version.java | 2 +- java/sage/epg/sd/SDErrors.java | 13 ++- java/sage/epg/sd/SDRipper.java | 33 +++++-- java/sage/epg/sd/SDSageSession.java | 1 - java/sage/epg/sd/SDSession.java | 34 ++++--- java/sage/epg/sd/SDUtils.java | 122 ++++++++++++++++++------- java/sage/msg/SystemMessage.java | 11 ++- 11 files changed, 190 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa57d567..71d5205bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/i18n/SageTVCoreTranslations.properties b/i18n/SageTVCoreTranslations.properties index 7590b47bc..9767dfd17 100755 --- a/i18n/SageTVCoreTranslations.properties +++ b/i18n/SageTVCoreTranslations.properties @@ -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. Team=Team Guest_Voice=Guest Voice Anchor=Anchor diff --git a/java/sage/MetaImage.java b/java/sage/MetaImage.java index 9fa2d0901..93c0e9824 100644 --- a/java/sage/MetaImage.java +++ b/java/sage/MetaImage.java @@ -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; @@ -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 @@ -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) @@ -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 + "'"); @@ -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"); @@ -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..."); diff --git a/java/sage/SageConstants.java b/java/sage/SageConstants.java index 064bdc9b6..69afde391 100644 --- a/java/sage/SageConstants.java +++ b/java/sage/SageConstants.java @@ -22,5 +22,5 @@ private SageConstants() // Non-instantiable } - public static final int BUILD_VERSION = 1037; + public static final int BUILD_VERSION = 1046; } diff --git a/java/sage/Version.java b/java/sage/Version.java index a280df441..dad3a9d6c 100644 --- a/java/sage/Version.java +++ b/java/sage/Version.java @@ -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; diff --git a/java/sage/epg/sd/SDErrors.java b/java/sage/epg/sd/SDErrors.java index 1b7867d3d..f0692a7c4 100644 --- a/java/sage/epg/sd/SDErrors.java +++ b/java/sage/epg/sd/SDErrors.java @@ -1,7 +1,7 @@ package sage.epg.sd; +import java.io.IOException; import sage.Sage; -import sage.SageTV; public enum SDErrors { @@ -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; @@ -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); } } diff --git a/java/sage/epg/sd/SDRipper.java b/java/sage/epg/sd/SDRipper.java index 15061a323..4886b426e 100644 --- a/java/sage/epg/sd/SDRipper.java +++ b/java/sage/epg/sd/SDRipper.java @@ -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; @@ -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; @@ -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); } @@ -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); } } @@ -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; } @@ -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 { diff --git a/java/sage/epg/sd/SDSageSession.java b/java/sage/epg/sd/SDSageSession.java index b4ef941ca..907abe096 100644 --- a/java/sage/epg/sd/SDSageSession.java +++ b/java/sage/epg/sd/SDSageSession.java @@ -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 { diff --git a/java/sage/epg/sd/SDSession.java b/java/sage/epg/sd/SDSession.java index 4fdcc5af4..66858f37b 100644 --- a/java/sage/epg/sd/SDSession.java +++ b/java/sage/epg/sd/SDSession.java @@ -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 { @@ -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. @@ -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. @@ -116,6 +116,7 @@ public abstract class SDSession URL newGetTokenCurrent; URL newGetStatus; URL newGetAvailable; + URL newGetIsBlocked; URL newGetLineups; URL newGetPrograms; URL newGetSeriesDesc; @@ -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/"); @@ -146,6 +148,7 @@ public abstract class SDSession newGetTokenCurrent = null; newGetStatus = null; newGetAvailable = null; + newGetIsBlocked = null; newGetLineups = null; newGetPrograms = null; newGetSeriesDesc = null; @@ -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; @@ -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) @@ -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); @@ -952,7 +958,9 @@ public SDProgram[] getPrograms(Collection 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); @@ -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); } @@ -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. diff --git a/java/sage/epg/sd/SDUtils.java b/java/sage/epg/sd/SDUtils.java index 4c34865d8..9db3204e2 100644 --- a/java/sage/epg/sd/SDUtils.java +++ b/java/sage/epg/sd/SDUtils.java @@ -46,13 +46,13 @@ import javax.net.ssl.HttpsURLConnection; import java.io.BufferedInputStream; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -60,6 +60,12 @@ import java.util.List; import java.util.TimeZone; import java.util.zip.GZIPInputStream; +import java.awt.image.BufferedImage; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import javax.imageio.ImageIO; +import static sage.epg.sd.SDSession.TIMEOUT; +import static sage.epg.sd.SDSession.USER_AGENT; public class SDUtils { @@ -81,6 +87,68 @@ public class SDUtils gsonBuilder.registerTypeAdapter(SDProgramSchedule.class, new SDProgramScheduleDeserializer()); GSON = gsonBuilder.create(); } + + //use the non token required "ip_isblocked" end point to determine if the user is blocked + //if an error is returned by the end point then the user is BLOCKED + public static boolean isSDBlocked() throws IOException, SDException + { + URL url = SDSession.GET_IS_BLOCKED; + HttpsURLConnection connection = (HttpsURLConnection)url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(TIMEOUT); + connection.setReadTimeout(TIMEOUT); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Accept-Encoding", "deflate,gzip"); + connection.setRequestProperty("Accept-Charset", "ISO-8859-1"); + //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)) { + connection.setRequestProperty("RouteTo", "debug"); + if (Sage.DBG) System.out.println("****debug_sd_support**** property set. Sending 'get' with url '" + url); + } + if (connection.getResponseCode() == 403){ + if (Sage.DBG) System.out.println("SDUtils:isSDBlocked - 403 response received - account is BLOCKED - raising ALERT"); + sage.msg.MsgManager.postMessage(sage.msg.SystemMessage.createSDAccountBlockedMsg()); + return true; + }else{ + //if (Sage.DBG) System.out.println("SDUtils:isSDBlocked - connection response:" + connection.getResponseCode()); + return false; + } + } + public static int handleSDJsonErrorFromHttpResponse(HttpURLConnection httpConn) throws IOException + { + InputStream inputStream = new BufferedInputStream(httpConn.getInputStream()); + InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.ISO_8859_1); + JsonElement errorElement = GSON.fromJson(reader, JsonElement.class); + if (errorElement instanceof JsonObject) + { + JsonElement codeElement = ((JsonObject) errorElement).get("code"); + int code = codeElement != null ? codeElement.getAsInt() : -1; + if (Sage.DBG) System.out.println("SDUtils.handleSDJsonErrorFromHttpResponse: Error:" + code + " : " + SDErrors.getErrorForCode(code)); + return code; + }else{ + if (Sage.DBG) System.out.println("SDUtils.handleSDJsonErrorFromHttpResponse: Unknown Error"); + return -9999; + } + } + + public static ByteArrayInputStream createBlankImageInputStream(int width, int height, String format) { + // Create a blank BufferedImage (white background) + BufferedImage blankImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + // Fill the image with white color + blankImage.createGraphics().fillRect(0, 0, width, height); + + // Convert BufferedImage to byte array using ImageIO + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + ImageIO.write(blankImage, format, baos); + } catch (IOException e) { + throw new RuntimeException("Failed to write blank image to stream", e); + } + + // Convert byte array to ByteArrayInputStream + return new ByteArrayInputStream(baos.toByteArray()); + } /** * Determine what kind of stream is returned and wrap it with an appropriate processing layer. @@ -443,29 +511,31 @@ public static String removeLeadingZeros(String channelNumber) return new String(channel, writeFrom, channel.length - writeFrom); } + //enfore sending 14 character programids public static String fromSageTVtoProgram(String program) { - if (program.length() == 12) + if (program.length() == 14) return program; + + if (program.startsWith("EP") && program.length()==12) //add zeros after EP { - char returnValue[] = new char[14]; - program.getChars(0, 2, returnValue, 0); - returnValue[2] = '0'; - returnValue[3] = '0'; - program.getChars(2, 12, returnValue, 4); - return new String(returnValue); + String programNumber = program.substring(2); + program = "EP00" + programNumber; + if (Sage.DBG) System.out.println("SDUtils.fromSageTVtoProgram: program = '" + program + "'"); + return program; } - if (program.length() == 10 && program.startsWith("EP")) + else if (program.startsWith("EP")) //add zeros to end { - program = program + "0000"; + program = String.format("%-14s", program).replace(' ', '0'); program.replace("EP", "SH"); + if (Sage.DBG) System.out.println("SDUtils.fromSageTVtoProgram: EP program converted to = '" + program + "'"); return program; - }else if(program.length() == 10){ - program = program + "0000"; + }else{ //add zerors to the start AFTER the 2 char type + String programType = program.substring(0, 2); + String programNumber = program.substring(2); + program = programType + String.format("%-12s", programNumber).replace(' ', '0'); + if (Sage.DBG) System.out.println("SDUtils.fromSageTVtoProgram: program = '" + program + "'"); return program; } - if (Sage.DBG && program.length()!=14) System.out.println("SDUtils.fromSageTVtoProgram: After conversion program is NOT 14 characters that SD requires. program = '" + program + "'"); - - return program; } public static String fromProgramToSageTV(String program) @@ -488,27 +558,9 @@ public static String fromProgramToSageTV(String program) */ public static boolean isValidProgramID(String programId) { - if (programId == null || programId.length() == 0 || - (programId.length() != 12 && programId.length() != 14) || - (!programId.startsWith("EP") && !programId.startsWith("SH") && - !programId.startsWith("MV") && !programId.startsWith("SP") && !programId.startsWith("EV"))) - return false; - return true; - } - - /** - * Check if a given program ID is valid to send to SD for metadata and is already shortened to 10 - * - * @param programId The program ID to check. - * @return true if a given program ID is valid to send to SD. - */ - public static boolean isValidShortProgramID(String programId) - { - if (programId == null || programId.length() == 0 || programId.length() != 10 || - (!programId.startsWith("EP") && !programId.startsWith("SH") && - !programId.startsWith("MV") && !programId.startsWith("SP") && !programId.startsWith("EV"))) + if(programId == null || programId.length() == 0) return false; + if (programId.startsWith("EP") || programId.startsWith("SH") || programId.startsWith("MV") || programId.startsWith("SP") || programId.startsWith("EV")) return true; return false; - return true; } /** diff --git a/java/sage/msg/SystemMessage.java b/java/sage/msg/SystemMessage.java index 269334b22..9e34b28ed 100644 --- a/java/sage/msg/SystemMessage.java +++ b/java/sage/msg/SystemMessage.java @@ -43,6 +43,7 @@ public class SystemMessage extends SageMsg public static final int LINEUP_SD_ACCOUNT_EXPIRED_MSG = 1011; public static final int LINEUP_SD_ACCOUNT_LOCKOUT_MSG = 1012; public static final int LINEUP_SD_TOO_MANY_LOGINS_MSG = 1013; + public static final int LINEUP_SD_ACCOUNT_BLOCKED_MSG = 1014; // Scheduler related public static final int MISSED_RECORDING_FROM_CONFLICT_MSG = 1050; @@ -93,6 +94,8 @@ public static String getNameForMsgType(int msgType) return sage.Sage.rez("LINEUP_SD_ACCOUNT_EXPIRED"); case LINEUP_SD_ACCOUNT_LOCKOUT_MSG: return sage.Sage.rez("LINEUP_SD_ACCOUNT_LOCKOUT"); + case LINEUP_SD_ACCOUNT_BLOCKED_MSG: + return sage.Sage.rez("LINEUP_SD_ACCOUNT_BLOCKED"); case LINEUP_SD_TOO_MANY_LOGINS_MSG: return sage.Sage.rez("LINEUP_SD_TOO_MANY_LOGINS"); case MISSED_RECORDING_FROM_CONFLICT_MSG: @@ -198,7 +201,7 @@ public static SystemMessage createVersionUpdateMsg(String version, String locati props.setProperty("Location", location); String versionMsg = "New version on github: " + version + " go to:" + location; return new SystemMessage(SOFTWARE_UPDATE_AVAILABLE_MSG, INFO_PRIORITY, - sage.Sage.rez(versionMsg, new Object[] { version, location }), props); + sage.Sage.rez("SOFTWARE_UPDATE_AVAILABLE", new Object[] { version, location }), props); } public static SystemMessage createPluginUpdateMsg(String pluginID, String pluginName, String version) @@ -343,6 +346,12 @@ public static SystemMessage createSDAccountLockOutMsg() sage.Sage.rez("LINEUP_SD_ACCOUNT_LOCKOUT_MSG"), null); } + public static SystemMessage createSDAccountBlockedMsg() + { + return new SystemMessage(LINEUP_SD_ACCOUNT_BLOCKED_MSG, ERROR_PRIORITY, + sage.Sage.rez("LINEUP_SD_ACCOUNT_BLOCKED_MSG"), null); + } + public static SystemMessage createSDTooManyLoginsMsg() { return new SystemMessage(LINEUP_SD_TOO_MANY_LOGINS_MSG, ERROR_PRIORITY, From fbab3ff13fdc918f20893d65b58e9e332f6a3c1c Mon Sep 17 00:00:00 2001 From: jusjoken Date: Thu, 27 Nov 2025 13:52:32 -0700 Subject: [PATCH 2/2] SD Fixes --- i18n/SageTVCoreTranslations.properties | 5 ++--- java/sage/SageConstants.java | 2 +- java/sage/epg/sd/SDRipper.java | 15 +++++---------- java/sage/msg/SystemMessage.java | 10 +++++----- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/i18n/SageTVCoreTranslations.properties b/i18n/SageTVCoreTranslations.properties index 9767dfd17..28a569a27 100755 --- a/i18n/SageTVCoreTranslations.properties +++ b/i18n/SageTVCoreTranslations.properties @@ -449,7 +449,6 @@ DISKSPACE_INADEQUATE_ERR_MSG=Your system is out of diskspace for recording TV. S SYSTEM_LOCKUP_DETECTION=System Lockup Detected OUT_OF_MEMORY=Out of Memory Detected OUT_OF_MEMORY_MSG=SageTV has detected that it has run out of memory. You may need to increase your JVM heap size or there could be a plugin/customization that is causing an issue. -SOFTWARE_UPDATE_AVAILABLE=Software Updates Available PLUGIN_UPDATE_AVAILABLE_MSG=There is an update available of the plugin "{0}" to version {1}. STORAGE_MONITOR=Storage Monitor STORAGE_MONITOR_MSG=A degraded state has been detected for the RAID device "{0}" due to a hard disk failure. Please use the server Administrator console from your web browser to diagnose and resolve the problem. @@ -485,8 +484,8 @@ 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. +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 Guest_Voice=Guest Voice Anchor=Anchor diff --git a/java/sage/SageConstants.java b/java/sage/SageConstants.java index 69afde391..064bdc9b6 100644 --- a/java/sage/SageConstants.java +++ b/java/sage/SageConstants.java @@ -22,5 +22,5 @@ private SageConstants() // Non-instantiable } - public static final int BUILD_VERSION = 1046; + public static final int BUILD_VERSION = 1037; } diff --git a/java/sage/epg/sd/SDRipper.java b/java/sage/epg/sd/SDRipper.java index 4886b426e..04c0b4548 100644 --- a/java/sage/epg/sd/SDRipper.java +++ b/java/sage/epg/sd/SDRipper.java @@ -91,6 +91,7 @@ import static sage.epg.sd.SDErrors.SAGETV_UNKNOWN; import static sage.epg.sd.SDErrors.SERVICE_OFFLINE; import static sage.epg.sd.SDErrors.TOO_MANY_LOGINS; +import static sage.epg.sd.SDSession.debugEnabled; public class SDRipper extends EPGDataSource { @@ -227,7 +228,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; @@ -242,7 +243,6 @@ private static SDSession openNewSession() throws IOException, SDException propUsername = serverProp.toString(); serverProp = SageTV.api("GetServerProperty", new Object[] { PROP_PASSWORD, null }); propPassword = serverProp.toString(); - //if (Sage.DBG) System.out.println("***EPG*** getting from CLIENT user/pass: username:" + username + " password:" + password); } catch (Throwable t) { @@ -256,10 +256,7 @@ private static SDSession openNewSession() throws IOException, SDException propPassword = Sage.get(PROP_PASSWORD, null); } - //if (Sage.DBG) System.out.println("***EPG*** checking for user/pass: username:" + username + " password:" + password); - //get user/pass from old sdauth file - //if (Sage.DBG) System.out.println("***EPG*** reading user/pass from old file"); File authFile = new File(AUTH_FILE); if (!authFile.exists() || authFile.length() == 0) throw new SDException(SDErrors.SAGETV_NO_PASSWORD); @@ -307,9 +304,8 @@ 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("SDRipper:openNewSession: Authenticated using prop based user/pass PASSED: username:" + propUsername + " password:" + propPassword); } catch (Exception e) { - if (Sage.DBG) System.out.println("SDRipper:openNewSession: 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"); } } @@ -321,9 +317,8 @@ 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("SDRipper:openNewSession: Authenticated using file based user/pass PASSED: username:" + fileUsername + " password:" + filePassword); } catch (Exception e) { - if (Sage.DBG) System.out.println("SDRipper:openNewSession: 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"); } } @@ -336,7 +331,7 @@ private static SDSession openNewSession() throws IOException, SDException // 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("SDRipper:openNewSession: Successfully got token: " + returnValue.token); + if (Sage.DBG&& debugEnabled()) System.out.println("SDRipper:openNewSession: Successfully got token: " + returnValue.token); return returnValue; } diff --git a/java/sage/msg/SystemMessage.java b/java/sage/msg/SystemMessage.java index 9e34b28ed..26408a4b7 100644 --- a/java/sage/msg/SystemMessage.java +++ b/java/sage/msg/SystemMessage.java @@ -61,10 +61,11 @@ public class SystemMessage extends SageMsg // General public static final int SYSTEM_LOCKUP_DETECTION_MSG = 1200; public static final int OUT_OF_MEMORY_MSG = 1201; - public static final int SOFTWARE_UPDATE_AVAILABLE_MSG = 1202; + public static final int PLUGIN_UPDATE_AVAILABLE_MSG = 1202; public static final int STORAGE_MONITOR_MSG = 1203; public static final int GENERAL_MSG = 1204; public static final int PLUGIN_INSTALL_MISSING_FILE_MSG = 1205; + public static final int SOFTWARE_UPDATE_AVAILABLE_MSG = 1206; public static String getNameForMsgType(int msgType) { @@ -117,7 +118,7 @@ public static String getNameForMsgType(int msgType) case RECORDING_BITRATE_TOO_LOW_ERROR_MSG: return sage.Sage.rez("RECORDING_BITRATE_TOO_LOW_ERROR"); case SOFTWARE_UPDATE_AVAILABLE_MSG: - return sage.Sage.rez("SOFTWARE_UPDATE_AVAILABLE"); + return sage.Sage.rez("SOFTWARE_UPDATE_AVAILABLE_MSG"); case STORAGE_MONITOR_MSG: return sage.Sage.rez("STORAGE_MONITOR"); case CAPTURE_DEVICE_DATASCAN_ERROR_MSG: @@ -199,9 +200,8 @@ public static SystemMessage createVersionUpdateMsg(String version, String locati java.util.Properties props = new java.util.Properties(); props.setProperty("Version", version); props.setProperty("Location", location); - String versionMsg = "New version on github: " + version + " go to:" + location; return new SystemMessage(SOFTWARE_UPDATE_AVAILABLE_MSG, INFO_PRIORITY, - sage.Sage.rez("SOFTWARE_UPDATE_AVAILABLE", new Object[] { version, location }), props); + sage.Sage.rez("SOFTWARE_UPDATE_AVAILABLE_MSG", new Object[] { version, location }), props); } public static SystemMessage createPluginUpdateMsg(String pluginID, String pluginName, String version) @@ -211,7 +211,7 @@ public static SystemMessage createPluginUpdateMsg(String pluginID, String plugin props.setProperty("PluginName", pluginName); props.setProperty("Version", version); props.setProperty("IsPluginUpdate", "true"); - return new SystemMessage(SOFTWARE_UPDATE_AVAILABLE_MSG, INFO_PRIORITY, + return new SystemMessage(PLUGIN_UPDATE_AVAILABLE_MSG, INFO_PRIORITY, sage.Sage.rez("PLUGIN_UPDATE_AVAILABLE_MSG", new Object[] { pluginName, version }), props); }