Merge pull request #488 from vexorian/feature/start-end
feature/start end
This commit is contained in:
commit
6adf09c41b
11
README.md
11
README.md
@ -77,4 +77,13 @@ npm run dev-server
|
||||
* Original pseudotv-Plex code was released under [MIT license (c) 2020 Dan Ferguson](https://github.com/DEFENDORe/pseudotv/blob/665e71e24ee5e93d9c9c90545addb53fdc235ff6/LICENSE)
|
||||
* dizqueTV's improvements are released under zlib license (c) 2020 Victor Hugo Soliz Kuncar
|
||||
* FontAwesome: [https://fontawesome.com/license/free](https://archive.fo/PRqis)
|
||||
* Bootstrap: https://github.com/twbs/bootstrap/blob/v4.4.1/LICENSE
|
||||
* Bootstrap: https://github.com/twbs/bootstrap/blob/v4.4.1/LICENSE
|
||||
|
||||
## Thanks
|
||||
|
||||
* DEFENDORe , George and everyone that worked on PseudoTV
|
||||
* Ahmed Said Al-Busaidi , for reporting exploits in advance before publishing in exploit-db.
|
||||
* Nathan for working on automation addons.
|
||||
* Timebomb, Rafael for the contributions during dizqueTV's active days.
|
||||
|
||||
|
||||
@ -27,6 +27,13 @@ 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;
|
||||
@ -44,23 +51,36 @@ 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]
|
||||
if (timeElapsed - program.duration < 0) {
|
||||
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) {
|
||||
currentProgramIndex = y
|
||||
if ( (program.duration > 2*SLACK) && (timeElapsed > program.duration - SLACK) ) {
|
||||
if ( ((end !== null ? end - seek : program.duration - seek) > 2*SLACK) && (timeElapsed > (end !== null ? end - seek : program.duration - seek) - SLACK) ) {
|
||||
timeElapsed = 0;
|
||||
currentProgramIndex = (y + 1) % channel.programs.length;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
timeElapsed -= program.duration
|
||||
timeElapsed -= (end !== null ? end - seek : program.duration - seek);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentProgramIndex === -1)
|
||||
throw new Error("No program found; find algorithm fucked up")
|
||||
|
||||
return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex }
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
||||
@ -70,6 +90,9 @@ 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 = []
|
||||
|
||||
@ -161,19 +184,29 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
||||
}
|
||||
beginningOffset = Math.max(0, originalTimeElapsed - timeElapsed);
|
||||
|
||||
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
|
||||
} ];
|
||||
// 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
|
||||
}];
|
||||
}
|
||||
|
||||
function weighedPick(a, total) {
|
||||
|
||||
@ -64,24 +64,66 @@ class PlexPlayer {
|
||||
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
|
||||
ffmpeg.setAudioOnly( this.context.audioOnly );
|
||||
this.ffmpeg = ffmpeg;
|
||||
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
|
||||
|
||||
|
||||
// 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 stream = await plexTranscoder.getStream(deinterlace);
|
||||
if (this.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
//let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined;
|
||||
//let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : lineupItem.start;
|
||||
let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined;
|
||||
|
||||
// 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 streamStats = stream.streamStats;
|
||||
streamStats.duration = lineupItem.streamDuration;
|
||||
|
||||
// Ensure we have a valid duration for error handling
|
||||
if (!streamStats.duration) {
|
||||
streamStats.duration = Math.max(streamDuration * 1000, 60000);
|
||||
}
|
||||
|
||||
let emitter = new EventEmitter();
|
||||
//setTimeout( () => {
|
||||
|
||||
@ -59,8 +59,19 @@ class ChannelService extends events.EventEmitter {
|
||||
|
||||
|
||||
function cleanUpProgram(program) {
|
||||
delete program.start
|
||||
delete program.stop
|
||||
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.streams;
|
||||
delete program.durationStr;
|
||||
delete program.commercials;
|
||||
@ -91,12 +102,23 @@ 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++) {
|
||||
channel.duration += channel.programs[i].duration;
|
||||
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;
|
||||
}
|
||||
return channel;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -83,7 +83,12 @@ class TVGuideService extends events.EventEmitter
|
||||
let arr = new Array( channel.programs.length + 1);
|
||||
arr[0] = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
let d = channel.programs[i].duration;
|
||||
// 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;
|
||||
|
||||
if (d == 0) {
|
||||
console.log("Found program with duration 0, correcting it");
|
||||
d = 1;
|
||||
@ -92,7 +97,6 @@ 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();
|
||||
}
|
||||
@ -360,10 +364,70 @@ 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;
|
||||
@ -580,6 +644,7 @@ function makeEntry(channel, x) {
|
||||
icon: icon,
|
||||
title: title,
|
||||
sub: sub,
|
||||
ratingKey: x.program.ratingKey // Add ratingKey to preserve it for merging
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -964,9 +964,24 @@ 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;
|
||||
scope.channel.duration += scope.channel.programs[i].duration
|
||||
|
||||
// Use effectiveDuration for timeline calculation
|
||||
scope.channel.duration += effectiveDuration;
|
||||
|
||||
// Set stop time using the updated duration
|
||||
scope.channel.programs[i].stop = new Date(scope.channel.startTime.valueOf() + scope.channel.duration)
|
||||
if (scope.channel.programs[i].isOffline) {
|
||||
scope.hasFlex = true;
|
||||
|
||||
@ -9,29 +9,138 @@ module.exports = function ($timeout) {
|
||||
onDone: "=onDone"
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
scope.finished = (prog) => {
|
||||
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' }
|
||||
// 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
|
||||
|
||||
if (scope.error != null) {
|
||||
$timeout(() => {
|
||||
scope.error = null
|
||||
}, 3500)
|
||||
return
|
||||
// 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
|
||||
|
||||
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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.onDone(JSON.parse(angular.toJson(prog)))
|
||||
scope.program = null
|
||||
// --- 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -727,6 +727,15 @@
|
||||
|
||||
<span class='text-muted' id="stealthHelp">(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.)</span>
|
||||
</div>
|
||||
|
||||
<div class='form-group' ng-show='! channel.stealth'>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="mergeAdjacentPrograms" ng-model="channel.mergeAdjacentPrograms">
|
||||
<label class="form-check-label" for="mergeAdjacentPrograms">Merge adjacent programs with same content</label>
|
||||
|
||||
<span class='text-muted' id="mergeAdjacentProgramsHelp">(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.)</span>
|
||||
</div>
|
||||
</div>
|
||||
<br></br>
|
||||
<div class='form-group' ng-show='! channel.stealth'>
|
||||
<label class='form-label' >Placeholder program title:</label>
|
||||
|
||||
@ -30,6 +30,33 @@
|
||||
<div class="text-center">
|
||||
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header" ng-click="trackAdvancedOpen = !trackAdvancedOpen" style="cursor: pointer;">
|
||||
<h6 class="mb-0">Advanced Options <i class="float-right" ng-class="{'fa fa-chevron-down': !trackAdvancedOpen, 'fa fa-chevron-up': trackAdvancedOpen}"></i></h6>
|
||||
</div>
|
||||
<div class="collapse" ng-class="{'show': trackAdvancedOpen}">
|
||||
<div class="card-body">
|
||||
<label>Custom Start Time (optional)</label>
|
||||
<div class="form-row mb-3">
|
||||
<div class="col">
|
||||
<input class="form-control form-control-sm" type="text" ng-model="timeInput.seek" placeholder="MM:SS (leave blank for start)">
|
||||
<small class="form-text text-muted">Format: minutes:seconds (e.g. 5:30)</small>
|
||||
<div ng-show="error && error.seekPosition" class="text-danger">{{error.seekPosition}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<label>Custom End Time (optional)
|
||||
<span class="text-danger pull-right">{{error.endPosition}}</span>
|
||||
</label>
|
||||
<div class="form-row mb-3">
|
||||
<div class="col">
|
||||
<input class="form-control form-control-sm" type="text" ng-model="timeInput.end" placeholder="MM:SS (leave blank for end)">
|
||||
<small class="form-text text-muted">Format: minutes:seconds (e.g. 10:45)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="program.type === 'movie'">
|
||||
<label>Movie Title
|
||||
@ -48,6 +75,33 @@
|
||||
<div class="text-center">
|
||||
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header" ng-click="movieAdvancedOpen = !movieAdvancedOpen" style="cursor: pointer;">
|
||||
<h6 class="mb-0">Advanced Options <i class="float-right" ng-class="{'fa fa-chevron-down': !movieAdvancedOpen, 'fa fa-chevron-up': movieAdvancedOpen}"></i></h6>
|
||||
</div>
|
||||
<div class="collapse" ng-class="{'show': movieAdvancedOpen}">
|
||||
<div class="card-body">
|
||||
<label>Custom Start Time (optional)</label>
|
||||
<div class="form-row mb-3">
|
||||
<div class="col">
|
||||
<input class="form-control form-control-sm" type="text" ng-model="timeInput.seek" placeholder="MM:SS (leave blank for start)">
|
||||
<small class="form-text text-muted">Format: minutes:seconds (e.g. 5:30)</small>
|
||||
<div ng-show="error && error.seekPosition" class="text-danger">{{error.seekPosition}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<label>Custom End Time (optional)
|
||||
<span class="text-danger pull-right">{{error.endPosition}}</span>
|
||||
</label>
|
||||
<div class="form-row mb-3">
|
||||
<div class="col">
|
||||
<input class="form-control form-control-sm" type="text" ng-model="timeInput.end" placeholder="MM:SS (leave blank for end)">
|
||||
<small class="form-text text-muted">Format: minutes:seconds (e.g. 10:45)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="program.type === 'episode'">
|
||||
<label>Show Title
|
||||
@ -94,6 +148,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header" ng-click="episodeAdvancedOpen = !episodeAdvancedOpen" style="cursor: pointer;">
|
||||
<h6 class="mb-0">Advanced Options <i class="float-right" ng-class="{'fa fa-chevron-down': !episodeAdvancedOpen, 'fa fa-chevron-up': episodeAdvancedOpen}"></i></h6>
|
||||
</div>
|
||||
<div class="collapse" ng-class="{'show': episodeAdvancedOpen}">
|
||||
<div class="card-body">
|
||||
<label>Custom Start Time (optional)</label>
|
||||
<div class="form-row mb-3">
|
||||
<div class="col">
|
||||
<input class="form-control form-control-sm" type="text" ng-model="timeInput.seek" placeholder="MM:SS (leave blank for start)">
|
||||
<small class="form-text text-muted">Format: minutes:seconds (e.g. 5:30)</small>
|
||||
<div ng-show="error && error.seekPosition" class="text-danger">{{error.seekPosition}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<label>Custom End Time (optional)
|
||||
<span class="text-danger pull-right">{{error.endPosition}}</span>
|
||||
</label>
|
||||
<div class="form-row mb-3">
|
||||
<div class="col">
|
||||
<input class="form-control form-control-sm" type="text" ng-model="timeInput.end" placeholder="MM:SS (leave blank for end)">
|
||||
<small class="form-text text-muted">Format: minutes:seconds (e.g. 10:45)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@ -210,12 +210,16 @@ module.exports = function (getShowData) {
|
||||
background = "repeating-linear-gradient( " + angle + "deg, " + rgb1 + ", " + rgb1 + " " + w + "px, " + rgb2 + " " + w + "px, " + rgb2 + " " + (w*2) + "px)";
|
||||
|
||||
}
|
||||
|
||||
// Width calculation based on effective duration
|
||||
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;
|
||||
|
||||
let f = interpolate;
|
||||
let w = 15.0;
|
||||
let t = 4*60*60*1000;
|
||||
//let d = Math.log( Math.min(t, program.duration) ) / Math.log(2);
|
||||
//let a = (d * Math.log(2) ) / Math.log(t);
|
||||
let a = ( f(program.duration) *w) / f(t);
|
||||
let a = (f(effectiveDuration) *w) / f(t);
|
||||
a = Math.min( w, Math.max(0.3, a) );
|
||||
b = w - a + 0.01;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user