From 1abdaa68f15a5ccda7c67dd61aa5089804b20224 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 1 Dec 2025 11:24:23 -0400 Subject: [PATCH 01/13] Update PR template --- pull_request_template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pull_request_template.md b/pull_request_template.md index b45a9ec..75eb426 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -7,8 +7,8 @@ * [ ] I have read the code of conduct. * [ ] I am submitting to the correct base branch ### Changes that modify the db structure From ba4ca135640858f9033911329fc6834ceef6fe84 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 1 Dec 2025 11:27:33 -0400 Subject: [PATCH 02/13] Revert start/end time changes. They are being included in 1.6.x --- src/helperFuncs.js | 69 +++-------- src/plex-player.js | 68 ++--------- src/services/channel-service.js | 30 +---- src/services/tv-guide-service.js | 69 +---------- web/directives/channel-config.js | 17 +-- web/directives/program-config.js | 147 +++-------------------- web/public/templates/channel-config.html | 9 -- web/public/templates/program-config.html | 81 ------------- web/services/common-program-tools.js | 10 +- 9 files changed, 60 insertions(+), 440 deletions(-) diff --git a/src/helperFuncs.js b/src/helperFuncs.js index 453dd57..1d905f9 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -27,13 +27,6 @@ const CHANNEL_CONTEXT_KEYS = [ module.exports.random = random; function getCurrentProgramAndTimeElapsed(date, channel) { - // If seekPosition is not set, default to 0 - function getSeek(program) { - return typeof program.seekPosition === 'number' ? program.seekPosition : 0; - } - function getEnd(program) { - return typeof program.endPosition === 'number' ? program.endPosition : null; - } let channelStartTime = (new Date(channel.startTime)).getTime(); if (channelStartTime > date) { let t0 = date; @@ -51,36 +44,23 @@ function getCurrentProgramAndTimeElapsed(date, channel) { let timeElapsed = (date - channelStartTime) % channel.duration let currentProgramIndex = -1 for (let y = 0, l2 = channel.programs.length; y < l2; y++) { - let program = channel.programs[y]; - // Compute effective duration based on seek/end - let seek = getSeek(program); - let end = getEnd(program); - let effectiveDurationForProgram = (end !== null ? end : program.duration) - seek; - if (timeElapsed - effectiveDurationForProgram < 0) { + let program = channel.programs[y] + if (timeElapsed - program.duration < 0) { currentProgramIndex = y - if ( ((end !== null ? end - seek : program.duration - seek) > 2*SLACK) && (timeElapsed > (end !== null ? end - seek : program.duration - seek) - SLACK) ) { + if ( (program.duration > 2*SLACK) && (timeElapsed > program.duration - SLACK) ) { timeElapsed = 0; currentProgramIndex = (y + 1) % channel.programs.length; } break; } else { - timeElapsed -= (end !== null ? end - seek : program.duration - seek); + timeElapsed -= program.duration } } if (currentProgramIndex === -1) throw new Error("No program found; find algorithm fucked up") - // Attach seek/end for downstream use - let program = channel.programs[currentProgramIndex]; - let seek = getSeek(program); - let end = getEnd(program); - let effectiveDurationForProgram = (end !== null ? end : program.duration) - seek; - return { - program: Object.assign({}, program, { seekPosition: seek, endPosition: end, effectiveDuration: effectiveDurationForProgram }), - timeElapsed: timeElapsed, - programIndex: currentProgramIndex - } + return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex } } function createLineup(programPlayTime, obj, channel, fillers, isFirst) { @@ -90,9 +70,6 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) { // Helps prevents loosing first few seconds of an episode upon lineup change let activeProgram = obj.program let beginningOffset = 0; - // Use seekPosition and endPosition for effective start and duration - let seek = typeof activeProgram.seekPosition === 'number' ? activeProgram.seekPosition : 0; - let end = typeof activeProgram.endPosition === 'number' ? activeProgram.endPosition : null; let lineup = [] @@ -184,29 +161,19 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) { } beginningOffset = Math.max(0, originalTimeElapsed - timeElapsed); - // Calculate effective start, duration, and streamDuration using seek/end - const effectiveSeek = seek; - const effectiveEnd = end !== null ? end : activeProgram.duration; - const effectiveDuration = effectiveEnd - effectiveSeek; - const effectiveTimeElapsed = Math.max(0, timeElapsed); - const effectiveStreamDuration = effectiveDuration - effectiveTimeElapsed; - - return [{ - type: 'program', - title: activeProgram.title, - key: activeProgram.key, - plexFile: activeProgram.plexFile, - file: activeProgram.file, - ratingKey: activeProgram.ratingKey, - start: effectiveSeek + effectiveTimeElapsed, // playback should start at seek + elapsed - streamDuration: effectiveStreamDuration, - beginningOffset: beginningOffset, - duration: effectiveDuration, - originalDuration: activeProgram.duration, - serverKey: activeProgram.serverKey, - seekPosition: effectiveSeek, - endPosition: end - }]; + return [ { + type: 'program', + title: activeProgram.title, + key: activeProgram.key, + plexFile: activeProgram.plexFile, + file: activeProgram.file, + ratingKey: activeProgram.ratingKey, + start: timeElapsed, + streamDuration: activeProgram.duration - timeElapsed, + beginningOffset: beginningOffset, + duration: activeProgram.duration, + serverKey: activeProgram.serverKey + } ]; } function weighedPick(a, total) { diff --git a/src/plex-player.js b/src/plex-player.js index 2f20ae1..9a75f84 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -64,66 +64,24 @@ class PlexPlayer { let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options ffmpeg.setAudioOnly( this.context.audioOnly ); this.ffmpeg = ffmpeg; - - // Get basic parameters - let seek = typeof lineupItem.seekPosition === 'number' ? lineupItem.seekPosition : 0; - let end = typeof lineupItem.endPosition === 'number' ? lineupItem.endPosition : null; - let currentElapsed = typeof lineupItem.start === 'number' ? lineupItem.start : 0; - let programEnd = end !== null ? end : lineupItem.duration; - - let deinterlace = ffmpegSettings.enableFFMPEGTranscoding; - - // Get stream first so we can handle direct play correctly + let streamDuration; + if (typeof(lineupItem.streamDuration)!=='undefined') { + if (lineupItem.start + lineupItem.streamDuration + constants.SLACK < lineupItem.duration) { + streamDuration = lineupItem.streamDuration / 1000; + } + } + let deinterlace = ffmpegSettings.enableFFMPEGTranscoding; //for now it will always deinterlace when transcoding is enabled but this is sub-optimal + let stream = await plexTranscoder.getStream(deinterlace); if (this.killed) { return; } - - // Calculate parameters differently for direct play vs transcoded mode - let streamDuration; - let streamStart; - - if (stream.directPlay) { - // DIRECT PLAY: - // 1. Calculate duration from endPos to currentElapsed (not from seek to endPos) - streamDuration = Math.max(0, programEnd - currentElapsed) / 1000; - - // 2. Start should be ONLY currentElapsed - streamStart = currentElapsed / 1000; - - console.log(`[PLEX-PLAYER] Direct Play: Using duration=${streamDuration}s (from currentElapsed=${currentElapsed/1000}s to endPos=${programEnd/1000}s)`); - - // For direct play, ignore the streamDuration override with custom end times - if (end !== null && typeof(lineupItem.streamDuration) !== 'undefined') { - // Store original value for reference - stream.streamStats.originalDuration = lineupItem.streamDuration; - stream.streamStats.duration = Math.max(streamDuration * 1000, 60000); - - console.log(`[PLEX-PLAYER] Direct Play: Custom end time detected, ignoring streamDuration override: ${lineupItem.streamDuration/1000}s`); - lineupItem.streamDuration = undefined; - } - } else { - // TRANSCODED: Keep existing behavior - streamStart = undefined; // Plex handles this internally for transcoded streams - - // Calculate duration based on programEnd and seek - streamDuration = Math.max(0, programEnd - seek) / 1000; - - // Apply streamDuration override if present - only for transcoded streams - if (typeof(lineupItem.streamDuration) !== 'undefined') { - streamDuration = lineupItem.streamDuration / 1000; - console.log(`[PLEX-PLAYER] Transcoding: Using override streamDuration: ${streamDuration}s`); - } - - console.log(`[PLEX-PLAYER] Transcoding: Using duration=${streamDuration}s (seek=${seek/1000}s, end=${programEnd/1000}s)`); - } - + + //let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined; + //let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : lineupItem.start; + let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined; let streamStats = stream.streamStats; - - // Ensure we have a valid duration for error handling - if (!streamStats.duration) { - streamStats.duration = Math.max(streamDuration * 1000, 60000); - } + streamStats.duration = lineupItem.streamDuration; let emitter = new EventEmitter(); //setTimeout( () => { diff --git a/src/services/channel-service.js b/src/services/channel-service.js index 831675e..cffcf70 100644 --- a/src/services/channel-service.js +++ b/src/services/channel-service.js @@ -59,19 +59,8 @@ class ChannelService extends events.EventEmitter { function cleanUpProgram(program) { - if (program.startPosition != null && program.startPosition !== '') { - // Convert startPosition to seekPosition for consistency - program.seekPosition = parseInt(program.startPosition, 10); - delete program.startPosition; - } - - if (program.endPosition != null && program.endPosition !== '') { - program.endPosition = parseInt(program.endPosition, 10); - } - - if (program.start && program.stop) { - program.duration = new Date(program.stop) - new Date(program.start); - } + delete program.start + delete program.stop delete program.streams; delete program.durationStr; delete program.commercials; @@ -102,23 +91,12 @@ function cleanUpChannel(channel) { delete channel.fillerContent; delete channel.filler; channel.fallback = channel.fallback.flatMap( cleanUpProgram ); - - // Set default for mergeAdjacentPrograms if not already defined - if (typeof channel.mergeAdjacentPrograms === 'undefined') { - channel.mergeAdjacentPrograms = false; // Disabled by default for backward compatibility - } - - // Calculate total channel duration using effective durations channel.duration = 0; for (let i = 0; i < channel.programs.length; i++) { - let program = channel.programs[i]; - let seek = typeof program.seekPosition === 'number' ? program.seekPosition : 0; - let end = typeof program.endPosition === 'number' ? program.endPosition : null; - let effectiveDuration = (end !== null ? end : program.duration) - seek; - - channel.duration += effectiveDuration; + channel.duration += channel.programs[i].duration; } return channel; + } diff --git a/src/services/tv-guide-service.js b/src/services/tv-guide-service.js index 85bbe3e..1b33c54 100644 --- a/src/services/tv-guide-service.js +++ b/src/services/tv-guide-service.js @@ -83,12 +83,7 @@ class TVGuideService extends events.EventEmitter let arr = new Array( channel.programs.length + 1); arr[0] = 0; for (let i = 0; i < n; i++) { - // Calculate effective duration based on seekPosition and endPosition - let program = channel.programs[i]; - let seek = typeof program.seekPosition === 'number' ? program.seekPosition : 0; - let end = typeof program.endPosition === 'number' ? program.endPosition : null; - let d = (end !== null ? end : program.duration) - seek; - + let d = channel.programs[i].duration; if (d == 0) { console.log("Found program with duration 0, correcting it"); d = 1; @@ -97,6 +92,7 @@ class TVGuideService extends events.EventEmitter console.log( `Found program in channel ${channel.number} with non-integer duration ${d}, correcting it`); d = Math.ceil(d); } + channel.programs[i].duration = d; arr[i+1] = arr[i] + d; await this._throttle(); } @@ -364,70 +360,10 @@ class TVGuideService extends events.EventEmitter result.programs.push( makeEntry(channel, programs[i] ) ); } } - - // Only merge programs if enabled in channel settings - if (channel.mergeAdjacentPrograms === true) { - result.programs = this.mergeAdjacentSamePrograms(result.programs); - } return result; } - // Merge adjacent programs that have the same ratingKey - mergeAdjacentSamePrograms(programs) { - if (!programs || programs.length <= 1) { - return programs; - } - - console.log(`Before merging: ${programs.length} programs`); - - // Debug: Check how many programs have ratingKeys - const programsWithRatingKey = programs.filter(p => p.ratingKey); - console.log(`Programs with ratingKey: ${programsWithRatingKey.length}`); - - const mergedPrograms = []; - let i = 0; - - while (i < programs.length) { - const currentProgram = programs[i]; - - // Skip if this is a flex/placeholder program with no ratingKey - if (!currentProgram.ratingKey) { - mergedPrograms.push(currentProgram); - i++; - continue; - } - - // Look ahead to see if there are adjacent programs with the same ratingKey - let j = i + 1; - while (j < programs.length && - programs[j].ratingKey && - programs[j].ratingKey === currentProgram.ratingKey) { - j++; - } - - if (j > i + 1) { - // We found programs to merge - console.log(`Merging ${j-i} programs with ratingKey ${currentProgram.ratingKey}`); - - const mergedProgram = {...currentProgram}; - mergedProgram.stop = programs[j-1].stop; - mergedPrograms.push(mergedProgram); - - // Skip all the programs we just merged - i = j; - } else { - // No programs to merge, just add the current one - mergedPrograms.push(currentProgram); - i++; - } - } - - console.log(`After merging: ${mergedPrograms.length} programs`); - - return mergedPrograms; - } - async buildItManaged() { let t0 = this.currentUpdate; let t1 = this.currentLimit; @@ -644,7 +580,6 @@ function makeEntry(channel, x) { icon: icon, title: title, sub: sub, - ratingKey: x.program.ratingKey // Add ratingKey to preserve it for merging } } diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index adce35a..d8da0fe 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -964,24 +964,9 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get scope.hasFlex = false; for (let i = 0, l = scope.channel.programs.length; i < l; i++) { - // Calculate effective duration using seekPosition and endPosition - let program = scope.channel.programs[i]; - let seek = typeof program.seekPosition === 'number' ? program.seekPosition : 0; - let end = typeof program.endPosition === 'number' ? program.endPosition : null; - let effectiveDuration = (end !== null ? end : program.duration) - seek; - - // Store effective values for consistency - program.effectiveStart = seek; - program.effectiveDuration = effectiveDuration; - - // Set start time based on accumulated duration scope.channel.programs[i].start = new Date(scope.channel.startTime.valueOf() + scope.channel.duration) scope.channel.programs[i].$index = i; - - // Use effectiveDuration for timeline calculation - scope.channel.duration += effectiveDuration; - - // Set stop time using the updated duration + scope.channel.duration += scope.channel.programs[i].duration scope.channel.programs[i].stop = new Date(scope.channel.startTime.valueOf() + scope.channel.duration) if (scope.channel.programs[i].isOffline) { scope.hasFlex = true; diff --git a/web/directives/program-config.js b/web/directives/program-config.js index 0715b45..f274eb5 100644 --- a/web/directives/program-config.js +++ b/web/directives/program-config.js @@ -9,138 +9,29 @@ module.exports = function ($timeout) { onDone: "=onDone" }, link: function (scope, element, attrs) { - // Conversion functions remain the same (using NaN for invalid) - scope.msToTimeString = function(ms) { - if (typeof ms !== 'number' || isNaN(ms) || ms < 0) { return ''; } - let totalS = Math.floor(ms / 1000); - let s = totalS % 60; - let m = Math.floor(totalS / 60); - return m + ":" + ( (s < 10) ? ("0" + s) : s ); - }; - - scope.timeStringToMs = function(timeString) { - if (timeString == null || timeString.trim() === '') { return 0; } // Empty is 0ms - let parts = timeString.split(':'); - if (parts.length !== 2) { return NaN; } // Invalid format - let min = parseInt(parts[0], 10); - let sec = parseInt(parts[1], 10); - if (isNaN(min) || isNaN(sec) || sec < 0 || sec >= 60 || min < 0) { return NaN; } // Invalid numbers - return (min * 60 + sec) * 1000; - }; - - // Intermediate model for UI binding - scope.timeInput = { - seek: '', - end: '' - }; - - let initialProgramLoad = true; // Flag for first load - - // Watch program to initialize/reset intermediate model ONLY - scope.$watch('program', function(newProgram) { - if (newProgram) { - console.log("Program loaded/changed. Initializing timeInput."); - // Initialize timeInput from program data - let initialSeekMs = newProgram.seekPosition; - let initialEndMs = newProgram.endPosition; - - scope.timeInput.seek = scope.msToTimeString( (typeof initialSeekMs === 'number' && !isNaN(initialSeekMs)) ? initialSeekMs : 0 ); - scope.timeInput.end = (typeof initialEndMs === 'number' && !isNaN(initialEndMs) && initialEndMs > 0) ? scope.msToTimeString(initialEndMs) : ''; - - initialProgramLoad = false; // Mark initial load complete - } else { - // Clear inputs if program is removed - scope.timeInput.seek = ''; - scope.timeInput.end = ''; - initialProgramLoad = true; // Reset flag if program is cleared - } - }); - scope.finished = (prog) => { - // prog here is the original program object passed to the directive - // We need to validate and apply changes from scope.timeInput + if (prog.title === "") + scope.error = { title: 'You must set a program title.' } + else if (prog.type === "episode" && prog.showTitle == "") + scope.error = { showTitle: 'You must set a show title when the program type is an episode.' } + else if (prog.type === "episode" && (prog.season == null)) + scope.error = { season: 'You must set a season number when the program type is an episode.' } + else if (prog.type === "episode" && prog.season <= 0) + scope.error = { season: 'Season number musat be greater than 0' } + else if (prog.type === "episode" && (prog.episode == null)) + scope.error = { episode: 'You must set a episode number when the program type is an episode.' } + else if (prog.type === "episode" && prog.episode <= 0) + scope.error = { episode: 'Episode number musat be greater than 0' } - let currentError = null; - - // --- Validate Time Inputs --- - let seekInputString = scope.timeInput.seek; - let endInputString = scope.timeInput.end; - - let seekMs = scope.timeStringToMs(seekInputString); - let endMs = scope.timeStringToMs(endInputString); // Will be 0 if empty, NaN if invalid - - // Check for invalid formats (NaN) - if (isNaN(seekMs)) { - currentError = { seekPosition: 'Invalid start time format. Use MM:SS.' }; - } else if (isNaN(endMs) && endInputString && endInputString.trim() !== '') { - // Only error on endMs if it's not empty but is invalid - currentError = { endPosition: 'Invalid end time format. Use MM:SS.' }; - } else { - // Format is valid or empty, now check relationship - // Treat endMs === 0 (from empty input) as 'undefined' for comparison - let effectiveEndMs = (endMs === 0 && (endInputString == null || endInputString.trim() === '')) ? undefined : endMs; - - // Validate Seek Position against Duration first - if (prog.duration && seekMs >= (prog.duration - 1000)) { - currentError = currentError || {}; - currentError.seekPosition = 'Start time must be at least 1 second before the program ends.'; - } - // Then validate End Position if specified - else if (typeof effectiveEndMs === 'number') { - if (effectiveEndMs <= seekMs) { - currentError = currentError || {}; - currentError.endPosition = 'End position must be greater than start position.'; - } else if (prog.duration && effectiveEndMs > prog.duration) { - // Error if end time EXCEEDS program duration - currentError = currentError || {}; - currentError.endPosition = 'End position cannot exceed program duration (' + scope.msToTimeString(prog.duration) + ').'; - } - // Check if start/end combination is valid (at least 1s apart) - else if ((effectiveEndMs - seekMs) < 1000) { - currentError = currentError || {}; - // Apply error to the field being edited or a general one if needed - currentError.endPosition = 'Effective program length must be at least 1 second.'; - } - } + if (scope.error != null) { + $timeout(() => { + scope.error = null + }, 3500) + return } - // --- Standard Validation (on original prog object) --- - if (!currentError) { // Only proceed if time validation passed - if (!prog.title) { currentError = { title: 'You must set a program title.' }; } - else if (prog.type === "episode" && !prog.showTitle) { currentError = { showTitle: 'You must set a show title when the program type is an episode.' }; } - else if (prog.type === "episode" && (prog.season == null || prog.season <= 0)) { currentError = { season: 'Season number must be greater than 0.' }; } - else if (prog.type === "episode" && (prog.episode == null || prog.episode <= 0)) { currentError = { episode: 'Episode number must be greater than 0.' }; } - // Add any other existing standard validations here, setting currentError - } - - // --- Error Handling --- - if (currentError && Object.keys(currentError).length !== 0) { - scope.error = currentError; - $timeout(() => { scope.error = null }, 3500); - return; // Stop execution - } - - // --- Prepare Final Object --- - // Create a clean object based on the original prog and validated time inputs - // Ensure seekMs is a valid number before assigning - let finalSeekMs = isNaN(seekMs) ? 0 : seekMs; - // Ensure endMs is valid number > 0 or undefined - let finalEndMs = (typeof endMs === 'number' && !isNaN(endMs) && endMs > 0) ? endMs : undefined; - - let finalProgData = { - ...prog, // Copy original properties - seekPosition: finalSeekMs, - endPosition: finalEndMs - }; - - // Explicitly remove endPosition if undefined - if (finalProgData.endPosition === undefined) { - delete finalProgData.endPosition; - } - - console.log("Validation passed. Calling onDone with:", finalProgData); - scope.onDone(JSON.parse(angular.toJson(finalProgData))); - scope.program = null; + scope.onDone(JSON.parse(angular.toJson(prog))) + scope.program = null } } }; diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index 7c60b99..081300d 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -727,15 +727,6 @@ (This will hide the channel from TV guides, spoofed HDHR, m3u playlist... The channel can still be streamed directly or be used as a redirect target.) - -
-
- - - - (When enabled, adjacent programs with the same content ID will appear as a single program in the guide. This is useful for shows split by commercials or bumpers.) -
-


