diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c219b0d1b..24a7d0d6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ on: jobs: build: -# runs-on: ubuntu-latest - runs-on: ubuntu-18.04 +# runs-on: ubuntu-latest 2025-03-04 changed to 22.04 as 20.04 is no longer supported as of 4/1/2025 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ef55096..3ad6ea990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,23 @@ # Change Log ## Next + +## Version 9.2.9 (2025-03-11) * Updated gradle script so that project could build in Netbeans * Updated the FFMPEGTranscoder to fallback to frame count instead of time to calculate progress * Allow IR blasters that support it to xmit non-numeric Tune strings (eg 42-1). +* SD EPG changes to correct image retrieval without token and other API corrections/updates +* SD EPG added debug_sd_support property to enable extra debug info when contacting SD support +* SD EPG added sdepg_core/bypassCelebrityImages to allow users in the future to bypass reteiving Celebrity images from SD if causing issues +* SD EPG added sdepg_core/bypassProgramImages to allow users in the future to bypass retrieving Program images from SD if causing issues +* SD EPG added sdepg_core/bypassEPGUpdates to allow users in the future to bypass retrieving EPG from SD if causing issues +* SD EPG added wizard/scheduled_maintenance and wizard/scheduled_maintenance_offset to allow users to set the hour that the daily maintenance will run +* SD EPG added code to support SD now passing back the current token along with its expiration +* SD EPG fix for send SD empty program lists as well as malformed endpoint for metadata/program +* SD EPG fix enpoint call for metadata/programs to use 14 character programID rather than shortended to 10 +* Added seeker/duration_for_watchdog property to handle long running watchdog process for larger libraries (defaults to 60000) +* Windows installer build notes updated for location of missing files needed for the build +* Added ability to notify user if a new version is available on Github (defaults to enabled but can be disabled) ## Version 9.2.8 (2022-01-05) * Update to build process to support Linux build on Ubuntu 18.04 and JDK 11 diff --git a/build.gradle b/build.gradle index ea67240e9..1d8e2aeff 100755 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ ext { version = fullVersion // Just printing out the SAGETV build version for informational purposes -// System.out.println("SAGETV VERSION ${versionArch}"); +//System.out.println("SAGETV VERSION ${versionArch}"); // set gradle's buildDir to something other than the default 'build', or // we'll end up deleting EVERYTHING in 'build' when we ./gradlew clean @@ -322,19 +322,22 @@ def getBuildNumber() { if (OperatingSystem.current().isLinux()) { // use git to find the build version - ant.exec( - command: 'git rev-list HEAD --count', - os: 'Linux', - failonerror: true, - outputproperty: 'sagebuildnum') - - buildVer = ant.sagebuildnum + // 03-06-2025 jusjoken: Also we should note that you will get error code 128 if you have git + // installed, but you did not pull this project using git because git will not know where + // to get the commit count from. + new ByteArrayOutputStream().withStream { outputStream -> + exec { + executable 'git' + args 'rev-list', 'HEAD', '--count' + standardOutput = outputStream + } + buildVer = outputStream.toString().trim() + } } else if (OperatingSystem.current().isWindows()) { //ensure full path to git\bin is in the Path Environment variable on windows // 04-24-2017 JS: I changed this to remove the dependency on sh. Also we should note that // you will get error code 128 if you have git installed, but you did not pull this project - // using git because git will not know where to get the commit count from. This is likely - // true for Linux too. + // using git because git will not know where to get the commit count from. new ByteArrayOutputStream().withStream { outputStream -> exec { executable 'git' @@ -381,7 +384,7 @@ task(updateBuildNumber) { // save buildnumber to file // we'll be referencing it later // Eventually we'll do this, but for now, fetch it every time - // new File('.buildnumber').write(buildVersion) + new File('.buildnumber').write(buildVersion) } } diff --git a/build/buildmplayer.sh b/build/buildmplayer.sh index 8f49bc428..0df7b8d7b 100755 --- a/build/buildmplayer.sh +++ b/build/buildmplayer.sh @@ -38,7 +38,7 @@ if [ "$MPLAYER_NEW" = "1" ] ; then else # use legacy mplayer build cd ../third_party/mplayer/ - LDFLAGS="-no-pie" ./configure --host-cc=gcc --disable-gcc-check --enable-runtime-cpudetection --disable-mencoder --disable-gl --enable-directx --enable-largefiles --disable-langinfo --disable-tv --disable-dvdread --disable-dvdread-internal --disable-menu --disable-libdvdcss-internal --enable-pthreads --disable-debug --disable-freetype --disable-fontconfig --enable-stv --enable-stream-sagetv --disable-ivtv --disable-x264 --extra-libs=-lpthread --disable-png || { echo "Build failed, exiting."; exit 1; } + LDFLAGS="-no-pie" ./configure --host-cc=gcc --disable-gcc-check --enable-runtime-cpudetection --disable-mencoder --disable-gl --enable-directx --enable-largefiles --disable-langinfo --disable-tv --disable-dvdread --disable-dvdread-internal --disable-menu --disable-libdvdcss-internal --enable-pthreads --disable-debug --disable-freetype --disable-fontconfig --enable-stv --enable-stream-sagetv --disable-ivtv --disable-x264 --extra-libs="-lpthread -pthread" --disable-png || { echo "Build failed, exiting."; exit 1; } make -j32 || { echo "Build failed, exiting."; exit 1; } echo "Built OLD mplayer" cp -v mplayer ../../build/elf diff --git a/installer/Readme.md b/installer/Readme.md index 4cee956ed..d556dc1c8 100644 --- a/installer/Readme.md +++ b/installer/Readme.md @@ -2,20 +2,19 @@ This folder includes the files needed to perform the build of the installers for windows - Note: this is a work in progress and I hope to add more scripting and automation over time - ## Change the versions for an installer release * all version information is retreived from java/sage/Version.java ## Use powershell script to build each part of the product ### From Admin Powershell -* cd C:\Projects\Installer\sagetv\installer\wix\SageTVSetup +* cd C:\Projects\sagetv\installer\wix\SageTVSetup #or whatever the path is to "" * .\installerbuild.ps1 -A ### Notes * run .\installerbuild.ps1 without any parameters to see the list of available options -* to upload to bintray add the -u parameter such as ".\installerbuild.ps1 -A -u" +* if building the sage.jar, miniclient.jar elsewhere then use .\installerbuild.ps1 -A -xAll -nj ## Notes: * building of imageloader.dll and swscale.dll still need to be added to this powershell script -* I will expand on this document as time permits as you will need WIX installed as well as VS2015 and a number of environment variables to make this work. +* after cloning the sagetv github repo you will be missing files needed for the installer build. They can be downloaded here... + https://github.com/OpenSageTV/sagetv-windows/releases/tag/v1.1 diff --git a/java/sage/EPG.java b/java/sage/EPG.java index df7dc0d85..a7b81aa73 100644 --- a/java/sage/EPG.java +++ b/java/sage/EPG.java @@ -37,6 +37,8 @@ public final class EPG implements Runnable private static final String DOWNLOAD_WHILE_INACTIVE = "download_while_inactive"; private static final String DOWNLOAD_FREQUENCY = "download_frequency"; private static final String DOWNLOAD_OFFSET = "download_offset"; + private static final String SCHEDULED_EPG_UPDATE = "scheduled_epg_update"; + private static final String SCHEDULED_EPG_UPDATE_OFFSET = "scheduled_epg_update_offset"; private static final String CHANNEL_LINEUPS = "channel_lineups"; private static final String PHYSICAL_CHANNEL_LINEUPS = "physical_channel_lineups"; private static final String LINEUP_OVERRIDES = "lineup_overrides"; @@ -597,6 +599,7 @@ public java.io.File getLogoDir() public void run() { + // Check if the DB file was deleted if (wiz.getChannels().length < 4) { @@ -624,19 +627,34 @@ public void run() // JS 8/21/2016: This is no longer used. //boolean[] didAdd = new boolean[1]; boolean updateFinished = true; + //JUSJOKEN: 2025-02-19 add ability to run FULL Maintenance at a specific time each day while (alive) { + try{ - if (Sage.time() - wiz.getLastMaintenance() > MAINTENANCE_FREQ) - reqMaintenanceType = MaintenanceType.FULL; + 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(); + + 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 (reqMaintenanceType != MaintenanceType.NONE) { Carny.getInstance().kickHard(); SchedulerSelector.getInstance().kick(false); } - + if (reqMaintenanceType != MaintenanceType.NONE && (!downloadWhileInactive || inactive)) { @@ -651,161 +669,190 @@ public void run() long minWait = MAINTENANCE_FREQ - (Sage.time() - wiz.getLastMaintenance()); if (minWait <= 0) minWait = 1; + synchronized (sources) { for (int i = 0; (i < sources.size()) && alive; i++) { - long currWait = sources.elementAt(i).getTimeTillUpdate(); - minWait = Math.min(minWait, currWait); - if (Sage.DBG) System.out.println(sources.elementAt(i) + " needs an update in " + Sage.durFormat(currWait)); - } - } - if (Sage.DBG) System.out.println("EPG needs an update in " + (minWait/60000) + " minutes"); - if (minWait > 0) - { - if (!updateFinished && (downloadFrequency != 0)) - { - nextDownloadTime += downloadFrequency; - if (Sage.DBG) System.out.println("EPG nextDownloadTime=" + Sage.df(nextDownloadTime)); - } - updateFinished = true; - Sage.disconnectInternet(); - if (Sage.DBG) System.out.println("EPG's works is done. Waiting..."); - synchronized (sources) - { - if (alive) - try{sources.wait(minWait + 15000L);} catch(InterruptedException e){} - } - } - else if (downloadWhileInactive && !inactive) - { - if (Sage.DBG) System.out.println("EPG is waiting because of system activity..."); - Sage.disconnectInternet(); - synchronized (sources) - { - if (alive) - try{sources.wait(minWait);} catch(InterruptedException e){} - } - } - else if (nextDownloadTime > Sage.time()) - { - long nextWait = Math.min(minWait, nextDownloadTime - Sage.time()); - if (nextWait > 0) - { - if (Sage.DBG) System.out.println("EPG is waiting for next scheduled download time in " + nextWait/60000L + " minutes"); - synchronized (sources) - { - if (alive) - try{sources.wait(nextWait + 15000L);} catch(InterruptedException e){} + if(Sage.getBoolean("wizard/" + SCHEDULED_EPG_UPDATE, false)){ + //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 (Sage.DBG) System.out.println(sources.elementAt(i) + " needs a scheduled update in " + Sage.durFormat(minWait)); + }else{ + long currWait = sources.elementAt(i).getTimeTillUpdate(); + minWait = Math.min(minWait, currWait); } } } - else - { - updateFinished = false; - boolean updatesFailed = false; - // Connect when we become active - if (!autodial || Sage.connectToInternet()) - { - java.util.List highPriorityDownloads = new java.util.ArrayList(); - synchronized (sources) + if(Sage.getBoolean("sdepg_core/bypassEPGUpdates", false)){ + if (Sage.DBG) System.out.println("SD EPG Updates are disabled by sdepg_core/bypassEPGUpdates setting. Check again in " + (minWait/60000) + " minutes"); + if (minWait > 0) { - for (int i = 0; i < sources.size(); i++) + synchronized (sources) { - if (!sources.get(i).isChanDownloadComplete()) - highPriorityDownloads.add(sources.get(i)); + if (alive) + try{sources.wait(minWait);} catch(InterruptedException e){} } } - for (int i = 0; (i < highPriorityDownloads.size()) && alive; i++) + }else{ + if (Sage.DBG) System.out.println("EPG needs an update in " + (minWait/60000) + " minutes"); + if (minWait > 0) { - currDS = highPriorityDownloads.get(i); - synchronized (sources) - { - if (!sources.contains(currDS)) - continue; - } - currDS.clearAbort(); - if (Sage.DBG) System.out.println("EPG PRIORITY EXPANSION attempting to expand " + currDS.getName()); - boolean updateSucceeded=false; - try{ - epgState=EpgState.UPDATING; - updateSucceeded=currDS.expand(); - } finally { - epgState=EpgState.IDLE; - } - if (! updateSucceeded) + if (!updateFinished && (downloadFrequency != 0)) { - updatesFailed = handleEpgDsUpdateFailed(updatesFailed); + nextDownloadTime += downloadFrequency; + if (Sage.DBG) System.out.println("EPG nextDownloadTime=" + Sage.df(nextDownloadTime)); } - else + updateFinished = true; + Sage.disconnectInternet(); + if (Sage.DBG) System.out.println("EPG's work is done. Waiting..."); + synchronized (sources) { - // set the next maintenance type based on how mucgh - // this EPG update thinks should be done... - reqMaintenanceType = checkEpgDsMaintenanceType(reqMaintenanceType); - epgErrorSleepTime = 60000; + if (alive) + try{sources.wait(minWait + 15000L);} catch(InterruptedException e){} } + } + else if (downloadWhileInactive && !inactive) + { + if (Sage.DBG) System.out.println("EPG is waiting because of system activity..."); + Sage.disconnectInternet(); synchronized (sources) { - currDS.clearAbort(); - currDS = null; + if (alive) + try{sources.wait(minWait);} catch(InterruptedException e){} } } - for (int i = 0; (i < sources.size()) && alive; i++) + else if (nextDownloadTime > Sage.time()) { - synchronized (sources) + long nextWait = Math.min(minWait, nextDownloadTime - Sage.time()); + if (nextWait > 0) { - if ((i < sources.size()) && alive) + if (Sage.DBG) System.out.println("EPG is waiting for next scheduled download time in " + nextWait/60000L + " minutes"); + synchronized (sources) { - currDS = sources.elementAt(i); + if (alive) + try{sources.wait(nextWait + 15000L);} catch(InterruptedException e){} } - else - { - break; - } - } - if (highPriorityDownloads.contains(currDS)) - continue; - currDS.clearAbort(); - if (Sage.DBG) System.out.println("EPG attempting to expand " + currDS.getName()); - boolean updateSucceeded=false; - try{ - epgState=EpgState.UPDATING; - updateSucceeded=currDS.expand(); - } finally { - epgState=EpgState.IDLE; - } - if (! updateSucceeded) - { - updatesFailed = handleEpgDsUpdateFailed(updatesFailed); - } - else - { - reqMaintenanceType = checkEpgDsMaintenanceType(reqMaintenanceType); - epgErrorSleepTime = 60000; - } - synchronized (sources) - { - currDS.clearAbort(); - currDS = null; } } - - // NOTE: If the user had 2 sources, and one was failing on the update, we don't want to - // continually save the DB each round until the update is complete. That'd be bad. - if (updatesFailed) - reqMaintenanceType = MaintenanceType.NONE; else - sage.plugin.PluginEventManager.postEvent(sage.plugin.PluginEventManager.EPG_UPDATE_COMPLETED, (Object[]) null); - } - else - { - if (Sage.DBG) System.out.println("ERROR Could not autodial...waiting..."); - synchronized (sources) { - if (alive) - try{sources.wait(ERROR_SLEEP);} catch(InterruptedException e){} + updateFinished = false; + boolean updatesFailed = false; + // Connect when we become active + if (!autodial || Sage.connectToInternet()) + { + + //03-02-2025 jusjoken: added a version check here. As this EPG process runs basically daily I added the + // version check here. It may fit best elsewhere but I will leave it here for now to ensure future updates are noticed + //check if an updated version is available on github + wiz.checkForUpdate(); + + java.util.List highPriorityDownloads = new java.util.ArrayList(); + synchronized (sources) + { + for (int i = 0; i < sources.size(); i++) + { + if (!sources.get(i).isChanDownloadComplete()) + highPriorityDownloads.add(sources.get(i)); + } + } + for (int i = 0; (i < highPriorityDownloads.size()) && alive; i++) + { + currDS = highPriorityDownloads.get(i); + synchronized (sources) + { + if (!sources.contains(currDS)) + continue; + } + currDS.clearAbort(); + if (Sage.DBG) System.out.println("EPG PRIORITY EXPANSION attempting to expand " + currDS.getName()); + boolean updateSucceeded=false; + try{ + epgState=EpgState.UPDATING; + updateSucceeded=currDS.expand(); + } finally { + epgState=EpgState.IDLE; + } + if (! updateSucceeded) + { + updatesFailed = handleEpgDsUpdateFailed(updatesFailed); + } + else + { + // set the next maintenance type based on how mucgh + // this EPG update thinks should be done... + reqMaintenanceType = checkEpgDsMaintenanceType(reqMaintenanceType); + epgErrorSleepTime = 60000; + } + synchronized (sources) + { + currDS.clearAbort(); + currDS = null; + } + } + for (int i = 0; (i < sources.size()) && alive; i++) + { + synchronized (sources) + { + if ((i < sources.size()) && alive) + { + currDS = sources.elementAt(i); + } + else + { + break; + } + } + if (highPriorityDownloads.contains(currDS)) + continue; + currDS.clearAbort(); + if (Sage.DBG) System.out.println("EPG attempting to expand " + currDS.getName()); + boolean updateSucceeded=false; + try{ + epgState=EpgState.UPDATING; + updateSucceeded=currDS.expand(); + } finally { + epgState=EpgState.IDLE; + } + if (! updateSucceeded) + { + updatesFailed = handleEpgDsUpdateFailed(updatesFailed); + } + else + { + reqMaintenanceType = checkEpgDsMaintenanceType(reqMaintenanceType); + epgErrorSleepTime = 60000; + } + synchronized (sources) + { + currDS.clearAbort(); + currDS = null; + } + } + + // NOTE: If the user had 2 sources, and one was failing on the update, we don't want to + // continually save the DB each round until the update is complete. That'd be bad. + if (updatesFailed) + reqMaintenanceType = MaintenanceType.NONE; + else + sage.plugin.PluginEventManager.postEvent(sage.plugin.PluginEventManager.EPG_UPDATE_COMPLETED, (Object[]) null); + } + else + { + if (Sage.DBG) System.out.println("ERROR Could not autodial...waiting..."); + synchronized (sources) + { + if (alive) + try{sources.wait(ERROR_SLEEP);} catch(InterruptedException e){} + } + } + } - } + + } // Don't try to save the prefs if we're going down to avoid corrupting it since the main shutdown code will handle this @@ -819,6 +866,23 @@ else if (nextDownloadTime > Sage.time()) } } } + + private long getNextScheduledEPGUpdateTime(){ + java.util.Calendar cal = new java.util.GregorianCalendar(); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + cal.set(java.util.Calendar.HOUR_OF_DAY, Sage.getInt("wizard/" + SCHEDULED_EPG_UPDATE_OFFSET, 0)); + 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){ + cal.add(java.util.Calendar.DATE,1); + calcNextScheduledEPGUpdateTime = cal.getTimeInMillis(); + } + return calcNextScheduledEPGUpdateTime; + } + /** * Handle logging errors and epg backoff timer * @@ -1678,6 +1742,7 @@ public static Object setProperty(String dataSource, String property, String para private int downloadOffset; private long nextDownloadTime; + private long nextScheduledEPGUpdateTime; private boolean inactive; private boolean autodial; diff --git a/java/sage/MetaImage.java b/java/sage/MetaImage.java index 4c58f8f82..336dd6acd 100644 --- a/java/sage/MetaImage.java +++ b/java/sage/MetaImage.java @@ -51,6 +51,9 @@ import java.util.Set; import java.util.Vector; import java.util.WeakHashMap; +import sage.epg.sd.SDRipper; +import sage.epg.sd.SDSageSession; +import sage.epg.sd.SDSession; /* * NOTE: DON'T DO THUMBNAIL GENERATION IN HERE. WE WANT TO STORE THEM IN SEPARATE FILES @@ -3207,8 +3210,9 @@ private boolean loadCacheFile() } } InputStream is = null; - if (src instanceof String) - is = getClass().getClassLoader().getResourceAsStream((String) src); + if (src instanceof String){ + is = getClass().getClassLoader().getResourceAsStream((String) src); + } else { HttpURLConnection.setFollowRedirects(true); @@ -3222,6 +3226,33 @@ private boolean loadCacheFile() myURLConn = myURL.openConnection(); myURLConn.setConnectTimeout(30000); myURLConn.setReadTimeout(30000); + if(src.toString().startsWith(SDSession.URL_VERSIONED)){ + //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 + "'"); + + //skip the image loading if the bypass properties are set + if(Sage.getBoolean("sdepg_core/bypassCelebrityImages", false) && Sage.getBoolean("sdepg_core/bypassProgramImages", false)){ + if (Sage.DBG) System.out.println("MetaImage.loadCacheFile: skipping image load as both bypass properties are set. src = '" + src + "'"); + break; + } + + String sdToken = null; + sdToken = SDRipper.ensureSession().getToken(); + if(sdToken==null){ + //no token so skip SD Image + 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 + "'"); + myURLConn.addRequestProperty("token", sdToken); + //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"); + if (Sage.DBG) System.out.println("****debug_sd_support**** property set. MetaImage.loadCacheFile loading sd image"); + } + } + } + is = myURLConn.getInputStream(); if (myURLConn instanceof HttpURLConnection) { diff --git a/java/sage/SageConstants.java b/java/sage/SageConstants.java index e2172004e..be9a36a3e 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 = 441; + public static final int BUILD_VERSION = 1056; } diff --git a/java/sage/Seeker.java b/java/sage/Seeker.java index 6709711d3..660c36efe 100644 --- a/java/sage/Seeker.java +++ b/java/sage/Seeker.java @@ -3639,6 +3639,7 @@ public void run() long altExpireTime = Sage.LINUX_OS ? Sage.getLong("window_size", 0) : 0; + verifyFiles(true, false); Thread watchdog = new Thread("SeekerWatchdog") @@ -3646,16 +3647,17 @@ public void run() public void run() { long lastDumpTime = 0; + long watchdogDur = Sage.getLong("seeker/duration_for_watchdog", 60000); while (alive) { long testTime = lastSeekerWakeupTime; - if (testTime != 0 && testTime != lastDumpTime && Sage.eventTime() - testTime > 60000) + if (testTime != 0 && testTime != lastDumpTime && Sage.eventTime() - testTime > watchdogDur) { - if (Sage.DBG) System.out.println("ERROR - Seeker has been hung for more than 60 seconds...system appears deadlocked...dumping thread states"); + if (Sage.DBG) System.out.println("ERROR - Seeker has been hung for more than " + watchdogDur + " milliseconds...system appears deadlocked...dumping thread states"); AWTThreadWatcher.dumpThreadStates(); lastDumpTime = testTime; } - try{Thread.sleep(60000);}catch(Exception e){} + try{Thread.sleep(watchdogDur);}catch(Exception e){} } } }; diff --git a/java/sage/Version.java b/java/sage/Version.java index efc207201..749022443 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 = 8; + public static final byte MICRO_VERSION = 9; public static final String VERSION = MAJOR_VERSION + "." + MINOR_VERSION + "." + MICRO_VERSION + "." + SageConstants.BUILD_VERSION; diff --git a/java/sage/Wizard.java b/java/sage/Wizard.java index edc3e4fda..270f53e10 100644 --- a/java/sage/Wizard.java +++ b/java/sage/Wizard.java @@ -60,7 +60,10 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.Socket; +import java.net.URL; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.SimpleDateFormat; @@ -7200,6 +7203,90 @@ public boolean ok(Airing x) (getAiringForID(x.id) != null); } + /* + * Github version checking + */ + public boolean checkForUpdate(){ + String ghOwner = Sage.get(prefsRoot + "githubReleaseCheckOwner", "OpenSageTV"); + String ghRepo = Sage.get(prefsRoot + "githubReleaseCheckRepo", "sagetv-linux"); + boolean ghReleaseCheckDisabled = Sage.getBoolean(prefsRoot + "githubReleaseCheckDisabled", false); + String currentVersion = Version.VERSION; + String latestVersion = ""; + URL url; + if(ghReleaseCheckDisabled || Sage.client){ + if (Sage.DBG) System.out.println("checkForUpdate: release check is disabled in properties or this is a client - see " + prefsRoot + "/githubReleaseCheckDisabled"); + return false; + } + + if (Sage.DBG) System.out.println("checkForUpdate: currentVersion:" + currentVersion); + + try { + url = new URL("https://github.com/" + ghOwner + "/" + ghRepo + "/releases/latest"); + } catch (MalformedURLException e) { + if (Sage.DBG) System.out.println("checkForUpdate ERROR: url creation failed. Check will not continue"); + return false; + } + if (Sage.DBG) System.out.println("checkForUpdate: url:" + url); + + try { + // Connect to GitHub website + HttpURLConnection con; + con = (HttpURLConnection) url.openConnection(); + con.setInstanceFollowRedirects(false); + + // Check if the response is a redirect + String newUrl = con.getHeaderField("Location"); + + if (newUrl == null) { + if (Sage.DBG) System.out.println("checkForUpdate ERROR: url redirection was not returned. Check will not continue"); + //throw new IOException("Did not get a redirect"); + return false; + } + + // Get the latest version tag from the redirect url + String[] split = newUrl.split("/"); + latestVersion = removePrefix(split[split.length - 1]); + + } catch (IOException ex) { + if (Sage.DBG) System.out.println("checkForUpdate ERROR: exception trying to get latest version from github"); + return false; + } + + if(compareVersion(latestVersion, currentVersion)==1){ + if (Sage.DBG) System.out.println("checkForUpdate: currentVersion:" + currentVersion + " latestVersion:" + latestVersion + " New version is available"); + //add notification of new version + sage.msg.MsgManager.postMessage(sage.msg.SystemMessage.createVersionUpdateMsg(latestVersion, url.toString())); + return true; + } + if (Sage.DBG) System.out.println("checkForUpdate: currentVersion:" + currentVersion + " latestVersion:" + latestVersion + " No new version is available"); + return false; + } + + private String removePrefix(String version) { + return version.replaceFirst("^v", ""); + } + + private int compareVersion(String version1, String version2) { + String[] arr1 = version1.split("\\."); + String[] arr2 = version2.split("\\."); + + int i = 0; + while (i < arr1.length || i < arr2.length) { + int num1 = i < arr1.length ? Integer.parseInt(arr1[i]) : 0; + int num2 = i < arr2.length ? Integer.parseInt(arr2[i]) : 0; + + if (num1 > num2) { + return 1; + } else if (num1 < num2) { + return -1; + } + + i++; + } + + return 0; +} + /* * File Operations */ diff --git a/java/sage/epg/sd/SDErrors.java b/java/sage/epg/sd/SDErrors.java index df25519a8..1b7867d3d 100644 --- a/java/sage/epg/sd/SDErrors.java +++ b/java/sage/epg/sd/SDErrors.java @@ -1,5 +1,8 @@ package sage.epg.sd; +import sage.Sage; +import sage.SageTV; + public enum SDErrors { OK(0 /*, "OK"*/), @@ -15,6 +18,7 @@ public enum SDErrors INVALID_PARAMETER_COUNTRY(2050 /*, "The COUNTRY parameter must be ISO-3166-1 alpha 3. See http://en.wikipedia.org/wiki/ISO_3166-1_alpha-3."*/), INVALID_PARAMETER_POSTALCODE(2051 /*, "The POSTALCODE parameter must be valid for the country you are searching. Post message to http://forums.schedulesdirect.org/viewforum.php?f=6 if you are having issues."*/), INVALID_PARAMETER_FETCHTYPE(2052 /*, "You provided a fetch type I don't know how to handle."*/), + INVALID_PARAMETER_DEBUG(2055 /*, "Unexpected debug connection from client."*/), DUPLICATE_LINEUP(2100 /*, "Lineup already in account."*/), LINEUP_NOT_FOUND(2101 /*, "Lineup not in account. Add lineup to account before requesting mapping."*/), UNKNOWN_LINEUP(2102 /*, "Invalid lineup requested. Check your COUNTRY / POSTALCODE combination for validity."*/), @@ -32,10 +36,13 @@ public enum SDErrors ACCOUNT_LOCKOUT(4004 /*, "Too many login failures. Locked for 15 minutes."*/), ACCOUNT_DISABLED(4005 /*, "Account has been disabled. Please contact Schedules Direct support: admin@schedulesdirect.org for more information."*/), TOKEN_EXPIRED(4006 /*, "Token has expired. Request new token."*/), + TOO_MANY_LOGINS(4009 /*, "Exceeded maximum number of logins in 24 hours."*/), MAX_LINEUP_CHANGES_REACHED(4100 /*, "Exceeded maximum number of lineup changes for today."*/), MAX_LINEUPS(4101 /*, "Exceeded number of lineups for this account."*/), NO_LINEUPS(4102 /*, "No lineups have been added to this account."*/), IMAGE_NOT_FOUND(5000 /*, "Could not find requested image. Post message to http://forums.schedulesdirect.org/viewforum.php?f=6 if you are having issues."*/), + MAX_IMAGE_DOWNLOADS(5002 /*, "Maximum image downloads reached. Counter resets every 24h"*/), + MAX_IMAGE_DOWNLOADS_TRIAL(5003 /*, "Maximum image downloads for trial user reached. Counter resets every 24h"*/), INVALID_PROGRAMID(6000 /*, "Could not find requested programID. Permanent failure."*/), PROGRAMID_QUEUED(6001 /*, "ProgramID should exist at the server, but doesn't. The server will regenerate the JSON for the program, so your application should retry."*/), FUTURE_PROGRAM(6002 /*, "The programID you requested has not occurred yet, so isComplete status is unknown."*/), @@ -108,8 +115,9 @@ public static void throwErrorForCode(int code) throws SDException { for (SDErrors error : SDErrors.values()) { - if (code == error.CODE) + if (code == error.CODE){ throw new SDException(error); + } } } diff --git a/java/sage/epg/sd/SDRecommended.java b/java/sage/epg/sd/SDRecommended.java index ea12e8541..d067b5dea 100644 --- a/java/sage/epg/sd/SDRecommended.java +++ b/java/sage/epg/sd/SDRecommended.java @@ -163,11 +163,8 @@ public static List getUsable(List editorials) String programId = recommendation.getProgramId(); // 05/05/2016 JS: I made this a check for an expected length and specifically prepending with // a known prefix because we were throwing some garbage at SD that we shouldn't. - if (programId == null || programId.length() == 0 || - (programId.length() != 12 && programId.length() != 14) || - (!programId.startsWith("EP") && !programId.startsWith("SH") && - !programId.startsWith("MV") && !programId.startsWith("SP"))) - continue; + //03.01.2025 jusjoken: move to common util function + if(SDUtils.isValidProgramID(programId)) continue; // Don't recommend movies that we have already seen or don't exist in the Wizard. if (programId.startsWith("MV")) diff --git a/java/sage/epg/sd/SDRipper.java b/java/sage/epg/sd/SDRipper.java index 6ee803afe..5a8c26d35 100644 --- a/java/sage/epg/sd/SDRipper.java +++ b/java/sage/epg/sd/SDRipper.java @@ -81,8 +81,18 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static sage.epg.sd.SDErrors.ACCOUNT_DISABLED; +import static sage.epg.sd.SDErrors.ACCOUNT_LOCKOUT; +import static sage.epg.sd.SDErrors.INVALID_USER; +import static sage.epg.sd.SDErrors.MAX_IMAGE_DOWNLOADS; +import static sage.epg.sd.SDErrors.MAX_IMAGE_DOWNLOADS_TRIAL; +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; public class SDRipper extends EPGDataSource { @@ -90,6 +100,8 @@ public class SDRipper extends EPGDataSource private static final String PROP_PREFIX = "sdepg_core"; private static final String AUTH_FILE = "sdauth"; + private static final String PROP_USERNAME = PROP_PREFIX + "/username"; + private static final String PROP_PASSWORD = PROP_PREFIX + "/password"; private static final String PROP_REGION = PROP_PREFIX + "/locale/region"; private static final String PROP_COUNTRY = PROP_PREFIX + "/locale/country"; private static final String PROP_POSTAL_CODE = PROP_PREFIX + "/locale/postal_code"; @@ -215,9 +227,41 @@ public static void reopenSession() throws SDException, IOException // Do not use this method without getting a sessionLock write lock first. private static SDSession openNewSession() throws IOException, SDException { - SDSession returnValue; + 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; + String fileUsername = null; + String filePassword = null; + + //get the property based user/pass if any + if(Sage.client){ //get the server property + try + { + Object serverProp = SageTV.api("GetServerProperty", new Object[] { PROP_USERNAME, null }); + 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) + { + if (Sage.DBG) System.out.println("ERROR executing server API call of:" + t); + t.printStackTrace(); + propUsername = null; + propPassword = null; + } + }else{ + propUsername = Sage.get(PROP_USERNAME, null); + 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); @@ -239,11 +283,9 @@ private static SDSession openNewSession() throws IOException, SDException throw new SDException(SDErrors.SAGETV_NO_PASSWORD); } - String username = auth.substring(0, split); - String password = auth.substring(split + 1); + fileUsername = auth.substring(0, split); + filePassword = auth.substring(split + 1); - // This will throw an exception if there are any issues connecting. - returnValue = new SDSageSession(username, password); } catch (FileNotFoundException e) { @@ -260,6 +302,39 @@ private static SDSession openNewSession() throws IOException, SDException } } + boolean authenticated = false; + if(propUsername!=null && propPassword!=null){ + //try the prop based user/pass + try { + // 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); + } catch (Exception e) { + if (Sage.DBG) System.out.println("SDEPG checking for prop based user/pass FAILED: username:" + propUsername + " password:" + propPassword); + } + } + + if(!authenticated && fileUsername!=null && filePassword!=null){ + //try the file based user/pass + try { + // This will throw an exception if there are any issues connecting. + returnValue = new SDSageSession(fileUsername, filePassword); + 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); + } catch (Exception e) { + if (Sage.DBG) System.out.println("SDEPG 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"); + 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; @@ -2102,7 +2177,8 @@ else if (seriesDetail.getCode() != 0) { newID[0] = 'S'; newID[1] = 'H'; - singleLookup[0] = new String(newID, 0, 10); + //03-04-2025 jusjoken SD now allows lookup with all 14 characters so no longer shorten to 10 + singleLookup[0] = new String(newID); SDProgramImages images[] = ensureSession().getProgramImages(singleLookup); singleLookup[0] = null; @@ -2477,12 +2553,19 @@ public void run() case INVALID_HASH: case INVALID_USER: sage.msg.MsgManager.postMessage(sage.msg.SystemMessage.createSDInvalidUsernamePasswordMsg()); + resetToken(); break; case ACCOUNT_LOCKOUT: sage.msg.MsgManager.postMessage(sage.msg.SystemMessage.createSDAccountLockOutMsg()); + resetToken(); break; case ACCOUNT_DISABLED: sage.msg.MsgManager.postMessage(sage.msg.SystemMessage.createSDAccountDisabledMsg()); + resetToken(); + break; + case TOO_MANY_LOGINS: + sage.msg.MsgManager.postMessage(sage.msg.SystemMessage.createSDTooManyLoginsMsg()); + resetToken(); break; } setExceptionTimeout(e.ERROR); @@ -2928,6 +3011,7 @@ public static SDInProgressSport[] getInProgressSport(String programIDs[]) case INVALID_HASH: case INVALID_USER: Arrays.fill(returnValues, i, returnValues.length, new SDInProgressSport(SDErrors.SAGETV_NO_PASSWORD.CODE)); + resetToken(); return returnValues; default: returnValues[i] = new SDInProgressSport(SDErrors.SAGETV_UNKNOWN.CODE); @@ -2952,6 +3036,8 @@ private static void setExceptionTimeout(SDErrors error) switch (error) { case SERVICE_OFFLINE: + case MAX_IMAGE_DOWNLOADS: + case MAX_IMAGE_DOWNLOADS_TRIAL: // When the service is offline, we should only check every 30 minutes to see if it's back. // This might generate EPG warnings in the UI if it goes on for a while. SDRipper.retryWait = -(Sage.time() + Sage.MILLIS_PER_MIN * 30); @@ -2959,13 +3045,41 @@ private static void setExceptionTimeout(SDErrors error) case SAGETV_NO_PASSWORD: case INVALID_HASH: case INVALID_USER: + case TOO_MANY_LOGINS: + case ACCOUNT_LOCKOUT: + case ACCOUNT_DISABLED: // Set this to an hour so we aren't too obnoxious about the authentication error messages // and so we shouldn't accidentally lock the account out. SDRipper.retryWait = Sage.time() + Sage.MILLIS_PER_HR; + resetToken(); break; - case ACCOUNT_LOCKOUT: - SDRipper.retryWait = Sage.time() + Sage.MILLIS_PER_HR; + case SAGETV_UNKNOWN: + case SAGETV_COMMUNICATION_ERROR: + case SAGETV_SERVICE_MISSING: + case SAGETV_TOKEN_RETURN_MISSING: + // wait 30 minutes before continuing as the error is unknown or service affecting but may lockout the account + SDRipper.retryWait = -(Sage.time() + Sage.MILLIS_PER_MIN * 30); break; } } + + //reset token to null by ending the session when SD sends an error that requires getting a new token + //JUSJOKEN:2025-02-13 + private static void resetToken(){ + try { + ensureSession().endSession(); + } catch (SDException ex) { + if (Sage.DBG) + { + System.out.println("SDEPG invalid SD authentication - token reset so next call will get a new token"); + } + } catch (IOException ex) { + if (Sage.DBG) + { + System.out.println("SDEPG invalid SD authentication - token reset so next call will get a new token"); + } + } + + } + } diff --git a/java/sage/epg/sd/SDSageSession.java b/java/sage/epg/sd/SDSageSession.java index d8a2e17f1..95069e07e 100644 --- a/java/sage/epg/sd/SDSageSession.java +++ b/java/sage/epg/sd/SDSageSession.java @@ -74,6 +74,12 @@ public InputStreamReader put(URL url, byte[] sendBytes, int off, int len, boolea connection.setRequestProperty("Accept-Charset", "ISO-8859-1"); // PUT must have a length or we will not get a reply. connection.setRequestProperty("Content-Length", Integer.toString(len)); + //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 'put' with url '" + url); + } + if (token != null) connection.setRequestProperty("token", token); try @@ -129,7 +135,17 @@ private InputStreamReader post(URL url, byte sendBytes[], int off, int len, bool connection.setRequestProperty("Accept-Charset", "ISO-8859-1"); // POST must have a length or we will not get a reply. connection.setRequestProperty("Content-Length", Integer.toString(len)); + //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 'post' with url '" + url); + } if (token != null) + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("POST Adding token '" + token + "' to post"); + } + connection.setRequestProperty("token", token); try { @@ -143,6 +159,10 @@ private InputStreamReader post(URL url, byte sendBytes[], int off, int len, bool if (retry && connection.getResponseCode() == 403) { token = null; + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("POST response 403 received. setting token to null"); + } try { @@ -154,6 +174,10 @@ private InputStreamReader post(URL url, byte sendBytes[], int off, int len, bool } catch (InterruptedException e) {} authenticate(); + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("POST retry after authenticate call. token = '" + token + "'"); + } return post(url, sendBytes, off, len, false); } @@ -188,14 +212,28 @@ private InputStreamReader get(URL url, boolean retry) throws IOException, SDExce 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 (token != null) - connection.setRequestProperty("token", token); + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("GET Adding token '" + token + "' to get"); + } + + connection.setRequestProperty("token", token); // Schedules Direct will return an http error 403 if the token has expired. The token can expire // because another program is using the same account, so we try once to get the token back. if (retry && connection.getResponseCode() == 403) { token = null; + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("GET response 403 received. setting token to null"); + } try { @@ -207,6 +245,10 @@ private InputStreamReader get(URL url, boolean retry) throws IOException, SDExce } catch (InterruptedException e) {} authenticate(); + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("GET retry after authenticate call. token = '" + token + "'"); + } return get(url, false); } @@ -243,6 +285,11 @@ private InputStreamReader delete(URL url, boolean retry) throws IOException, SDE 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 'delete' with url '" + url); + } if (token != null) connection.setRequestProperty("token", token); diff --git a/java/sage/epg/sd/SDSession.java b/java/sage/epg/sd/SDSession.java index d44a804dd..862523017 100644 --- a/java/sage/epg/sd/SDSession.java +++ b/java/sage/epg/sd/SDSession.java @@ -55,6 +55,8 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.InvalidParameterException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Collection; public abstract class SDSession @@ -86,6 +88,8 @@ public abstract class SDSession // These are set in the static constructor because they can throw format exceptions. // Returns a token if the credentials are valid. protected static final URL GET_TOKEN; + // Returns the current token if the credentials are valid. + protected static final URL GET_TOKEN_CURRENT; // Get the current account status/saved lineups. private static final URL GET_STATUS; // Get a list of available services. @@ -107,6 +111,7 @@ public abstract class SDSession { // Work around so that the URL's are constants. URL newGetToken; + URL newGetTokenCurrent; URL newGetStatus; URL newGetAvailable; URL newGetLineups; @@ -119,12 +124,13 @@ public abstract class SDSession try { newGetToken = new URL(URL_VERSIONED + "/token"); + newGetTokenCurrent = new URL(URL_VERSIONED + "/token/current"); newGetStatus = new URL(URL_VERSIONED + "/status"); newGetAvailable = new URL(URL_VERSIONED + "/available"); newGetLineups = new URL(URL_VERSIONED + "/lineups"); newGetPrograms = new URL(URL_VERSIONED + "/programs"); - newGetSeriesDesc = new URL(URL_VERSIONED + "/metadata/description"); - newGetProgramsImages = new URL(URL_VERSIONED + "/metadata/programs"); + newGetSeriesDesc = new URL(URL_VERSIONED + "/metadata/description/"); + newGetProgramsImages = new URL(URL_VERSIONED + "/metadata/programs/"); newGetSchedules = new URL(URL_VERSIONED + "/schedules"); newGetSchedulesMd5 = new URL(URL_VERSIONED + "/schedules/md5"); } @@ -135,6 +141,7 @@ public abstract class SDSession e.printStackTrace(System.out); newGetToken = null; + newGetTokenCurrent = null; newGetStatus = null; newGetAvailable = null; newGetLineups = null; @@ -146,6 +153,7 @@ public abstract class SDSession } GET_TOKEN = newGetToken; + GET_TOKEN_CURRENT = newGetTokenCurrent; GET_STATUS = newGetStatus; GET_AVAILABLE = newGetAvailable; GET_LINEUPS = newGetLineups; @@ -204,6 +212,16 @@ public String getUsername() return username; } + /** + * Returns the provided token. + * + * @return The current token. + */ + public String getToken() + { + return token; + } + /** * Enable debug logging for all JSON in and out. */ @@ -228,11 +246,19 @@ public static void enableDebug() } debugBytes = (int) fileSize; + //check if the image bypass settings are set and log that in the sagetv debug log file so the use is aware + if(Sage.DBG && Sage.getBoolean("sdepg_core/bypassCelebrityImages", false)){ + System.out.println("sdepg_core/bypassCelebrityImages=true so skipping image load for celebrities"); + } + if(Sage.DBG && Sage.getBoolean("sdepg_core/bypassProgramImages", false)){ + System.out.println("sdepg_core/bypassProgramImages=true so skipping image load for programs"); + } + } } catch (IOException e) { - System.out.println("Unable to open sd_epg.log"); + System.out.println("enableDebug - Unable to open sd_epg.log"); e.printStackTrace(System.out); } } @@ -253,7 +279,7 @@ public static void writeDebugException(Throwable line) { String message = line.getMessage(); if (message == null) message = "null"; - debugWriter.write("#### Exception Start ####"); + debugWriter.write(debugDateTime() + "#### Exception Start ####"); debugWriter.write(message); debugWriter.write(System.lineSeparator()); line.printStackTrace(debugWriter); @@ -268,7 +294,7 @@ public static void writeDebugException(Throwable line) } catch (Exception e) { - System.out.println("Unable to write to sd_epg.log"); + System.out.println("writeDebugException - Unable to write to sd_epg.log"); e.printStackTrace(System.out); } } @@ -283,7 +309,7 @@ public static void writeDebugLine(String line) synchronized (debugLock) { if (line == null) line = "null"; - debugWriter.write(line); + debugWriter.write(debugDateTime() + line); debugWriter.write(System.lineSeparator()); debugWriter.flush(); debugBytes += line.length() + 2; @@ -292,7 +318,7 @@ public static void writeDebugLine(String line) } catch (Exception e) { - System.out.println("Unable to write to sd_epg.log"); + System.out.println("writeDebugLine - Unable to write to sd_epg.log"); e.printStackTrace(System.out); } } @@ -306,14 +332,18 @@ public static void writeDebug(char[] line, int offset, int length) { synchronized (debugLock) { + debugWriter.write(System.lineSeparator()); + debugWriter.write(debugDateTime() + "....."); + debugWriter.write(System.lineSeparator()); debugWriter.write(line, offset, length); + debugWriter.write(System.lineSeparator()); debugBytes += length; // Don't perform a rollover in this method because we could cut a String of JSON in half. } } catch (Exception e) { - System.out.println("Unable to write to sd_epg.log"); + System.out.println("writeDebug - Unable to write to sd_epg.log"); e.printStackTrace(System.out); } } @@ -332,6 +362,16 @@ private static void debugRollover() } } + private static String debugDateTime(){ + LocalDateTime now = LocalDateTime.now(); + + // Define a DateTimeFormatter to format the LocalDateTime object + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // Format the LocalDateTime object into a string + return now.format(formatter) + ": "; + } + /** * Connect to Schedules Direct and get a token if there isn't a token or 12 hours has passed since * the last token was acquired. @@ -341,9 +381,13 @@ private static void debugRollover() */ 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() < tokenExpiration && token != null) + if (System.currentTimeMillis()/1000 < tokenExpiration && token != null) { + if(Sage.DBG && debugEnabled()) System.out.println("SDSession/authenticate: using existing token:" + token + " with expiry:" + tokenExpiration + " expires in:" + ((tokenExpiration - (System.currentTimeMillis()/1000))/60) + " mins"); return; } @@ -355,7 +399,9 @@ public synchronized void authenticate() throws IOException, SDException authRequest.addProperty("username", username); authRequest.addProperty("password", passHash); + //new token endpoint will either return the current token or a new token and will include the expiry in UTC InputStreamReader reader = post(GET_TOKEN, authRequest); + JsonObject response = gson.fromJson(reader, JsonObject.class); try @@ -388,9 +434,18 @@ else if (tokenElement == null) } token = tokenElement.getAsString(); - // The token is good for 24 hours, but I don't trust that we won't introduce a race condition by - // relying on that down to the millisecond, so we renew at least every 12 hours. - tokenExpiration = System.currentTimeMillis() + Sage.MILLIS_PER_DAY / 2; + + JsonElement tokenExpiryElement = response.get("tokenExpires"); + if(tokenExpiryElement != null){ + //The token is good for 24 hours, and now includes the expiration time in UTC + //the expiration should be the value of the JSON element + tokenExpiration = tokenExpiryElement.getAsLong(); + if(Sage.DBG && debugEnabled()) System.out.println("SDSession/authenticate: retrieved token:" + token + " with passed expiry:" + tokenExpiration); + }else{ + tokenExpiration = (System.currentTimeMillis()/1000) + (Sage.MILLIS_PER_DAY/1000) / 2; + if(Sage.DBG && debugEnabled()) System.out.println("SDSession/authenticate: retrieved token:" + token + " with calculated expiry:" + tokenExpiration); + } + } /** @@ -883,6 +938,11 @@ public SDLineupMap getLineup(String uri) throws IOException, SDException */ public SDProgram[] getPrograms(Collection programs) throws IOException, SDException { + //03-03-2025 jusjoken: check for empty array so we do not bother sending it to AD + if (programs.size()==0){ + if (Sage.DBG) System.out.println("EPG getPrograms requested for empty list - returning empty list to avoid sending nothing to SD"); + return new SDProgram[0]; + } if (programs.size() > 5000) throw new InvalidParameterException("You cannot get more than 5000 programs in one query."); @@ -1026,19 +1086,36 @@ public SDSeriesDesc[] getSeriesDesc(String[] programs) throws IOException, SDExc */ public SDProgramImages[] getProgramImages(String[] programs) throws IOException, SDException { + + //check for image processing bypass property and skip all images if set to true + if(Sage.getBoolean("sdepg_core/bypassProgramImages", false)){ + if (SDSession.debugEnabled()){ + writeDebugLine("getProgramImages: sdepg_core/bypassProgramImages=true so skipping image load for programs = " + programs); + } + return null; + } + if (programs.length > 500) throw new InvalidParameterException("You cannot get more than 500 images in one query."); JsonArray submit = new JsonArray(); for (String program : programs) { - if (program.length() != 10) - submit.add(program.substring(0, 10)); - else + //03-01-2025 jusjoken: added validation for program ids + //first check if its already formated correctly + if(SDUtils.isValidShortProgramID(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); + } + } - SDProgramImages[] returnValues = postJson(GET_PROGRAMS_IMAGES, SDProgramImages[].class, submit); + // JUSJOKEN: 2025-02-13 - SD now require a token for images + SDProgramImages[] returnValues = postAuthJson(GET_PROGRAMS_IMAGES, SDProgramImages[].class, submit); return returnValues; } @@ -1056,10 +1133,18 @@ public SDImage[] getCelebrityImages(String personId) throws IOException, SDExcep if (personId == null || personId.length() == 0 || personId.equals("0")) return SDProgramImages.EMPTY_IMAGES; + //check for image processing bypass property and skip all images if set to true + if(Sage.getBoolean("sdepg_core/bypassCelebrityImages", false)){ + if (SDSession.debugEnabled()){ + writeDebugLine("getCelebrityImages: sdepg_core/bypassCelebrityImages=true so skipping image load for personId = " + personId); + } + return SDProgramImages.EMPTY_IMAGES; + } + try { - // A token is not required to perform this lookup. - return getJson(new URL(GET_CELEBRITY_IMAGES + personId), SDImage[].class); + // JUSJOKEN: 2025-02-13 - SD now require a token for images + return getAuthJson(new URL(GET_CELEBRITY_IMAGES + personId), SDImage[].class); } catch (JsonSyntaxException e) { diff --git a/java/sage/epg/sd/SDUtils.java b/java/sage/epg/sd/SDUtils.java index b130323b0..843cab55f 100644 --- a/java/sage/epg/sd/SDUtils.java +++ b/java/sage/epg/sd/SDUtils.java @@ -46,6 +46,7 @@ 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; @@ -93,6 +94,7 @@ public static InputStreamReader getStream(HttpsURLConnection connection) throws { // Determine how we should get the stream and if we should assume there's an error. boolean errorPresent = connection.getResponseCode() == 400; + boolean errorPresent403 = connection.getResponseCode() == 403; boolean gzipPresent = false; InputStream inputStream; @@ -100,8 +102,28 @@ public static InputStreamReader getStream(HttpsURLConnection connection) throws // an exception so we need to treat this error code like it's not an error. if (errorPresent) { + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("HTTP 400 returned"); + } inputStream = connection.getErrorStream(); } + //process 403 error which indicates debug_sd_support is enabled but SD is not accepting that on their side + else if (errorPresent403){ + if (SDSession.debugEnabled()) + { + SDSession.writeDebugLine("HTTP 403 received. Processing"); + SDSession.writeDebugLine("HTTP 403 Disabling debug_sd_support. Process will restart"); + if(Sage.getBoolean("debug_sd_support", false)){ + Sage.putBoolean("debug_sd_support", false); + } + } + + //stop processing + SDErrors.throwErrorForCode(2055); + throw new SDException(SDErrors.SAGETV_UNKNOWN); + + } else { // Use a buffered input stream so we can check the first few bytes for encoding so to help @@ -131,8 +153,11 @@ public static InputStreamReader getStream(HttpsURLConnection connection) throws reader = new InputStreamReader(inputStream, SDSession.IN_CHARSET); } + //process 400 error if (errorPresent) { + if (SDSession.debugEnabled()){SDSession.writeDebugLine("HTTP 400 processing");} + JsonElement errorElement = GSON.fromJson(reader, JsonElement.class); if (SDSession.debugEnabled()) @@ -150,7 +175,7 @@ public static InputStreamReader getStream(HttpsURLConnection connection) throws throw new SDException(SDErrors.SAGETV_UNKNOWN); } - + if (SDSession.debugEnabled()) { try @@ -409,6 +434,15 @@ public static String fromSageTVtoProgram(String program) program.getChars(2, 12, returnValue, 4); return new String(returnValue); } + if (program.length() == 10 && program.startsWith("EP")) + { + program = program + "0000"; + program.replace("EP", "SH"); + return program; + }else if(program.length() == 10){ + program = program + "0000"; + return program; + } return program; } @@ -425,6 +459,37 @@ public static String fromProgramToSageTV(String program) return program; } + /** + * Check if a given program ID is valid to send to SD. + * + * @param programId The program ID to check. + * @return true if a given program ID is valid to send to SD. + */ + 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"))) + return false; + return true; + } + /** * Check if a given external ID will likely have an associated series. * @@ -509,8 +574,10 @@ public static Person getPerson(SDPerson person, Wizard wiz) int personId = person.getPersonIdAsInt(); // Sports teams will not have any kind of person ID, so all we can do is just add their name. - if (personId == 0) + if (personId == 0){ + //JUSJOKEN 2025-02-12 - NO CHANGE but may need to adjust this as these 0 ids are always being fetched return wiz.getPersonForName(personName); + } if (person.isAlias()) personId *= -1; @@ -521,6 +588,7 @@ public static Person getPerson(SDPerson person, Wizard wiz) newPersion = wiz.addPerson(personName, personId, 0, 0, "", Pooler.EMPTY_SHORT_ARRAY, Pooler.EMPTY_STRING_ARRAY, Pooler.EMPTY_2D_BYTE_ARRAY, DBObject.MEDIA_MASK_TV); } + //JUSJOKEN 2025-02-12 - may need to make a change here to NOT return the person if it was already in the DB return newPersion; } diff --git a/java/sage/msg/SystemMessage.java b/java/sage/msg/SystemMessage.java index 6ce64e21f..269334b22 100644 --- a/java/sage/msg/SystemMessage.java +++ b/java/sage/msg/SystemMessage.java @@ -42,6 +42,7 @@ public class SystemMessage extends SageMsg public static final int LINEUP_SD_ACCOUNT_DISABLED_MSG = 1010; 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; // Scheduler related public static final int MISSED_RECORDING_FROM_CONFLICT_MSG = 1050; @@ -92,6 +93,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_TOO_MANY_LOGINS_MSG: + return sage.Sage.rez("LINEUP_SD_TOO_MANY_LOGINS"); case MISSED_RECORDING_FROM_CONFLICT_MSG: return sage.Sage.rez("MISSED_RECORDING_FROM_CONFLICT"); case CAPTURE_DEVICE_LOAD_ERROR_MSG: @@ -188,6 +191,16 @@ public static SystemMessage createPlaylistMissingSegmentMsg(String playlistPath, sage.Sage.rez("PLAYLIST_IMPORT_MISSING_SEGMENT_MSG", new Object[] { playlistPath, segmentPath }), props); } + public static SystemMessage createVersionUpdateMsg(String version, String location) + { + 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(versionMsg, new Object[] { version, location }), props); + } + public static SystemMessage createPluginUpdateMsg(String pluginID, String pluginName, String version) { java.util.Properties props = new java.util.Properties(); @@ -330,6 +343,12 @@ public static SystemMessage createSDAccountLockOutMsg() sage.Sage.rez("LINEUP_SD_ACCOUNT_LOCKOUT_MSG"), null); } + public static SystemMessage createSDTooManyLoginsMsg() + { + return new SystemMessage(LINEUP_SD_TOO_MANY_LOGINS_MSG, ERROR_PRIORITY, + sage.Sage.rez("LINEUP_SD_TOO_MANY_LOGINS_MSG"), null); + } + public static SystemMessage createOOMMsg() { return new SystemMessage(OUT_OF_MEMORY_MSG, ERROR_PRIORITY, sage.Sage.rez("OUT_OF_MEMORY_MSG"), null); diff --git a/native/so/HDHomeRun2.0/DTVChannel.cpp b/native/so/HDHomeRun2.0/DTVChannel.cpp index bd977a528..4b5938573 100755 --- a/native/so/HDHomeRun2.0/DTVChannel.cpp +++ b/native/so/HDHomeRun2.0/DTVChannel.cpp @@ -463,10 +463,10 @@ int DTVChannel::setTuning(SageTuningParams *params) } if(dbg->debug_source[0]) { - if(dbg->source_fd > 0) + if(dbg->source_fd != 0) fclose(dbg->source_fd); dbg->source_fd = fopen(dbg->debug_source, "r"); - if(dbg->source_fd > 0) + if(dbg->source_fd != 0) flog("Native.log", "DTVChannel: Debug source file open: %s mode:%d (%d).\r\n", dbg->debug_source, dbg->debug_source_mode, dbg->source_fd ); } @@ -712,7 +712,7 @@ void DTVChannel::splitStream(void *buffer, size_t size) //flog("Native.log", "DTVChannel::splitStream(%p, %d this:0x%x 0x%x 0x%x)\r\n", buffer, size, (uint32_t)this, // parserEnabled, scanChannelEnabled ); mBytesIn += (off_t)size; - if ( 0 && dbg->dump_fd > 0 && dbg->dump_size > dbg->dumped_bytes ) + if ( 0 && dbg->dump_fd != 0 && dbg->dump_size > dbg->dumped_bytes ) { fwrite( pData, 1, lDataLen, dbg->dump_fd ); dbg->dumped_bytes += lDataLen; @@ -1417,7 +1417,7 @@ int DTVChannel::scanChannelState( int *pScanState, int *pFoundChannelNum ) ASSERT( scanFilter != NULL ); *pScanState = ScanChannelState( scanFilter ); *pFoundChannelNum = ScanChannelNum( scanFilter ); - return pScanState > 0; + return pScanState != 0; } int DTVChannel::scanChannelList( void** ppChannelList ) diff --git a/third_party/mplayer/stream/stream_sagetv.c b/third_party/mplayer/stream/stream_sagetv.c index 21ffb8068..610f694dc 100644 --- a/third_party/mplayer/stream/stream_sagetv.c +++ b/third_party/mplayer/stream/stream_sagetv.c @@ -664,7 +664,7 @@ static int open_s(stream_t *stream,int mode, void* opts, int* file_format) { #ifdef CONFIG_DARWIN pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); #else - pthread_mutexattr_setkind_np(&attr, PTHREAD_MUTEX_RECURSIVE_NP); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); #endif if (pthread_mutex_init(p->mutex, &attr) != 0) {