Merge remote-tracking branch 'origin/dev/1.5.x' into dev/1.6.x

This commit is contained in:
vexorian 2025-12-01 11:30:20 -04:00
commit caa99226ae
12 changed files with 64 additions and 444 deletions

View File

@ -3,7 +3,7 @@ name: Development Binaries
on:
push:
branches:
- dev/1.5.x
- dev/1.6.x
jobs:
binaries:

View File

@ -3,7 +3,7 @@ name: Development Tag
on:
push:
branches:
- dev/1.5.x
- dev/1.6.x
jobs:
docker:

View File

@ -7,8 +7,8 @@
* [ ] I have read the code of conduct.
* [ ] I am submitting to the correct base branch
<!--
* Bug fixes must go to `dev/1.4.x`.
* New features must go to `dev/1.5.x`.
* Bug fixes must go to `dev/1.5.x`.
* New features must go to `dev/1.6.x`.
-->
### Changes that modify the db structure

View File

@ -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 = []
@ -182,29 +159,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) {

View File

@ -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( () => {

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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;

View File

@ -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
}
}
};

View File

@ -727,15 +727,6 @@
<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>

View File

@ -30,33 +30,6 @@
<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
@ -75,33 +48,6 @@
<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
@ -148,33 +94,6 @@
</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">

View File

@ -210,16 +210,12 @@ 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 a = (f(effectiveDuration) *w) / f(t);
//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);
a = Math.min( w, Math.max(0.3, a) );
b = w - a + 0.01;