diff --git a/web/public/templates/program-config.html b/web/public/templates/program-config.html index b86ebb8..5ba9c7f 100644 --- a/web/public/templates/program-config.html +++ b/web/public/templates/program-config.html @@ -30,33 +30,6 @@
- -
-
-
Advanced Options
-
-
-
- -
-
- - Format: minutes:seconds (e.g. 5:30) -
{{error.seekPosition}}
-
-
- -
-
- - Format: minutes:seconds (e.g. 10:45) -
-
-
-
-
- -
-
-
Advanced Options
-
-
-
- -
-
- - Format: minutes:seconds (e.g. 5:30) -
{{error.seekPosition}}
-
-
- -
-
- - Format: minutes:seconds (e.g. 10:45) -
-
-
-
-
+ +
+
+ + + + (When enabled, adjacent programs with the same content ID will appear as a single program in the guide. This is useful for shows split by commercials or bumpers.) +
+


diff --git a/web/public/templates/program-config.html b/web/public/templates/program-config.html index 5ba9c7f..b86ebb8 100644 --- a/web/public/templates/program-config.html +++ b/web/public/templates/program-config.html @@ -30,6 +30,33 @@
+ +
+
+
Advanced Options
+
+
+
+ +
+
+ + Format: minutes:seconds (e.g. 5:30) +
{{error.seekPosition}}
+
+
+ +
+
+ + Format: minutes:seconds (e.g. 10:45) +
+
+
+
+
+ +
+
+
Advanced Options
+
+
+
+ +
+
+ + Format: minutes:seconds (e.g. 5:30) +
{{error.seekPosition}}
+
+
+ +
+
+ + Format: minutes:seconds (e.g. 10:45) +
+
+
+
+