Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8258a59e27 | ||
|
|
b54c64445e | ||
|
|
0346a67ca9 | ||
|
|
b65eaae38a | ||
|
|
caa99226ae | ||
|
|
682d4365b5 | ||
|
|
ba4ca13564 | ||
|
|
1aee6abdcb | ||
|
|
88982104aa | ||
|
|
c1374d6de7 | ||
|
|
1cd6409401 | ||
|
|
9193d62e07 | ||
|
|
6adf09c41b | ||
|
|
44a2cd9b8b |
@ -6,7 +6,7 @@ COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly
|
RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly
|
||||||
|
|
||||||
FROM jrottenberg/ffmpeg:4.3-ubuntu1804
|
FROM akashisn/ffmpeg:4.4.5
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
ENTRYPOINT [ "./dizquetv" ]
|
ENTRYPOINT [ "./dizquetv" ]
|
||||||
|
|||||||
@ -6,7 +6,7 @@ COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly
|
RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly
|
||||||
|
|
||||||
FROM jrottenberg/ffmpeg:4.3-nvidia1804
|
FROM jrottenberg/ffmpeg:4.4.5-nvidia2204
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
ENTRYPOINT [ "./dizquetv" ]
|
ENTRYPOINT [ "./dizquetv" ]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# dizqueTV 1.5.5
|
# dizqueTV 1.6.1-development
|
||||||
  
|
  
|
||||||
|
|
||||||
Create live TV channel streams from media on your Plex servers.
|
Create live TV channel streams from media on your Plex servers.
|
||||||
|
|||||||
@ -35,5 +35,5 @@ module.exports = {
|
|||||||
// staying active, it checks every 5 seconds
|
// staying active, it checks every 5 seconds
|
||||||
PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000,
|
PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000,
|
||||||
|
|
||||||
VERSION_NAME: "1.5.5"
|
VERSION_NAME: "1.6.1-development"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,10 +163,7 @@ class FillerDB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFillersFromChannel(channel) {
|
async getFillersFromChannel(channel) {
|
||||||
let f = [];
|
|
||||||
if (typeof(channel.fillerCollections) !== 'undefined') {
|
|
||||||
f = channel.fillerContent;
|
|
||||||
}
|
|
||||||
let loadChannelFiller = async(fillerEntry) => {
|
let loadChannelFiller = async(fillerEntry) => {
|
||||||
let content = [];
|
let content = [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -27,6 +27,13 @@ const CHANNEL_CONTEXT_KEYS = [
|
|||||||
module.exports.random = random;
|
module.exports.random = random;
|
||||||
|
|
||||||
function getCurrentProgramAndTimeElapsed(date, channel) {
|
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();
|
let channelStartTime = (new Date(channel.startTime)).getTime();
|
||||||
if (channelStartTime > date) {
|
if (channelStartTime > date) {
|
||||||
let t0 = date;
|
let t0 = date;
|
||||||
@ -44,23 +51,36 @@ function getCurrentProgramAndTimeElapsed(date, channel) {
|
|||||||
let timeElapsed = (date - channelStartTime) % channel.duration
|
let timeElapsed = (date - channelStartTime) % channel.duration
|
||||||
let currentProgramIndex = -1
|
let currentProgramIndex = -1
|
||||||
for (let y = 0, l2 = channel.programs.length; y < l2; y++) {
|
for (let y = 0, l2 = channel.programs.length; y < l2; y++) {
|
||||||
let program = channel.programs[y]
|
let program = channel.programs[y];
|
||||||
if (timeElapsed - program.duration < 0) {
|
// 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
|
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;
|
timeElapsed = 0;
|
||||||
currentProgramIndex = (y + 1) % channel.programs.length;
|
currentProgramIndex = (y + 1) % channel.programs.length;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
timeElapsed -= program.duration
|
timeElapsed -= (end !== null ? end - seek : program.duration - seek);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentProgramIndex === -1)
|
if (currentProgramIndex === -1)
|
||||||
throw new Error("No program found; find algorithm fucked up")
|
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) {
|
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
|
// Helps prevents loosing first few seconds of an episode upon lineup change
|
||||||
let activeProgram = obj.program
|
let activeProgram = obj.program
|
||||||
let beginningOffset = 0;
|
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 = []
|
let lineup = []
|
||||||
|
|
||||||
@ -98,7 +121,7 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
|||||||
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 0) ) {
|
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 0) ) {
|
||||||
special = JSON.parse(JSON.stringify(channel.fallback[0]));
|
special = JSON.parse(JSON.stringify(channel.fallback[0]));
|
||||||
}
|
}
|
||||||
let randomResult = pickRandomWithMaxDuration(programPlayTime, channel, fillers, remaining + (isFirst? (7*24*60*60*1000) : 0) );
|
let randomResult = pickRandomWithMaxDuration(programPlayTime, channel, fillers, remaining + (isFirst? (7*24*60*60*1000) : 0) , isFirst );
|
||||||
filler = randomResult.filler;
|
filler = randomResult.filler;
|
||||||
if (filler == null && (typeof(randomResult.minimumWait) !== undefined) && (remaining > randomResult.minimumWait) ) {
|
if (filler == null && (typeof(randomResult.minimumWait) !== undefined) && (remaining > randomResult.minimumWait) ) {
|
||||||
remaining = randomResult.minimumWait;
|
remaining = randomResult.minimumWait;
|
||||||
@ -114,8 +137,6 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
|||||||
if (isSpecial) {
|
if (isSpecial) {
|
||||||
if (filler.duration > remaining) {
|
if (filler.duration > remaining) {
|
||||||
fillerstart = filler.duration - remaining;
|
fillerstart = filler.duration - remaining;
|
||||||
} else {
|
|
||||||
ffillerstart = 0;
|
|
||||||
}
|
}
|
||||||
} else if(isFirst) {
|
} else if(isFirst) {
|
||||||
fillerstart = Math.max(0, filler.duration - remaining);
|
fillerstart = Math.max(0, filler.duration - remaining);
|
||||||
@ -132,7 +153,7 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
|||||||
file: filler.file,
|
file: filler.file,
|
||||||
ratingKey: filler.ratingKey,
|
ratingKey: filler.ratingKey,
|
||||||
start: fillerstart,
|
start: fillerstart,
|
||||||
streamDuration: Math.max(1, Math.min(filler.duration - fillerstart, remaining) ),
|
streamDuration: Math.max(1, Math.min(filler.duration - fillerstart, remaining + SLACK ) ),
|
||||||
duration: filler.duration,
|
duration: filler.duration,
|
||||||
fillerId: filler.fillerId,
|
fillerId: filler.fillerId,
|
||||||
beginningOffset: beginningOffset,
|
beginningOffset: beginningOffset,
|
||||||
@ -161,6 +182,13 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
|||||||
}
|
}
|
||||||
beginningOffset = Math.max(0, originalTimeElapsed - timeElapsed);
|
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 [{
|
return [{
|
||||||
type: 'program',
|
type: 'program',
|
||||||
title: activeProgram.title,
|
title: activeProgram.title,
|
||||||
@ -168,11 +196,14 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
|||||||
plexFile: activeProgram.plexFile,
|
plexFile: activeProgram.plexFile,
|
||||||
file: activeProgram.file,
|
file: activeProgram.file,
|
||||||
ratingKey: activeProgram.ratingKey,
|
ratingKey: activeProgram.ratingKey,
|
||||||
start: timeElapsed,
|
start: effectiveSeek + effectiveTimeElapsed, // playback should start at seek + elapsed
|
||||||
streamDuration: activeProgram.duration - timeElapsed,
|
streamDuration: effectiveStreamDuration,
|
||||||
beginningOffset: beginningOffset,
|
beginningOffset: beginningOffset,
|
||||||
duration: activeProgram.duration,
|
duration: effectiveDuration,
|
||||||
serverKey: activeProgram.serverKey
|
originalDuration: activeProgram.duration,
|
||||||
|
serverKey: activeProgram.serverKey,
|
||||||
|
seekPosition: effectiveSeek,
|
||||||
|
endPosition: end
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +211,7 @@ function weighedPick(a, total) {
|
|||||||
return random.bool(a, total);
|
return random.bool(a, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration) {
|
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration, isFirst) {
|
||||||
let list = [];
|
let list = [];
|
||||||
for (let i = 0; i < fillers.length; i++) {
|
for (let i = 0; i < fillers.length; i++) {
|
||||||
list = list.concat(fillers[i].content);
|
list = list.concat(fillers[i].content);
|
||||||
@ -190,7 +221,14 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
|||||||
let t0 = (new Date()).getTime();
|
let t0 = (new Date()).getTime();
|
||||||
let minimumWait = 1000000000;
|
let minimumWait = 1000000000;
|
||||||
const D = 7*24*60*60*1000;
|
const D = 7*24*60*60*1000;
|
||||||
const E = 5*60*60*1000;
|
|
||||||
|
let minPick = null;
|
||||||
|
let minPickN = 0;
|
||||||
|
let minPickSet = 0;
|
||||||
|
let minPickPlayTime = t0 + 1;
|
||||||
|
let minPickFillerId = 0;
|
||||||
|
let pickLastPlayed = null;
|
||||||
|
|
||||||
if (typeof(channel.fillerRepeatCooldown) === 'undefined') {
|
if (typeof(channel.fillerRepeatCooldown) === 'undefined') {
|
||||||
channel.fillerRepeatCooldown = 30*60*1000;
|
channel.fillerRepeatCooldown = 30*60*1000;
|
||||||
}
|
}
|
||||||
@ -234,7 +272,7 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
|||||||
minimumWait = Math.min(minimumWait, w);
|
minimumWait = Math.min(minimumWait, w);
|
||||||
}
|
}
|
||||||
timeSince = 0;
|
timeSince = 0;
|
||||||
//30 minutes is too little, don't repeat it at all
|
//Can't pick from this filler list due to cooldown
|
||||||
} else if (!pickedList) {
|
} else if (!pickedList) {
|
||||||
let t1 = channelCache.getFillerLastPlayTime( channel.number, fillers[j].id );
|
let t1 = channelCache.getFillerLastPlayTime( channel.number, fillers[j].id );
|
||||||
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
|
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
|
||||||
@ -260,12 +298,26 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
|||||||
if (timeSince <= 0) {
|
if (timeSince <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let s = norm_s( (timeSince >= E) ? E : timeSince );
|
minPickSet += 1;
|
||||||
|
if (t1 < minPickPlayTime) {
|
||||||
|
// new minimum
|
||||||
|
minPickN = 0;
|
||||||
|
minPickPlayTime = t1;
|
||||||
|
}
|
||||||
|
if (t1 == minPickPlayTime) {
|
||||||
|
// tie
|
||||||
|
minPickN += 1;
|
||||||
|
if ( (minPickN == 1) || weighedPick(1,minPickN)) {
|
||||||
|
minPick = clip;
|
||||||
|
minPickFillerId = fillers[j].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
let d = norm_d( clip.duration);
|
let d = norm_d( clip.duration);
|
||||||
let w = s + d;
|
let w = d;
|
||||||
n += w;
|
n += w;
|
||||||
if (weighedPick(w,n)) {
|
if (weighedPick(w,n)) {
|
||||||
pick1 = clip;
|
pick1 = clip;
|
||||||
|
pickLastPlayed = t1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,6 +327,11 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let pick = pick1;
|
let pick = pick1;
|
||||||
|
let Q = Math.max(30, Math.ceil(10* Math.log(minPickSet) / Math.log(2) ) );
|
||||||
|
if (!isFirst && (minPick != null) && weighedPick(10,Q) ) {
|
||||||
|
pick = minPick;
|
||||||
|
pick.fillerId = minPickFillerId;
|
||||||
|
}
|
||||||
if (pick != null) {
|
if (pick != null) {
|
||||||
pick = JSON.parse( JSON.stringify(pick) );
|
pick = JSON.parse( JSON.stringify(pick) );
|
||||||
pick.fillerId = fillerId;
|
pick.fillerId = fillerId;
|
||||||
@ -288,18 +345,7 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
function norm_d(x) {
|
function norm_d(x) {
|
||||||
x /= 60 * 1000;
|
return 1 + Math.ceil( Math.log(x+1) / Math.log(2) )
|
||||||
if (x >= 3.0) {
|
|
||||||
x = 3.0 + Math.log(x);
|
|
||||||
}
|
|
||||||
let y = 10000 * ( Math.ceil(x * 1000) + 1 );
|
|
||||||
return Math.ceil(y / 1000000) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function norm_s(x) {
|
|
||||||
let y = Math.ceil(x / 600) + 1;
|
|
||||||
y = y*y;
|
|
||||||
return Math.ceil(y / 1000000) + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -64,24 +64,66 @@ class PlexPlayer {
|
|||||||
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
|
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
|
||||||
ffmpeg.setAudioOnly( this.context.audioOnly );
|
ffmpeg.setAudioOnly( this.context.audioOnly );
|
||||||
this.ffmpeg = ffmpeg;
|
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);
|
let stream = await plexTranscoder.getStream(deinterlace);
|
||||||
if (this.killed) {
|
if (this.killed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined;
|
// Calculate parameters differently for direct play vs transcoded mode
|
||||||
//let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : lineupItem.start;
|
let streamDuration;
|
||||||
let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined;
|
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;
|
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();
|
let emitter = new EventEmitter();
|
||||||
//setTimeout( () => {
|
//setTimeout( () => {
|
||||||
|
|||||||
@ -59,8 +59,16 @@ class ChannelService extends events.EventEmitter {
|
|||||||
|
|
||||||
|
|
||||||
function cleanUpProgram(program) {
|
function cleanUpProgram(program) {
|
||||||
delete program.start
|
if (program.startPosition != null && program.startPosition !== '') {
|
||||||
delete program.stop
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
delete program.streams;
|
delete program.streams;
|
||||||
delete program.durationStr;
|
delete program.durationStr;
|
||||||
delete program.commercials;
|
delete program.commercials;
|
||||||
@ -91,12 +99,23 @@ function cleanUpChannel(channel) {
|
|||||||
delete channel.fillerContent;
|
delete channel.fillerContent;
|
||||||
delete channel.filler;
|
delete channel.filler;
|
||||||
channel.fallback = channel.fallback.flatMap( cleanUpProgram );
|
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;
|
channel.duration = 0;
|
||||||
for (let i = 0; i < channel.programs.length; i++) {
|
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;
|
return channel;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const events = require('events')
|
const events = require('events')
|
||||||
const constants = require("../constants");
|
const constants = require("../constants");
|
||||||
const FALLBACK_ICON = "https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png";
|
const FALLBACK_ICON = "https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png";
|
||||||
const throttle = require('./throttle');
|
const throttle = require('./throttle');
|
||||||
|
|
||||||
class TVGuideService extends events.EventEmitter
|
class TVGuideService extends events.EventEmitter
|
||||||
@ -83,7 +83,12 @@ class TVGuideService extends events.EventEmitter
|
|||||||
let arr = new Array( channel.programs.length + 1);
|
let arr = new Array( channel.programs.length + 1);
|
||||||
arr[0] = 0;
|
arr[0] = 0;
|
||||||
for (let i = 0; i < n; i++) {
|
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) {
|
if (d == 0) {
|
||||||
console.log("Found program with duration 0, correcting it");
|
console.log("Found program with duration 0, correcting it");
|
||||||
d = 1;
|
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`);
|
console.log( `Found program in channel ${channel.number} with non-integer duration ${d}, correcting it`);
|
||||||
d = Math.ceil(d);
|
d = Math.ceil(d);
|
||||||
}
|
}
|
||||||
channel.programs[i].duration = d;
|
|
||||||
arr[i+1] = arr[i] + d;
|
arr[i+1] = arr[i] + d;
|
||||||
await this._throttle();
|
await this._throttle();
|
||||||
}
|
}
|
||||||
@ -361,9 +365,69 @@ class TVGuideService extends events.EventEmitter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only merge programs if enabled in channel settings
|
||||||
|
if (channel.mergeAdjacentPrograms === true) {
|
||||||
|
result.programs = this.mergeAdjacentSamePrograms(result.programs);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
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() {
|
async buildItManaged() {
|
||||||
let t0 = this.currentUpdate;
|
let t0 = this.currentUpdate;
|
||||||
let t1 = this.currentLimit;
|
let t1 = this.currentLimit;
|
||||||
@ -580,6 +644,7 @@ function makeEntry(channel, x) {
|
|||||||
icon: icon,
|
icon: icon,
|
||||||
title: title,
|
title: title,
|
||||||
sub: sub,
|
sub: sub,
|
||||||
|
ratingKey: x.program.ratingKey // Add ratingKey to preserve it for merging
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -334,7 +334,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
|
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
|
||||||
u2 = Math.min(u2 , lineupItem.streamDuration);
|
u2 = Math.min(u2 , lineupItem.streamDuration);
|
||||||
}
|
}
|
||||||
lineupItem.streamDuration = Math.min(u2, u);
|
lineupItem.streamDuration = Math.min(lineupItem.streamDuration, Math.min(u2, u) + constants.SLACK);
|
||||||
upperBound = lineupItem.streamDuration;
|
upperBound = lineupItem.streamDuration;
|
||||||
}
|
}
|
||||||
channelCache.recordPlayback( programPlayTimeDB, redirectChannels[i].number, t0, lineupItem );
|
channelCache.recordPlayback( programPlayTimeDB, redirectChannels[i].number, t0, lineupItem );
|
||||||
|
|||||||
@ -964,9 +964,24 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
scope.hasFlex = false;
|
scope.hasFlex = false;
|
||||||
|
|
||||||
for (let i = 0, l = scope.channel.programs.length; i < l; i++) {
|
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].start = new Date(scope.channel.startTime.valueOf() + scope.channel.duration)
|
||||||
scope.channel.programs[i].$index = i;
|
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)
|
scope.channel.programs[i].stop = new Date(scope.channel.startTime.valueOf() + scope.channel.duration)
|
||||||
if (scope.channel.programs[i].isOffline) {
|
if (scope.channel.programs[i].isOffline) {
|
||||||
scope.hasFlex = true;
|
scope.hasFlex = true;
|
||||||
@ -1014,9 +1029,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
} else if (channel.overlayIcon && !validURL(channel.icon)) {
|
} else if (channel.overlayIcon && !validURL(channel.icon)) {
|
||||||
scope.error.icon = "Please enter a valid image URL. Cant overlay an invalid image."
|
scope.error.icon = "Please enter a valid image URL. Cant overlay an invalid image."
|
||||||
scope.error.tab = "basic";
|
scope.error.tab = "basic";
|
||||||
} else if (now < channel.startTime) {
|
|
||||||
scope.error.startTime = "Start time must not be set in the future."
|
|
||||||
scope.error.tab = "programming";
|
|
||||||
} else if (channel.programs.length === 0) {
|
} else if (channel.programs.length === 0) {
|
||||||
scope.error.programs = "No programs have been selected. Select at least one program."
|
scope.error.programs = "No programs have been selected. Select at least one program."
|
||||||
scope.error.tab = "programming";
|
scope.error.tab = "programming";
|
||||||
|
|||||||
@ -9,29 +9,138 @@ module.exports = function ($timeout) {
|
|||||||
onDone: "=onDone"
|
onDone: "=onDone"
|
||||||
},
|
},
|
||||||
link: function (scope, element, attrs) {
|
link: function (scope, element, attrs) {
|
||||||
scope.finished = (prog) => {
|
// Conversion functions remain the same (using NaN for invalid)
|
||||||
if (prog.title === "")
|
scope.msToTimeString = function(ms) {
|
||||||
scope.error = { title: 'You must set a program title.' }
|
if (typeof ms !== 'number' || isNaN(ms) || ms < 0) { return ''; }
|
||||||
else if (prog.type === "episode" && prog.showTitle == "")
|
let totalS = Math.floor(ms / 1000);
|
||||||
scope.error = { showTitle: 'You must set a show title when the program type is an episode.' }
|
let s = totalS % 60;
|
||||||
else if (prog.type === "episode" && (prog.season == null))
|
let m = Math.floor(totalS / 60);
|
||||||
scope.error = { season: 'You must set a season number when the program type is an episode.' }
|
return m + ":" + ( (s < 10) ? ("0" + s) : s );
|
||||||
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' }
|
|
||||||
|
|
||||||
if (scope.error != null) {
|
scope.timeStringToMs = function(timeString) {
|
||||||
$timeout(() => {
|
if (timeString == null || timeString.trim() === '') { return 0; } // Empty is 0ms
|
||||||
scope.error = null
|
let parts = timeString.split(':');
|
||||||
}, 3500)
|
if (parts.length !== 2) { return NaN; } // Invalid format
|
||||||
return
|
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
|
||||||
|
|
||||||
|
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)))
|
// --- Standard Validation (on original prog object) ---
|
||||||
scope.program = null
|
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>
|
<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>
|
||||||
|
|
||||||
|
<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>
|
<br></br>
|
||||||
<div class='form-group' ng-show='! channel.stealth'>
|
<div class='form-group' ng-show='! channel.stealth'>
|
||||||
<label class='form-label' >Placeholder program title:</label>
|
<label class='form-label' >Placeholder program title:</label>
|
||||||
|
|||||||
@ -30,6 +30,33 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
|
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
|
||||||
</div>
|
</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>
|
||||||
<div ng-if="program.type === 'movie'">
|
<div ng-if="program.type === 'movie'">
|
||||||
<label>Movie Title
|
<label>Movie Title
|
||||||
@ -48,6 +75,33 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
|
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
|
||||||
</div>
|
</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>
|
||||||
<div ng-if="program.type === 'episode'">
|
<div ng-if="program.type === 'episode'">
|
||||||
<label>Show Title
|
<label>Show Title
|
||||||
@ -94,6 +148,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="modal-footer">
|
<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)";
|
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 f = interpolate;
|
||||||
let w = 15.0;
|
let w = 15.0;
|
||||||
let t = 4*60*60*1000;
|
let t = 4*60*60*1000;
|
||||||
//let d = Math.log( Math.min(t, program.duration) ) / Math.log(2);
|
let a = (f(effectiveDuration) *w) / f(t);
|
||||||
//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) );
|
a = Math.min( w, Math.max(0.3, a) );
|
||||||
b = w - a + 0.01;
|
b = w - a + 0.01;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user