Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17711fcb76 | ||
|
|
4df39ed177 | ||
|
|
1175840b29 | ||
|
|
9b5fef01b0 | ||
|
|
647c401370 | ||
|
|
2541e42513 | ||
|
|
caf3b3b72c | ||
|
|
7daad9e33f | ||
|
|
4cdf87121a | ||
|
|
c413580b00 | ||
|
|
8258a59e27 | ||
|
|
b54c64445e | ||
|
|
0346a67ca9 | ||
|
|
b65eaae38a | ||
|
|
caa99226ae | ||
|
|
682d4365b5 | ||
|
|
ba4ca13564 | ||
|
|
483ace42d2 | ||
|
|
1aee6abdcb | ||
|
|
88982104aa | ||
|
|
c1374d6de7 | ||
|
|
1cd6409401 | ||
|
|
9193d62e07 | ||
|
|
6adf09c41b | ||
|
|
44a2cd9b8b |
@ -2,64 +2,73 @@
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We pledge to make our community welcoming, safe, and equitable for all.
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
## Encouraged Behaviors
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
|
||||
2. Engaging **kindly and honestly** with others.
|
||||
3. Respecting **different viewpoints** and experiences.
|
||||
4. **Taking responsibility** for our actions and contributions.
|
||||
5. Gracefully giving and accepting **constructive feedback**.
|
||||
6. Committing to **repairing harm** when it occurs.
|
||||
7. Behaving in other ways that promote and sustain the **well-being of our community**.
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
## Restricted Behaviors
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
|
||||
## Scope
|
||||
|
||||
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
|
||||
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
|
||||
3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits.
|
||||
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
|
||||
5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
|
||||
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
|
||||
7. Behaving in other ways that **threaten the well-being** of our community.
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
### Other Restrictions
|
||||
## Enforcement
|
||||
|
||||
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
|
||||
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
|
||||
3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
|
||||
4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at vexorian@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
## 'AI' policy
|
||||
|
||||
There are ways in which a LLM-based tool can be helpful such as when searching the web or for learning. More so, tools like github and google themselves are being modified to railroad users into using LLM-based tools. It'd be really impractical and neigh-impossible to ban 'AI' altogether from being used during the development of a contribution and that's not really the purpose of this policy.
|
||||
|
||||
HOWEVER, from a legal standpoint, it is too difficult to know the origin of LLM-generated code so there are risks of it infringing on copyright. And from a pragmatic stand point, we want to be able to trust the quality of the code. That is to say, we do not want contributions where the bulk of the code was AI-generated or or where the contributors themselves do not understand the code being pushed. This is also in relation to items 4 and 5 of the encouraged behaviors. We want contributors that can vouch for the code they contribute and can receive and act on constructive feedback to it.
|
||||
|
||||
Note that this is only a policy affecting Pull Requests to this project. This project is released under a permissive FOSS licence, so there's nothing really that can stop forks from having a different policy on this and any individual is allowed to create such a fork should they want to.
|
||||
|
||||
## Reporting an Issue
|
||||
|
||||
Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
|
||||
|
||||
When an incident does occur, it is important to report it promptly. To report a possible violation, may be
|
||||
reported by contacting the project team at vexorian@gmail.com
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 3.0,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html . The AI policy is a modification.
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
|
||||
@ -6,9 +6,10 @@ COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
|
||||
COPY . .
|
||||
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
|
||||
WORKDIR /home/node/app
|
||||
ENTRYPOINT [ "./dizquetv" ]
|
||||
ENV DIZQUETV_FFMPEG_PATH=/usr/bin/ffmpeg
|
||||
COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/
|
||||
RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg
|
||||
|
||||
@ -6,9 +6,10 @@ COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
|
||||
COPY . .
|
||||
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
|
||||
WORKDIR /home/node/app
|
||||
ENTRYPOINT [ "./dizquetv" ]
|
||||
ENV DIZQUETV_FFMPEG_PATH=/usr/bin/ffmpeg
|
||||
COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/
|
||||
RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# dizqueTV 1.5.5
|
||||
# dizqueTV 1.7.1-development
|
||||
  
|
||||
|
||||
Create live TV channel streams from media on your Plex servers.
|
||||
|
||||
56
index.js
56
index.js
@ -17,6 +17,7 @@ const HDHR = require('./src/hdhr')
|
||||
const FileCacheService = require('./src/services/file-cache-service');
|
||||
const CacheImageService = require('./src/services/cache-image-service');
|
||||
const ChannelService = require("./src/services/channel-service");
|
||||
const FillerService = require("./src/services/filler-service");
|
||||
|
||||
const xmltv = require('./src/xmltv')
|
||||
const Plex = require('./src/plex');
|
||||
@ -32,6 +33,9 @@ const ProgrammingService = require("./src/services/programming-service");
|
||||
const ActiveChannelService = require('./src/services/active-channel-service')
|
||||
const ProgramPlayTimeDB = require('./src/dao/program-play-time-db')
|
||||
const FfmpegSettingsService = require('./src/services/ffmpeg-settings-service')
|
||||
const PlexProxyService = require('./src/services/plex-proxy-service')
|
||||
const PlexServerDB = require('./src/dao/plex-server-db');
|
||||
const FFMPEGInfo = require('./src/ffmpeg-info');
|
||||
|
||||
const onShutdown = require("node-graceful-shutdown").onShutdown;
|
||||
|
||||
@ -51,16 +55,12 @@ if (NODE < 12) {
|
||||
console.error(`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`);
|
||||
}
|
||||
|
||||
unlockPath = false;
|
||||
for (let i = 0, l = process.argv.length; i < l; i++) {
|
||||
if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l)
|
||||
process.env.PORT = process.argv[i + 1]
|
||||
if ((process.argv[i] === "-d" || process.argv[i] === "--database") && i + 1 !== l)
|
||||
process.env.DATABASE = process.argv[i + 1]
|
||||
|
||||
if (process.argv[i] === "--unlock") {
|
||||
unlockPath = true;
|
||||
}
|
||||
}
|
||||
|
||||
process.env.DATABASE = process.env.DATABASE || path.join(".", ".dizquetv")
|
||||
@ -92,6 +92,8 @@ if(!fs.existsSync(path.join(process.env.DATABASE, 'cache','images'))) {
|
||||
fs.mkdirSync(path.join(process.env.DATABASE, 'cache','images'))
|
||||
}
|
||||
|
||||
const ffmpegInfo = new FFMPEGInfo(process.env, process.env.DATABASE);
|
||||
ffmpegInfo.initialize();
|
||||
|
||||
channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') );
|
||||
|
||||
@ -103,10 +105,13 @@ initDB(db, channelDB)
|
||||
|
||||
channelService = new ChannelService(channelDB);
|
||||
|
||||
fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelService );
|
||||
let fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') );
|
||||
customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') );
|
||||
let programPlayTimeDB = new ProgramPlayTimeDB( path.join(process.env.DATABASE, 'play-cache') );
|
||||
let ffmpegSettingsService = new FfmpegSettingsService(db, unlockPath);
|
||||
let ffmpegSettingsService = new FfmpegSettingsService(db);
|
||||
let plexServerDB = new PlexServerDB(channelService, fillerDB, customShowDB, db);
|
||||
let plexProxyService = new PlexProxyService(plexServerDB);
|
||||
|
||||
|
||||
async function initializeProgramPlayTimeDB() {
|
||||
try {
|
||||
@ -130,6 +135,9 @@ activeChannelService = new ActiveChannelService(onDemandService, channelService)
|
||||
|
||||
eventService = new EventService();
|
||||
|
||||
let fillerService = new FillerService(fillerDB, plexProxyService,
|
||||
channelService);
|
||||
|
||||
i18next
|
||||
.use(i18nextBackend)
|
||||
.use(i18nextMiddleware.LanguageDetector)
|
||||
@ -251,6 +259,38 @@ channelService.on("channel-update", (data) => {
|
||||
|
||||
let hdhr = HDHR(db, channelDB)
|
||||
let app = express()
|
||||
|
||||
const responseInterceptor = (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
|
||||
let t0 = new Date().getTime();
|
||||
|
||||
const originalSend = res.send;
|
||||
let responseSent = false;
|
||||
console.log(`${req.method} ${req.url} ...`);
|
||||
if (req.method === "GET" && req.url.includes("images/uploads") ) {
|
||||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||
}
|
||||
|
||||
res.send = function (body) {
|
||||
|
||||
if (!responseSent) {
|
||||
let t1 = new Date().getTime();
|
||||
let dt = t1 - t0;
|
||||
console.log(`${req.method} ${req.url} ${res.statusCode} in ${dt}ms`);
|
||||
responseSent = true;
|
||||
}
|
||||
|
||||
return originalSend.call(this, body);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
app.use(responseInterceptor);
|
||||
|
||||
eventService.setup(app);
|
||||
|
||||
app.use(
|
||||
@ -290,12 +330,12 @@ app.use('/favicon.svg', express.static(
|
||||
app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css')))
|
||||
|
||||
// API Routers
|
||||
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService))
|
||||
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo, fillerService))
|
||||
app.use('/api/cache/images', cacheImageService.apiRouters())
|
||||
app.use('/' + fontAwesome, express.static(path.join(process.env.DATABASE, fontAwesome)))
|
||||
app.use('/' + bootstrap, express.static(path.join(process.env.DATABASE, bootstrap)))
|
||||
|
||||
app.use(video.router( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ))
|
||||
app.use(video.router( channelService, fillerService, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ))
|
||||
app.use(hdhr.router)
|
||||
app.listen(process.env.PORT, () => {
|
||||
console.log(`HTTP server running on port: http://*:${process.env.PORT}`)
|
||||
|
||||
@ -19,7 +19,3 @@
|
||||
|
||||
* [ ] I understand that the feature may not be accepted if it doesn't fit the upstream app's planned design direction. But that in this case I am encouraged to share this as an available modification other users can use if they want.
|
||||
|
||||
### Code Standards
|
||||
|
||||
* [ ] I understand the code being contributed and it's purpose. <!-- Please read CODE_OF_CONDUCT for more info. -->
|
||||
|
||||
|
||||
38
src/api.js
38
src/api.js
@ -4,8 +4,6 @@ const path = require('path')
|
||||
const fs = require('fs')
|
||||
const constants = require('./constants');
|
||||
const JSONStream = require('JSONStream');
|
||||
const FFMPEGInfo = require('./ffmpeg-info');
|
||||
const PlexServerDB = require('./dao/plex-server-db');
|
||||
const Plex = require("./plex.js");
|
||||
|
||||
const timeSlotsService = require('./services/time-slots-service');
|
||||
@ -24,15 +22,13 @@ function safeString(object) {
|
||||
}
|
||||
|
||||
module.exports = { router: api }
|
||||
function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService ) {
|
||||
function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo, fillerService ) {
|
||||
let m3uService = _m3uService;
|
||||
const router = express.Router()
|
||||
const plexServerDB = new PlexServerDB(channelService, fillerDB, customShowDB, db);
|
||||
|
||||
router.get('/api/version', async (req, res) => {
|
||||
try {
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0];
|
||||
let v = await (new FFMPEGInfo(ffmpegSettings)).getVersion();
|
||||
let v = await ffmpegInfo.getVersion();
|
||||
res.send( {
|
||||
"dizquetv" : constants.VERSION_NAME,
|
||||
"ffmpeg" : v,
|
||||
@ -216,7 +212,15 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/api/plex-server/:serverName64/:path(*)', async (req, res) => {
|
||||
try {
|
||||
let result = await plexProxyService.get(req.params.serverName64, req.params.path);
|
||||
res.status(200).send(result);
|
||||
} catch (err) {
|
||||
console.error("Could not use plex proxy.", err);
|
||||
res.status(404).send("Could not call plex server.");
|
||||
}
|
||||
});
|
||||
|
||||
// Channels
|
||||
router.get('/api/channels', async (req, res) => {
|
||||
@ -415,7 +419,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
||||
if (typeof(id) === 'undefined') {
|
||||
return res.status(400).send("Missing id");
|
||||
}
|
||||
await fillerDB.saveFiller(id, req.body );
|
||||
await fillerService.saveFiller(id, req.body );
|
||||
return res.status(204).send({});
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
@ -424,7 +428,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
||||
})
|
||||
router.put('/api/filler', async (req, res) => {
|
||||
try {
|
||||
let uuid = await fillerDB.createFiller(req.body );
|
||||
let uuid = await fillerService.createFiller(req.body );
|
||||
return res.status(201).send({id: uuid});
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
@ -437,7 +441,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
||||
if (typeof(id) === 'undefined') {
|
||||
return res.status(400).send("Missing id");
|
||||
}
|
||||
await fillerDB.deleteFiller(id);
|
||||
await fillerService.deleteFiller(id);
|
||||
return res.status(204).send({});
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
@ -451,7 +455,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
||||
if (typeof(id) === 'undefined') {
|
||||
return res.status(400).send("Missing id");
|
||||
}
|
||||
let channels = await fillerDB.getFillerChannels(id);
|
||||
let channels = await fillerService.getFillerChannels(id);
|
||||
if (channels == null) {
|
||||
return res.status(404).send("Filler not found");
|
||||
}
|
||||
@ -609,6 +613,18 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
||||
|
||||
})
|
||||
|
||||
router.get('/api/ffmpeg-info', async (req, res) => {
|
||||
try {
|
||||
let ffmpeg = await ffmpegInfo.getPath();
|
||||
let obj = { ffmpegPath: ffmpeg }
|
||||
res.send(obj)
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// PLEX SETTINGS
|
||||
router.get('/api/plex-settings', (req, res) => {
|
||||
try {
|
||||
|
||||
@ -35,5 +35,5 @@ module.exports = {
|
||||
// staying active, it checks every 5 seconds
|
||||
PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000,
|
||||
|
||||
VERSION_NAME: "1.5.5"
|
||||
VERSION_NAME: "1.7.1-development"
|
||||
}
|
||||
|
||||
@ -4,12 +4,9 @@ let fs = require('fs');
|
||||
|
||||
class FillerDB {
|
||||
|
||||
constructor(folder, channelService) {
|
||||
constructor(folder) {
|
||||
this.folder = folder;
|
||||
this.cache = {};
|
||||
this.channelService = channelService;
|
||||
|
||||
|
||||
}
|
||||
|
||||
async $loadFiller(id) {
|
||||
@ -77,40 +74,8 @@ class FillerDB {
|
||||
return id;
|
||||
}
|
||||
|
||||
async getFillerChannels(id) {
|
||||
let numbers = await this.channelService.getAllChannelNumbers();
|
||||
let channels = [];
|
||||
await Promise.all( numbers.map( async(number) => {
|
||||
let ch = await this.channelService.getChannel(number);
|
||||
let name = ch.name;
|
||||
let fillerCollections = ch.fillerCollections;
|
||||
for (let i = 0 ; i < fillerCollections.length; i++) {
|
||||
if (fillerCollections[i].id === id) {
|
||||
channels.push( {
|
||||
number: number,
|
||||
name : name,
|
||||
} );
|
||||
break;
|
||||
}
|
||||
}
|
||||
ch = null;
|
||||
|
||||
} ) );
|
||||
return channels;
|
||||
}
|
||||
|
||||
async deleteFiller(id) {
|
||||
try {
|
||||
let channels = await this.getFillerChannels(id);
|
||||
await Promise.all( channels.map( async(channel) => {
|
||||
console.log(`Updating channel ${channel.number} , remove filler: ${id}`);
|
||||
let json = await channelService.getChannel(channel.number);
|
||||
json.fillerCollections = json.fillerCollections.filter( (col) => {
|
||||
return col.id != id;
|
||||
} );
|
||||
await this.channelService.saveChannel( channel.number, json );
|
||||
} ) );
|
||||
|
||||
let f = path.join(this.folder, `${id}.json` );
|
||||
await new Promise( (resolve, reject) => {
|
||||
fs.unlink(f, function (err) {
|
||||
@ -162,30 +127,6 @@ class FillerDB {
|
||||
} );
|
||||
}
|
||||
|
||||
async getFillersFromChannel(channel) {
|
||||
let f = [];
|
||||
if (typeof(channel.fillerCollections) !== 'undefined') {
|
||||
f = channel.fillerContent;
|
||||
}
|
||||
let loadChannelFiller = async(fillerEntry) => {
|
||||
let content = [];
|
||||
try {
|
||||
let filler = await this.getFiller(fillerEntry.id);
|
||||
content = filler.content;
|
||||
} catch(e) {
|
||||
console.error(`Channel #${channel.number} - ${channel.name} references an unattainable filler id: ${fillerEntry.id}`);
|
||||
}
|
||||
return {
|
||||
id: fillerEntry.id,
|
||||
content: content,
|
||||
weight: fillerEntry.weight,
|
||||
cooldown: fillerEntry.cooldown,
|
||||
}
|
||||
};
|
||||
return await Promise.all(
|
||||
channel.fillerCollections.map(loadChannelFiller)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -15,6 +15,17 @@ class PlexServerDB
|
||||
this.showDB = showDB;
|
||||
}
|
||||
|
||||
async getPlexServerByName(name) {
|
||||
let servers = this.db['plex-servers'].find()
|
||||
let server = servers.sort( (a,b) => { return a.index - b.index } )
|
||||
.filter( (server) => name === server.name )
|
||||
[0];
|
||||
if (typeof(server) === "undefined") {
|
||||
return null;
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
async fixupAllChannels(name, newServer) {
|
||||
let channelNumbers = await this.channelService.getAllChannelNumbers();
|
||||
let report = await Promise.all( channelNumbers.map( async (i) => {
|
||||
|
||||
@ -20,8 +20,7 @@
|
||||
const path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
const TARGET_VERSION = 805;
|
||||
const DAY_MS = 1000 * 60 * 60 * 24;
|
||||
const TARGET_VERSION = 1000;
|
||||
|
||||
const STEPS = [
|
||||
// [v, v2, x] : if the current version is v, call x(db), and version becomes v2
|
||||
@ -44,8 +43,10 @@ const STEPS = [
|
||||
[ 800, 801, (db) => addImageCache(db) ],
|
||||
[ 801, 802, () => addGroupTitle() ],
|
||||
[ 802, 803, () => fixNonIntegerDurations() ],
|
||||
[ 803, 805, (db) => addFFMpegLock(db) ],
|
||||
[ 804, 805, (db) => addFFMpegLock(db) ],
|
||||
[ 803, 900, (db) => fixFFMpegPathSetting(db) ],
|
||||
[ 804, 900, (db) => fixFFMpegPathSetting(db) ],
|
||||
[ 805, 900, (db) => fixFFMpegPathSetting(db) ],
|
||||
[ 900, 1000, () => fixFillerModes() ],
|
||||
]
|
||||
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
@ -75,7 +76,7 @@ function appNameChange(db) {
|
||||
|
||||
function basicDB(db) {
|
||||
//this one should either try recovering the db from a very old version
|
||||
//or buildl a completely empty db at version 0
|
||||
//or build a completely empty db at version 0
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()
|
||||
let plexSettings = db['plex-settings'].find()
|
||||
|
||||
@ -386,8 +387,6 @@ function ffmpeg() {
|
||||
return {
|
||||
//How default ffmpeg settings should look
|
||||
configVersion: 5,
|
||||
ffmpegPath: "/usr/bin/ffmpeg",
|
||||
ffmpegPathLockDate: new Date().getTime() + DAY_MS,
|
||||
threads: 4,
|
||||
concatMuxDelay: "0",
|
||||
logFfmpeg: false,
|
||||
@ -681,6 +680,25 @@ function extractFillersFromChannels() {
|
||||
|
||||
}
|
||||
|
||||
function fixFillerModes() {
|
||||
console.log("Fixing filler modes...");
|
||||
let fillers = path.join(process.env.DATABASE, 'filler');
|
||||
let fillerFiles = fs.readdirSync(fillers);
|
||||
|
||||
for (let i = 0; i < fillerFiles.length; i++) {
|
||||
if (path.extname( fillerFiles[i] ) === '.json') {
|
||||
console.log("Migrating filler : " + fillerFiles[i] +"..." );
|
||||
let fillerPath = path.join(fillers, fillerFiles[i]);
|
||||
let filler = JSON.parse(fs.readFileSync(fillerPath, 'utf-8'));
|
||||
if ( typeof(filler.mode) !== "string" ) {
|
||||
filler.mode = "custom";
|
||||
}
|
||||
fs.writeFileSync( fillerPath, JSON.stringify(filler), 'utf-8');
|
||||
}
|
||||
}
|
||||
console.log("Done fixing filler modes.");
|
||||
}
|
||||
|
||||
function addFPS(db) {
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0];
|
||||
let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json');
|
||||
@ -769,19 +787,23 @@ function addScalingAlgorithm(db) {
|
||||
fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) );
|
||||
}
|
||||
|
||||
function addFFMpegLock(db) {
|
||||
function fixFFMpegPathSetting(db) {
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0];
|
||||
let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json');
|
||||
if ( typeof(ffmpegSettings.ffmpegPathLockDate) === 'undefined' || ffmpegSettings.ffmpegPathLockDate == null ) {
|
||||
let f2 = path.join(process.env.DATABASE, 'ffmpeg-path.json');
|
||||
delete ffmpegSettings.ffmpegPathLockDate;
|
||||
let fpath = ffmpegSettings.ffmpegPath;
|
||||
delete ffmpegSettings.ffmpegPath;
|
||||
|
||||
console.log("Adding ffmpeg lock. For your security it will not be possible to modify the ffmpeg path using the UI anymore unless you launch dizquetv by following special instructions..");
|
||||
// We are migrating an existing db that had a ffmpeg path. Make sure
|
||||
// it's already locked.
|
||||
ffmpegSettings.ffmpegPathLockDate = new Date().getTime() - 2 * DAY_MS;
|
||||
if (typeof(fpath) === "string" ) {
|
||||
console.log(`Found existing setting ffmpegPath=${fpath}, creating setting file (This file will get ignored if you are already setting an environment variable (the docker images do that)).`);
|
||||
let pathJson = { ffmpegPath : fpath };
|
||||
fs.writeFileSync( f2, JSON.stringify( pathJson ) );
|
||||
}
|
||||
fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) );
|
||||
}
|
||||
|
||||
|
||||
function moveBackup(path) {
|
||||
if (fs.existsSync(`${process.env.DATABASE}${path}`) ) {
|
||||
let i = 0;
|
||||
|
||||
@ -1,13 +1,98 @@
|
||||
const exec = require('child_process').exec;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class FFMPEGInfo {
|
||||
constructor(opts) {
|
||||
this.ffmpegPath = opts.ffmpegPath
|
||||
|
||||
constructor(env, dbPath) {
|
||||
this.initialized = false;
|
||||
this.env = env;
|
||||
this.dbPath = dbPath;
|
||||
this.ffmpegPath = null;
|
||||
this.origin = "Not found";
|
||||
}
|
||||
async getVersion() {
|
||||
|
||||
async initialize() {
|
||||
let selectedPath = null;
|
||||
if (typeof(this.env.DIZQUETV_FFMPEG_PATH) === "string") {
|
||||
selectedPath = this.env.DIZQUETV_FFMPEG_PATH;
|
||||
this.origin = "env.DIZQUETV_FFMPEG_PATH";
|
||||
} else {
|
||||
selectedPath = await this.getPathFromFile(this.dbPath, 'ffmpeg-path.json');
|
||||
this.origin = "ffmpeg-path.json";
|
||||
}
|
||||
if (selectedPath == null) {
|
||||
//windows Path environment var
|
||||
let paths = this.env.Path;
|
||||
if (typeof(paths) === "string") {
|
||||
let maybe = paths.split(";").filter(
|
||||
(str) => str.contains("ffmpeg" )
|
||||
)[0];
|
||||
if (typeof(maybe) === "string") {
|
||||
selectedPath = path.join(maybe, "ffmpeg.exe");
|
||||
this.origin = "Widnows Env. Path";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedPath == null) {
|
||||
//Default install path for ffmpeg in n*x OSes.
|
||||
// if someone has built ffmpeg manually or wants an alternate
|
||||
// path, they are most likely capable of configuring it manually.
|
||||
selectedPath = "/usr/bin/ffmpeg";
|
||||
this.origin = "Default";
|
||||
}
|
||||
|
||||
if (selectedPath != null) {
|
||||
let version = await this.checkVersion(selectedPath);
|
||||
if (version == null) {
|
||||
selectedPath = null;
|
||||
} else {
|
||||
console.log(`FFmpeg found: ${selectedPath} from: ${this.origin}. version: ${version}`);
|
||||
this.ffmpegPath = selectedPath;
|
||||
}
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
}
|
||||
|
||||
async getPath() {
|
||||
if (! this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
return this.ffmpegPath;
|
||||
}
|
||||
|
||||
async getPathFromFile(folder, fileName) {
|
||||
let f = path.join(folder, fileName);
|
||||
try {
|
||||
let json = await new Promise( (resolve, reject) => {
|
||||
fs.readFile(f, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
try {
|
||||
resolve( JSON.parse(data) )
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
});
|
||||
let ffmpeg = json["ffmpegPath"];
|
||||
if (typeof(ffmpeg) === "string") {
|
||||
return ffmpeg;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async checkVersion(ffmpegPath) {
|
||||
try {
|
||||
let s = await new Promise( (resolve, reject) => {
|
||||
exec( `"${this.ffmpegPath}" -version`, function(error, stdout, stderr){
|
||||
exec( `"${ffmpegPath}" -version`, function(error, stdout, stderr){
|
||||
if (error !== null) {
|
||||
reject(error);
|
||||
} else {
|
||||
@ -23,7 +108,20 @@ class FFMPEGInfo {
|
||||
return m[1];
|
||||
} catch (err) {
|
||||
console.error("Error getting ffmpeg version", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getVersion() {
|
||||
if (! this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
let version = await this.checkVersion(this.ffmpegPath);
|
||||
if (version == null) {
|
||||
return "Error";
|
||||
} else {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ const REALLY_RIDICULOUSLY_HIGH_FPS_FOR_DIZQUETVS_USECASE = 120;
|
||||
class FFMPEG extends events.EventEmitter {
|
||||
constructor(opts, channel) {
|
||||
super()
|
||||
this.ffmpegPath = opts.ffmpegPath;
|
||||
this.opts = opts;
|
||||
this.errorPicturePath = `http://localhost:${process.env.PORT}/images/generic-error-screen.png`;
|
||||
this.ffmpegName = "unnamed ffmpeg";
|
||||
@ -22,7 +23,6 @@ class FFMPEG extends events.EventEmitter {
|
||||
this.opts.maxFPS = REALLY_RIDICULOUSLY_HIGH_FPS_FOR_DIZQUETVS_USECASE;
|
||||
}
|
||||
this.channel = channel
|
||||
this.ffmpegPath = opts.ffmpegPath
|
||||
|
||||
let resString = opts.targetResolution;
|
||||
if (
|
||||
@ -322,7 +322,7 @@ class FFMPEG extends events.EventEmitter {
|
||||
if (watermark.animated === true) {
|
||||
ffmpegArgs.push('-ignore_loop', '0');
|
||||
}
|
||||
ffmpegArgs.push(`-i`, `${watermark.url}` );
|
||||
ffmpegArgs.push(`-i`, `async:cache:${watermark.url}` );
|
||||
overlayFile = inputFiles++;
|
||||
this.ensureResolution = true;
|
||||
}
|
||||
@ -601,7 +601,7 @@ class FFMPEG extends events.EventEmitter {
|
||||
return;
|
||||
}
|
||||
if (! this.sentData) {
|
||||
this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` })
|
||||
this.emit('error', { code: code, cmd: `${this.ffmpegPath} ${ffmpegArgs.join(' ')}` })
|
||||
}
|
||||
console.log( `${this.ffmpegName} exited with code 255.` );
|
||||
this.emit('close', code)
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -98,7 +121,7 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
||||
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 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;
|
||||
if (filler == null && (typeof(randomResult.minimumWait) !== undefined) && (remaining > randomResult.minimumWait) ) {
|
||||
remaining = randomResult.minimumWait;
|
||||
@ -114,8 +137,6 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
||||
if (isSpecial) {
|
||||
if (filler.duration > remaining) {
|
||||
fillerstart = filler.duration - remaining;
|
||||
} else {
|
||||
ffillerstart = 0;
|
||||
}
|
||||
} else if(isFirst) {
|
||||
fillerstart = Math.max(0, filler.duration - remaining);
|
||||
@ -132,7 +153,7 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
||||
file: filler.file,
|
||||
ratingKey: filler.ratingKey,
|
||||
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,
|
||||
fillerId: filler.fillerId,
|
||||
beginningOffset: beginningOffset,
|
||||
@ -161,26 +182,36 @@ 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) {
|
||||
return random.bool(a, total);
|
||||
}
|
||||
|
||||
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration) {
|
||||
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration, isFirst) {
|
||||
let list = [];
|
||||
for (let i = 0; i < fillers.length; i++) {
|
||||
list = list.concat(fillers[i].content);
|
||||
@ -190,7 +221,14 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
||||
let t0 = (new Date()).getTime();
|
||||
let minimumWait = 1000000000;
|
||||
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') {
|
||||
channel.fillerRepeatCooldown = 30*60*1000;
|
||||
}
|
||||
@ -234,7 +272,7 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
||||
minimumWait = Math.min(minimumWait, w);
|
||||
}
|
||||
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) {
|
||||
let t1 = channelCache.getFillerLastPlayTime( channel.number, fillers[j].id );
|
||||
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
|
||||
@ -260,12 +298,26 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
||||
if (timeSince <= 0) {
|
||||
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 w = s + d;
|
||||
let w = d;
|
||||
n += w;
|
||||
if (weighedPick(w,n)) {
|
||||
pick1 = clip;
|
||||
pickLastPlayed = t1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -275,6 +327,11 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
||||
}
|
||||
}
|
||||
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) {
|
||||
pick = JSON.parse( JSON.stringify(pick) );
|
||||
pick.fillerId = fillerId;
|
||||
@ -288,18 +345,7 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
||||
}
|
||||
|
||||
function norm_d(x) {
|
||||
x /= 60 * 1000;
|
||||
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;
|
||||
return 1 + Math.ceil( Math.log(x+1) / Math.log(2) )
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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,16 @@ 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);
|
||||
}
|
||||
|
||||
delete program.streams;
|
||||
delete program.durationStr;
|
||||
delete program.commercials;
|
||||
@ -91,12 +99,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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,50 +1,22 @@
|
||||
const databaseMigration = require('../database-migration');
|
||||
const DAY_MS = 1000 * 60 * 60 * 24;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
class FfmpegSettingsService {
|
||||
constructor(db, unlock) {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
if (unlock) {
|
||||
this.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
get() {
|
||||
let ffmpeg = this.getCurrentState();
|
||||
if (isLocked(ffmpeg)) {
|
||||
ffmpeg.lock = true;
|
||||
}
|
||||
// Hid this info from the API
|
||||
delete ffmpeg.ffmpegPathLockDate;
|
||||
return ffmpeg;
|
||||
}
|
||||
|
||||
unlock() {
|
||||
let ffmpeg = this.getCurrentState();
|
||||
console.log("ffmpeg path UI unlocked for another day...");
|
||||
ffmpeg.ffmpegPathLockDate = new Date().getTime() + DAY_MS;
|
||||
this.db['ffmpeg-settings'].update({ _id: ffmpeg._id }, ffmpeg)
|
||||
}
|
||||
|
||||
|
||||
update(attempt) {
|
||||
let ffmpeg = this.getCurrentState();
|
||||
attempt.ffmpegPathLockDate = ffmpeg.ffmpegPathLockDate;
|
||||
if (isLocked(ffmpeg)) {
|
||||
console.log("Note: ffmpeg path is not being updated since it's been locked for your security.");
|
||||
attempt.ffmpegPath = ffmpeg.ffmpegPath;
|
||||
if (typeof(ffmpeg.ffmpegPathLockDate) === 'undefined') {
|
||||
// make sure to lock it even if it was undefined
|
||||
attempt.ffmpegPathLockDate = new Date().getTime() - DAY_MS;
|
||||
}
|
||||
} else if (attempt.addLock === true) {
|
||||
// lock it right now
|
||||
attempt.ffmpegPathLockDate = new Date().getTime() - DAY_MS;
|
||||
} else {
|
||||
attempt.ffmpegPathLockDate = new Date().getTime() + DAY_MS;
|
||||
}
|
||||
delete attempt.ffmpegPathLockDate;
|
||||
delete attempt.addLock;
|
||||
delete attempt.lock;
|
||||
|
||||
@ -62,7 +34,6 @@ class FfmpegSettingsService {
|
||||
}
|
||||
|
||||
reset() {
|
||||
// Even if reseting, it's impossible to unlock the ffmpeg path
|
||||
let ffmpeg = databaseMigration.defaultFFMPEG() ;
|
||||
this.update(ffmpeg);
|
||||
return this.get();
|
||||
@ -76,13 +47,7 @@ class FfmpegSettingsService {
|
||||
}
|
||||
|
||||
function fixupFFMPEGSettings(ffmpeg) {
|
||||
if (typeof(ffmpeg.ffmpegPath) !== 'string') {
|
||||
return "ffmpeg path is required."
|
||||
}
|
||||
if (! isValidFilePath(ffmpeg.ffmpegPath)) {
|
||||
return "ffmpeg path must be a valid file path."
|
||||
}
|
||||
|
||||
|
||||
if (typeof(ffmpeg.maxFPS) === 'undefined') {
|
||||
ffmpeg.maxFPS = 60;
|
||||
return null;
|
||||
@ -91,32 +56,6 @@ function fixupFFMPEGSettings(ffmpeg) {
|
||||
}
|
||||
}
|
||||
|
||||
//These checks are good but might not be enough, as long as we are letting the
|
||||
//user choose any path and we are making dizqueTV execute, it is too risky,
|
||||
//hence why we are also adding the lock feature on top of these checks.
|
||||
function isValidFilePath(filePath) {
|
||||
const normalizedPath = path.normalize(filePath);
|
||||
|
||||
if (!path.isAbsolute(normalizedPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(normalizedPath);
|
||||
return stats.isFile();
|
||||
} catch (err) {
|
||||
// Handle potential errors (e.g., file not found, permission issues)
|
||||
if (err.code === 'ENOENT') {
|
||||
return false; // File does not exist
|
||||
} else {
|
||||
throw err; // Re-throw other errors for debugging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isLocked(ffmpeg) {
|
||||
return isNaN(ffmpeg.ffmpegPathLockDate) || ffmpeg.ffmpegPathLockDate < new Date().getTime();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
149
src/services/filler-service.js
Normal file
149
src/services/filler-service.js
Normal file
@ -0,0 +1,149 @@
|
||||
const events = require('events')
|
||||
|
||||
const FILLER_UPDATE = 30 * 60 * 1000; //30 minutes might be too aggressive
|
||||
//this will be configurable one day.
|
||||
|
||||
class FillerService extends events.EventEmitter {
|
||||
|
||||
constructor(fillerDB, plexProxyService, channelService) {
|
||||
super();
|
||||
this.fillerDB = fillerDB;
|
||||
this.plexProxyService = plexProxyService;
|
||||
this.channelService = channelService;
|
||||
}
|
||||
|
||||
async saveFiller(id, body) {
|
||||
body = await this.prework(body);
|
||||
return this.fillerDB.saveFiller(id, body);
|
||||
}
|
||||
|
||||
async createFiller(body) {
|
||||
body = await this.prework(body);
|
||||
return this.fillerDB.createFiller(body);
|
||||
}
|
||||
|
||||
async getFillerChannels(id) {
|
||||
let numbers = await this.channelService.getAllChannelNumbers();
|
||||
let channels = [];
|
||||
await Promise.all( numbers.map( async(number) => {
|
||||
let ch = await this.channelService.getChannel(number);
|
||||
let name = ch.name;
|
||||
let fillerCollections = ch.fillerCollections;
|
||||
for (let i = 0 ; i < fillerCollections.length; i++) {
|
||||
if (fillerCollections[i].id === id) {
|
||||
channels.push( {
|
||||
number: number,
|
||||
name : name,
|
||||
} );
|
||||
break;
|
||||
}
|
||||
}
|
||||
ch = null;
|
||||
|
||||
} ) );
|
||||
return channels;
|
||||
}
|
||||
|
||||
async deleteFiller(id) {
|
||||
try {
|
||||
let channels = await this.getFillerChannels(id);
|
||||
await Promise.all( channels.map( async(channel) => {
|
||||
console.log(`Updating channel ${channel.number} , remove filler: ${id}`);
|
||||
let json = await channelService.getChannel(channel.number);
|
||||
json.fillerCollections = json.fillerCollections.filter( (col) => {
|
||||
return col.id != id;
|
||||
} );
|
||||
await this.channelService.saveChannel( channel.number, json );
|
||||
} ) );
|
||||
} finally {
|
||||
await this.fillerDB.deleteFiller(id);
|
||||
}
|
||||
}
|
||||
|
||||
async prework(body) {
|
||||
if (body.mode === "import") {
|
||||
body.content = await this.getContents(body);
|
||||
body.import.lastRefreshTime = new Date().getTime();
|
||||
} else {
|
||||
delete body.import;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
async getContents(body) {
|
||||
let serverKey = body.import.serverName;
|
||||
let key = body.import.key;
|
||||
let content = await this.plexProxyService.getKeyMediaContents(serverKey, key);
|
||||
return content;
|
||||
}
|
||||
|
||||
async getFillersFromChannel(channel) {
|
||||
|
||||
let loadChannelFiller = async(fillerEntry) => {
|
||||
let content = [];
|
||||
try {
|
||||
let filler = await this.fillerDB.getFiller(fillerEntry.id);
|
||||
await this.fillerUsageWatcher(fillerEntry.id, filler);
|
||||
content = filler.content;
|
||||
} catch(e) {
|
||||
console.error(`Channel #${channel.number} - ${channel.name} references an unattainable filler id: ${fillerEntry.id}`, e);
|
||||
}
|
||||
return {
|
||||
id: fillerEntry.id,
|
||||
content: content,
|
||||
weight: fillerEntry.weight,
|
||||
cooldown: fillerEntry.cooldown,
|
||||
}
|
||||
};
|
||||
return await Promise.all(
|
||||
channel.fillerCollections.map(loadChannelFiller)
|
||||
);
|
||||
}
|
||||
|
||||
async fillerUsageWatcher(id, filler) {
|
||||
if (filler.mode === "import") {
|
||||
//I need to upgrade nodejs version ASAP
|
||||
let lastTime = 0;
|
||||
if (
|
||||
(typeof(filler.import) !== "undefined")
|
||||
&&
|
||||
!isNaN(filler.import.lastRefreshTime)
|
||||
) {
|
||||
lastTime = filler.import.lastRefreshTime;
|
||||
}
|
||||
let t = new Date().getTime();
|
||||
if ( t - lastTime >= FILLER_UPDATE) {
|
||||
//time to do an update.
|
||||
if ( (typeof(filler.content) === "undefined")
|
||||
|| (filler.content.length == 0)
|
||||
) {
|
||||
//It should probably be an sync update...
|
||||
await this.refreshFiller(id);
|
||||
} else {
|
||||
this.refreshFiller(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async refreshFiller(id) {
|
||||
let t0 = new Date().getTime();
|
||||
console.log(`Refreshing filler with id=${id}`);
|
||||
try {
|
||||
let filler = await this.fillerDB.getFiller(id);
|
||||
await this.saveFiller(id, filler);
|
||||
} catch (err) {
|
||||
console.log(`Unable to update filler: ${id}`, err);
|
||||
} finally {
|
||||
let t1 = new Date().getTime();
|
||||
console.log(`Refreshed filler with id=${id} in ${t1-t0}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = FillerService
|
||||
94
src/services/plex-proxy-service.js
Normal file
94
src/services/plex-proxy-service.js
Normal file
@ -0,0 +1,94 @@
|
||||
const Plex = require('../plex.js')
|
||||
const events = require('events')
|
||||
|
||||
class PlexProxyService extends events.EventEmitter {
|
||||
|
||||
constructor(plexServerDB) {
|
||||
super();
|
||||
this.plexServerDB = plexServerDB;
|
||||
}
|
||||
|
||||
async get(serverName64, path) {
|
||||
let plexServer = await getPlexServer64(this.plexServerDB, serverName64);
|
||||
// A potential area of improvement is to reuse the client when possible
|
||||
let client = new Plex(plexServer);
|
||||
return { MediaContainer: await client.Get("/" + path) };
|
||||
}
|
||||
|
||||
async getKeyMediaContents(serverName, key) {
|
||||
let plexServer = await getPlexServer(this.plexServerDB, serverName);
|
||||
let client = new Plex(plexServer);
|
||||
let obj = { MediaContainer: await client.Get(key) };
|
||||
let metadata = obj.MediaContainer.Metadata;
|
||||
if ( typeof(metadata) !== "object") {
|
||||
return [];
|
||||
}
|
||||
metadata = metadata.map( (item) => fillerMapper(serverName, item) );
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
function fillerMapper(serverName, plexMetadata) {
|
||||
let image = {};
|
||||
if ( (typeof(plexMetadata.Image) === "object")
|
||||
&& (typeof(plexMetadata.Image[0]) === "object")
|
||||
) {
|
||||
image = plexMetadata.Image[0];
|
||||
}
|
||||
|
||||
let media = {};
|
||||
if ( (typeof(plexMetadata.Media) === "object")
|
||||
&& (typeof(plexMetadata.Media[0]) === "object")
|
||||
) {
|
||||
media = plexMetadata.Media[0];
|
||||
}
|
||||
|
||||
|
||||
let part = {};
|
||||
if ( (typeof(media.Part) === "object")
|
||||
&& (typeof(media.Part[0]) === "object")
|
||||
) {
|
||||
part = media.Part[0];
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
title : plexMetadata.title,
|
||||
key : plexMetadata.key,
|
||||
ratingKey: plexMetadata.ratingKey,
|
||||
icon : image.url,
|
||||
type : plexMetadata.type,
|
||||
duration : part.duration,
|
||||
durationStr : undefined,
|
||||
summary : "",
|
||||
date : "",
|
||||
year : plexMetadata.year,
|
||||
plexFile : part.key,
|
||||
file : part.file,
|
||||
showTitle: plexMetadata.title,
|
||||
episode : 1,
|
||||
season : 1,
|
||||
serverKey: serverName,
|
||||
commercials: [],
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlexServer(plexServerDB, serverKey) {
|
||||
let server = await plexServerDB.getPlexServerByName(serverKey);
|
||||
if (server == null) {
|
||||
throw Error("server not found");
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getPlexServer64(plexServerDB, serverName64) {
|
||||
let serverKey = Buffer.from(serverName64, 'base64').toString('utf-8');
|
||||
return await getPlexServer(plexServerDB, serverKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = PlexProxyService
|
||||
@ -1,6 +1,6 @@
|
||||
const events = require('events')
|
||||
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');
|
||||
|
||||
class TVGuideService extends events.EventEmitter
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
src/video.js
37
src/video.js
@ -18,17 +18,18 @@ async function shutdown() {
|
||||
stopPlayback = true;
|
||||
}
|
||||
|
||||
function video( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ) {
|
||||
function video( channelService, fillerService, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ) {
|
||||
var router = express.Router()
|
||||
|
||||
router.get('/setup', (req, res) => {
|
||||
router.get('/setup', async (req, res) => {
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||
// Check if ffmpeg path is valid
|
||||
if (!fs.existsSync(ffmpegSettings.ffmpegPath)) {
|
||||
res.status(500).send("FFMPEG path is invalid. The file (executable) doesn't exist.")
|
||||
console.error("The FFMPEG Path is invalid. Please check your configuration.")
|
||||
let ffmpegPath = await ffmpegInfo.getPath();
|
||||
if (ffmpegPath == null) {
|
||||
res.status(500).send("Missing FFmpeg.")
|
||||
return
|
||||
}
|
||||
ffmpegSettings.ffmpegPath = ffmpegPath;
|
||||
|
||||
console.log(`\r\nStream starting. Channel: 1 (dizqueTV)`)
|
||||
|
||||
@ -72,14 +73,14 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
return
|
||||
}
|
||||
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||
|
||||
// Check if ffmpeg path is valid
|
||||
if (!fs.existsSync(ffmpegSettings.ffmpegPath)) {
|
||||
res.status(500).send("FFMPEG path is invalid. The file (executable) doesn't exist.")
|
||||
console.error("The FFMPEG Path is invalid. Please check your configuration.")
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||
let ffmpegPath = await ffmpegInfo.getPath();
|
||||
if (ffmpegPath == null) {
|
||||
res.status(500).send("Missing FFmpeg.")
|
||||
return
|
||||
}
|
||||
ffmpegSettings.ffmpegPath = ffmpegPath;
|
||||
|
||||
if (step == 0) {
|
||||
res.writeHead(200, {
|
||||
@ -174,14 +175,14 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
|
||||
let isBetween = ( (typeof req.query.between !== 'undefined') && (req.query.between=='1') );
|
||||
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||
|
||||
// Check if ffmpeg path is valid
|
||||
if (!fs.existsSync(ffmpegSettings.ffmpegPath)) {
|
||||
res.status(500).send("FFMPEG path is invalid. The file (executable) doesn't exist.")
|
||||
console.error("The FFMPEG Path is invalid. Please check your configuration.")
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||
let ffmpegPath = await ffmpegInfo.getPath();
|
||||
if (ffmpegPath == null) {
|
||||
res.status(500).send("Missing FFmpeg.")
|
||||
return
|
||||
}
|
||||
ffmpegSettings.ffmpegPath = ffmpegPath;
|
||||
|
||||
if (ffmpegSettings.disablePreludes === true) {
|
||||
//disable the preludes
|
||||
@ -303,7 +304,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
if ( (prog == null) || (typeof(prog) === 'undefined') || (prog.program == null) || (typeof(prog.program) == "undefined") ) {
|
||||
throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted."
|
||||
}
|
||||
let fillers = await fillerDB.getFillersFromChannel(brandChannel);
|
||||
let fillers = await fillerService.getFillersFromChannel(brandChannel);
|
||||
try {
|
||||
let lineup = helperFuncs.createLineup(programPlayTimeDB, prog, brandChannel, fillers, isFirst)
|
||||
lineupItem = lineup.shift();
|
||||
@ -332,9 +333,9 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
if (typeof(u) !== 'undefined') {
|
||||
let u2 = upperBound;
|
||||
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;
|
||||
}
|
||||
channelCache.recordPlayback( programPlayTimeDB, redirectChannels[i].number, t0, lineupItem );
|
||||
|
||||
@ -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;
|
||||
@ -1014,9 +1029,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
||||
} else if (channel.overlayIcon && !validURL(channel.icon)) {
|
||||
scope.error.icon = "Please enter a valid image URL. Cant overlay an invalid image."
|
||||
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) {
|
||||
scope.error.programs = "No programs have been selected. Select at least one program."
|
||||
scope.error.tab = "programming";
|
||||
|
||||
@ -6,7 +6,14 @@ module.exports = function (dizquetv, resolutionOptions) {
|
||||
scope: {
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
//add validations to ffmpeg settings, speciall commas in codec name
|
||||
|
||||
scope.ffmpegPathLoading = true;
|
||||
scope.ffmpegPath = ""
|
||||
dizquetv.getFFMpegPath().then( (fpath) => {
|
||||
scope.ffmpegPath = fpath.ffmpegPath;
|
||||
scope.ffmpegPathLoading = false;
|
||||
});
|
||||
//add validations to ffmpeg settings, special commas in codec name
|
||||
dizquetv.getFfmpegSettings().then((settings) => {
|
||||
scope.settings = settings
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
module.exports = function ($timeout, commonProgramTools, getShowData) {
|
||||
module.exports = function ($timeout, dizquetv, commonProgramTools, getShowData) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'templates/filler-config.html',
|
||||
@ -13,6 +13,16 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
|
||||
scope.content = [];
|
||||
scope.visible = false;
|
||||
scope.error = undefined;
|
||||
scope.modes = [ {
|
||||
name: "import",
|
||||
description: "Collection/Playlist from Plex",
|
||||
}, {
|
||||
name: "custom",
|
||||
description: "Custom List of Clips",
|
||||
} ];
|
||||
scope.servers = [];
|
||||
scope.libraries = [];
|
||||
scope.sources = [];
|
||||
|
||||
function refreshContentIndexes() {
|
||||
for (let i = 0; i < scope.content.length; i++) {
|
||||
@ -47,20 +57,173 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
|
||||
console.log("movedFunction(" + index + ")");
|
||||
}
|
||||
|
||||
scope.serverChanged = async () => {
|
||||
if (scope.server === "") {
|
||||
scope.libraryKey = "";
|
||||
return;
|
||||
}
|
||||
scope.loadingLibraries = true;
|
||||
try {
|
||||
let libraries = (await dizquetv.getFromPlexProxy(scope.server, "/library/sections")).Directory;
|
||||
if ( typeof(libraries) === "undefined") {
|
||||
libraries = []
|
||||
}
|
||||
let officialLibraries = libraries.map( (library) => {
|
||||
return {
|
||||
"key" : library.key,
|
||||
"description" : library.title,
|
||||
}
|
||||
} );
|
||||
|
||||
let defaultLibrary = {
|
||||
"key": "",
|
||||
"description" : "Select a Library...",
|
||||
}
|
||||
let playlists = [
|
||||
{
|
||||
"key": "$PLAYLISTS",
|
||||
"description" : "Playlists",
|
||||
}
|
||||
];
|
||||
let combined = officialLibraries.concat(playlists);
|
||||
if (! combined.some( (library) => library.key === scope.libraryKey) ) {
|
||||
scope.libraryKey = "";
|
||||
scope.libraries = [defaultLibrary].concat(combined);
|
||||
} else {
|
||||
scope.libraries = combined;
|
||||
}
|
||||
} catch (err) {
|
||||
scope.libraries = [ { name: "", description: "Unable to load libraries"} ];
|
||||
scope.libraryKey = ""
|
||||
throw err;
|
||||
} finally {
|
||||
scope.loadingLibraries = false;
|
||||
$timeout( () => {}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
scope.linker( (filler) => {
|
||||
scope.libraryChanged = async () => {
|
||||
if (scope.libraryKey == null) {
|
||||
throw Error(`null libraryKey? ${scope.libraryKey} ${new Date().getTime()} `);
|
||||
}
|
||||
if (scope.libraryKey === "") {
|
||||
scope.sourceKey = "";
|
||||
return;
|
||||
}
|
||||
scope.loadingCollections = true;
|
||||
try {
|
||||
let collections;
|
||||
if (scope.libraryKey === "$PLAYLISTS") {
|
||||
collections = (await dizquetv.getFromPlexProxy(scope.server, `/playlists`)).Metadata;
|
||||
} else {
|
||||
collections = (await dizquetv.getFromPlexProxy(scope.server, `/library/sections/${scope.libraryKey}/collections`));
|
||||
collections = collections.Metadata
|
||||
}
|
||||
if (typeof(collections) === "undefined") {
|
||||
//when the library has no collections it returns size=0
|
||||
//and no array
|
||||
collections = [];
|
||||
}
|
||||
let officialCollections = collections.map( (col) => {
|
||||
return {
|
||||
"key" : col.key,
|
||||
"description" : col.title,
|
||||
}
|
||||
} );
|
||||
let defaultSource = {
|
||||
"key": "",
|
||||
"description" : "Select a Source...",
|
||||
};
|
||||
if (officialCollections.length == 0) {
|
||||
defaultSource = {
|
||||
"key": "",
|
||||
"description" : "(No collections/lists found)",
|
||||
}
|
||||
}
|
||||
if (! officialCollections.some( (col) => col.key === scope.sourceKey ) ) {
|
||||
scope.sourceKey = "";
|
||||
scope.sources = [defaultSource].concat(officialCollections);
|
||||
} else {
|
||||
scope.sources = officialCollections;
|
||||
}
|
||||
} catch (err) {
|
||||
scope.sources = [ { name: "", description: "Unable to load collections"} ];
|
||||
scope.sourceKey = "";
|
||||
throw err;
|
||||
} finally {
|
||||
scope.loadingCollections = false;
|
||||
$timeout( () => {}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
let reloadServers = async() => {
|
||||
scope.loadingServers = true;
|
||||
try {
|
||||
let servers = await dizquetv.getPlexServers();
|
||||
scope.servers = servers.map( (s) => {
|
||||
return {
|
||||
"name" : s.name,
|
||||
"description" : `Plex - ${s.name}`,
|
||||
}
|
||||
} );
|
||||
let defaultServer = {
|
||||
name: "",
|
||||
description: "Select a Plex server..."
|
||||
};
|
||||
if (! scope.servers.some( (server) => server.name === scope.server) ) {
|
||||
scope.server = "";
|
||||
scope.servers = [defaultServer].concat(scope.servers);
|
||||
}
|
||||
} catch (err) {
|
||||
scope.server = "";
|
||||
scope.servers = [ {name:"", description:"Could not load servers"} ];
|
||||
throw err;
|
||||
} finally {
|
||||
scope.loadingServers = false;
|
||||
$timeout( () => {}, 0);
|
||||
}
|
||||
|
||||
await scope.serverChanged();
|
||||
await scope.libraryChanged();
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
scope.linker( async (filler) => {
|
||||
|
||||
if ( typeof(filler) === 'undefined') {
|
||||
scope.name = "";
|
||||
scope.content = [];
|
||||
scope.id = undefined;
|
||||
scope.title = "Create Filler List";
|
||||
scope.mode = "import";
|
||||
scope.server = "";
|
||||
scope.libraryKey = "";
|
||||
scope.sourceKey = "";
|
||||
} else {
|
||||
scope.name = filler.name;
|
||||
scope.content = filler.content;
|
||||
scope.id = filler.id;
|
||||
scope.title = "Edit Filler List";
|
||||
scope.mode = filler.mode;
|
||||
scope.server = filler?.import?.serverName;
|
||||
if ( typeof(scope.server) !== "string" ) {
|
||||
scope.server = "";
|
||||
}
|
||||
scope.libraryKey = filler?.import?.meta?.libraryKey;
|
||||
if ( typeof(scope.libraryKey) !== "string" ) {
|
||||
scope.libraryKey = "";
|
||||
}
|
||||
scope.sourceKey = filler?.import?.key;
|
||||
if ( typeof(scope.sourceKey) !== "string" ) {
|
||||
scope.sourceKey = "";
|
||||
}
|
||||
}
|
||||
await reloadServers();
|
||||
scope.source = "";
|
||||
refreshContentIndexes();
|
||||
scope.visible = true;
|
||||
} );
|
||||
@ -73,8 +236,17 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
|
||||
if ( (typeof(scope.name) === 'undefined') || (scope.name.length == 0) ) {
|
||||
scope.error = "Please enter a name";
|
||||
}
|
||||
if ( scope.content.length == 0) {
|
||||
scope.error = "Please add at least one clip.";
|
||||
if ( scope?.mode === "import" ) {
|
||||
if ( (typeof(scope?.server) !== "string" ) || (scope?.server === "") ) {
|
||||
scope.error = "Please select a server"
|
||||
}
|
||||
if ( (typeof(scope?.source) !== "string" ) && (scope?.source === "") ) {
|
||||
scope.error = "Please select a source."
|
||||
}
|
||||
} else {
|
||||
if ( scope.content.length == 0) {
|
||||
scope.error = "Please add at least one clip.";
|
||||
}
|
||||
}
|
||||
if (typeof(scope.error) !== 'undefined') {
|
||||
$timeout( () => {
|
||||
@ -83,14 +255,30 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
|
||||
return;
|
||||
}
|
||||
scope.visible = false;
|
||||
scope.onDone( {
|
||||
let object = {
|
||||
name: scope.name,
|
||||
content: scope.content.map( (c) => {
|
||||
delete c.$index
|
||||
return c;
|
||||
} ),
|
||||
id: scope.id,
|
||||
} );
|
||||
mode: scope.mode,
|
||||
|
||||
};
|
||||
if (object.mode === "import") {
|
||||
object.content = [];
|
||||
//In reality dizqueTV only needs to know the server name
|
||||
//and the source key, the meta object is for extra data
|
||||
//that is useful for external things like this UI.
|
||||
object.import = {
|
||||
serverName : scope.server,
|
||||
key: scope.sourceKey,
|
||||
meta: {
|
||||
libraryKey : scope.libraryKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
scope.onDone( object );
|
||||
}
|
||||
scope.getText = (clip) => {
|
||||
let show = getShowData(clip);
|
||||
|
||||
@ -31,13 +31,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
||||
});
|
||||
}
|
||||
scope.selectOrigin = function (origin) {
|
||||
if ( origin.type === 'plex' ) {
|
||||
scope.plexServer = origin.server;
|
||||
updateLibrary(scope.plexServer);
|
||||
} else {
|
||||
scope.plexServer = undefined;
|
||||
updateCustomShows();
|
||||
}
|
||||
updateLibrary(origin);
|
||||
}
|
||||
scope._onFinish = (s, insertPoint) => {
|
||||
if (s.length > scope.limit) {
|
||||
@ -99,20 +93,31 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
||||
"type" : "plex",
|
||||
"name" : `Plex - ${s.name}`,
|
||||
"server": s,
|
||||
"loaded" : false,
|
||||
}
|
||||
} );
|
||||
scope.currentOrigin = scope.origins[0];
|
||||
scope.plexServer = scope.currentOrigin.server;
|
||||
scope.origins.push( {
|
||||
"type": "dizquetv",
|
||||
"name" : "dizqueTV - Custom Shows",
|
||||
"loaded" : false,
|
||||
} );
|
||||
updateLibrary(scope.plexServer)
|
||||
updateLibrary(scope.origins[0])
|
||||
})
|
||||
|
||||
let updateLibrary = async(server) => {
|
||||
let updateLibrary = async(origin) => {
|
||||
scope.currentOrigin = origin;
|
||||
origin.loaded = false;
|
||||
if ( origin.type !== 'plex' ) {
|
||||
scope.plexServer = undefined;
|
||||
await updateCustomShows();
|
||||
origin.loaded = true;
|
||||
return;
|
||||
}
|
||||
let server = scope.currentOrigin.server;
|
||||
let lib = await plex.getLibrary(server);
|
||||
let play = await plex.getPlaylists(server);
|
||||
scope.currentOrigin.loaded = true;
|
||||
scope.plexServer = server;
|
||||
|
||||
play.forEach( p => {
|
||||
p.type = "playlist";
|
||||
|
||||
@ -20,12 +20,9 @@ module.exports = function (plex, dizquetv, $timeout) {
|
||||
scope.servers = servers;
|
||||
if(servers) {
|
||||
for (let i = 0; i < scope.servers.length; i++) {
|
||||
scope.servers[i].uiStatus = 0;
|
||||
scope.servers[i].backendStatus = 0;
|
||||
let t = (new Date()).getTime();
|
||||
scope.servers[i].uiPending = t;
|
||||
scope.servers[i].backendPending = t;
|
||||
scope.refreshUIStatus(t, i);
|
||||
scope.refreshBackendStatus(t, i);
|
||||
}
|
||||
}
|
||||
@ -51,22 +48,6 @@ module.exports = function (plex, dizquetv, $timeout) {
|
||||
scope.refreshServerList();
|
||||
}
|
||||
|
||||
scope.isAnyUIBad = () => {
|
||||
let t = (new Date()).getTime();
|
||||
if(scope.servers) {
|
||||
for (let i = 0; i < scope.servers.length; i++) {
|
||||
let s = scope.servers[i];
|
||||
if (
|
||||
(s.uiStatus == -1)
|
||||
|| ( (s.uiStatus == 0) && (s.uiPending + 30000 < t) )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
scope.isAnyBackendBad = () => {
|
||||
let t = (new Date()).getTime();
|
||||
if(scope.servers) {
|
||||
@ -84,15 +65,6 @@ module.exports = function (plex, dizquetv, $timeout) {
|
||||
};
|
||||
|
||||
|
||||
scope.refreshUIStatus = async (t, i) => {
|
||||
let s = await plex.check(scope.servers[i]);
|
||||
if (scope.servers[i].uiPending == t) {
|
||||
// avoid updating for a previous instance of the row
|
||||
scope.servers[i].uiStatus = s;
|
||||
}
|
||||
scope.$apply();
|
||||
};
|
||||
|
||||
scope.refreshBackendStatus = async (t, i) => {
|
||||
let s = await dizquetv.checkExistingPlexServer(scope.servers[i].name);
|
||||
if (scope.servers[i].backendPending == t) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -19,12 +19,10 @@
|
||||
"minutes_to_sign_plex": "You have 2 minutes to sign into your Plex Account.",
|
||||
"name": "Name",
|
||||
"uri": "URI",
|
||||
"ui_route": "UI Route",
|
||||
"backend_route": "Backend Route",
|
||||
"routeStatus": "Route Status",
|
||||
"ok": "ok",
|
||||
"error": "error",
|
||||
"ui_bad": "If a Plex server configuration has problems with the UI route, the channel editor won't be able to access its content.",
|
||||
"backend_bad": "If a Plex server configuration has problems with the backend route, dizqueTV won't be able to play its content.",
|
||||
"backend_bad": "A route problem means the dizqueTV server can't establish a connection with the configured Plex server.",
|
||||
"plex_transcoder_settings": "Plex Transcoder Settings",
|
||||
"update": "Update",
|
||||
"reset_options": "Reset Options",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -15,40 +15,20 @@
|
||||
|
||||
<hr></hr>
|
||||
<h6>FFMPEG Executable Path</h6>
|
||||
<div class="row" ng-show="settings.lock !== true">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label>Path</label>
|
||||
<input id="ffmpegPath" ria-describedby="ffmpegHelp" type="text" class="form-control form-control-sm" ng-model="settings.ffmpegPath"></input>
|
||||
<small class="form-text text-muted">
|
||||
The path to the ffmpeg executable. (e.g: /usr/bin/ffmpeg or C:\ffmpeg\bin\ffmpeg.exe) FFMPEG version 4.2+ required. Check by opening the version tab.
|
||||
|
||||
<div class='loader' ng-show='ffmpegPathLoading'></div>
|
||||
<input id="ffmpegPath" ria-describedby="ffmpegHelp" type="text" class="form-control form-control-sm" ng-model="ffmpegPath" readonly ng-hide="ffmpegPathLoading"></input>
|
||||
<small class="form-text text-muted" ng-show="ffmpegPath != null">
|
||||
The path to the ffmpeg executable. Please check the instructions if you need to change it.
|
||||
</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-group">
|
||||
<input id="lockFfmpeg" type="checkbox" ng-model="settings.addLock"></input>
|
||||
<label for="lockFfmpeg">Lock ffmpeg path setting</label>
|
||||
<small class="form-text text-muted">This will lock the ffmpeg path setting so that it is no longer editable from UI. Even if you don't toggle this option, the setting will get locked in 24 hours.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row" ng-show="settings.lock === true">
|
||||
<div class="col-sm-9">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label>Path</label>
|
||||
<input id="ffmpegPath" ria-describedby="ffmpegHelp" type="text" class="form-control form-control-sm" ng-model="settings.ffmpegPath" readonly></input>
|
||||
<small class="form-text text-muted">
|
||||
The ffmpeg path setting is currently locked and can't be edited from the UI. It's not usually necessary to update this path once it's known to be working. Run dizquetv with the <b>--unlock</b> command line argument to enable editing it again.
|
||||
<small class="form-text text-muted" ng-show="ffmpegPath == null">
|
||||
dizqueTV uses FFmpeg to create video streams. Please check the instructions for info about how to set this up.
|
||||
</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -11,8 +11,52 @@
|
||||
<div class="form-group">
|
||||
<label for="name">Filler Name:</label>
|
||||
<input type="text" class="form-control" id="name" placeholder="Filler Name" ng-model="name" ></input>
|
||||
|
||||
<div>Mode:</div>
|
||||
<select class="custom-select"
|
||||
ng-model="mode"
|
||||
ng-options="theMode.name as theMode.description for theMode in modes" ></select>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-show="mode==='import'">
|
||||
<div class="form-group">
|
||||
<label for="server">Server:</label>
|
||||
|
||||
<div class="loader" ng-show="loadingServers === true" ></div>
|
||||
<select
|
||||
ng-show="loadingServers !== true"
|
||||
id="server"
|
||||
class="custom-select"
|
||||
ng-change="serverChanged()"
|
||||
ng-model="server"
|
||||
ng-options="x.name as x.description for x in servers" ></select>
|
||||
</div>
|
||||
<div ng-show="server !==''">
|
||||
<label for="library">Library:</label>
|
||||
|
||||
<div class="loader" ng-show="loadingLibraries === true" ></div>
|
||||
<select
|
||||
ng-show="loadingLibraries !== true"
|
||||
id="library"
|
||||
class="custom-select"
|
||||
ng-change="libraryChanged()"
|
||||
ng-model="libraryKey"
|
||||
ng-options="x.key as x.description for x in libraries" ></select>
|
||||
</div>
|
||||
<div ng-show="libraryKey !==''">
|
||||
<label for="source">Source:</label>
|
||||
|
||||
<div class="loader" ng-show="loadingCollections === true" ></div>
|
||||
<select
|
||||
ng-show="loadingCollections !== true"
|
||||
id="source"
|
||||
class="custom-select"
|
||||
ng-model="sourceKey"
|
||||
ng-options="x.key as x.description for x in sources" ></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="mode==='custom'">
|
||||
<h6 style="margin-top: 10px;">Clips</h6>
|
||||
|
||||
<div class="flex-container">
|
||||
@ -65,10 +109,11 @@
|
||||
<div ng-show="content.length === 0">
|
||||
<p class="text-center text-info">Click the <span class="fa fa-plus"></span> to import filler content from your Plex server(s).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div vs-repeat class="modal-body container list-group list-group-root filler-list"
|
||||
<div ng-show="mode==='custom'" vs-repeat class="modal-body container list-group list-group-root filler-list"
|
||||
dnd-list="content" ng-if="showList()"
|
||||
vs-repeat-reinitialized="vsReinitialized(event, startIndex, endIndex)"
|
||||
ng-init="setUpWatcher()"
|
||||
@ -76,8 +121,7 @@
|
||||
dnd-list=""
|
||||
|
||||
>
|
||||
<div class="list-group-item flex-container filler-row" style="cursor: default; height:1.1em; overflow:hidden" ng-repeat="x in content" track-by="x.$index" dnd-draggable="x"
|
||||
"
|
||||
<div class="list-group-item flex-container filler-row" style="cursor: default; height:1.1em; overflow:hidden" ng-repeat="x in content" track-by="x.$index" dnd-draggable="x"
|
||||
dnd-effect-allowed="move"
|
||||
dnd-moved="movedFunction(x.$index)"
|
||||
>
|
||||
|
||||
@ -43,7 +43,13 @@
|
||||
</button>
|
||||
Content:
|
||||
</label>
|
||||
<ul ng-show="currentOrigin.type=='plex' " class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
|
||||
|
||||
<ul ng-show="currentOrigin.loaded!==true" class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
|
||||
|
||||
<li class="list-group-item"><div class="loader"></div> <div>Loading Library Contents...</div></li>
|
||||
|
||||
</ul>
|
||||
<ul ng-show="currentOrigin.type=='plex' && currentOrigin.loaded===true " class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
|
||||
<li class="list-group-item" ng-repeat="a in libraries">
|
||||
<div class="flex-container library-item-hover {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
|
||||
<span class="fa {{ a.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
|
||||
@ -122,7 +128,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul ng-show="currentOrigin.type=='dizquetv' " class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
|
||||
<ul ng-show="currentOrigin.type=='dizquetv' && currentOrigin.loaded===true" class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
|
||||
<li class="list-group-item" ng-repeat="x in customShows">
|
||||
<div class="flex-container" ng-click="addCustomShow(x);">
|
||||
<span class="fa fa-plus-circle tab"></span>
|
||||
|
||||
@ -16,8 +16,7 @@
|
||||
<tr>
|
||||
<th>{{'settings_server.name' | i18next}}</th>
|
||||
<th>{{'settings_server.uri' | i18next}}</th>
|
||||
<th>{{'settings_server.ui_route' | i18next}}</th>
|
||||
<th>{{'settings_server.backend_route' | i18next}}</th>
|
||||
<th>{{'settings_server.routeStatus' | i18next}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-if="servers.length === 0">
|
||||
@ -31,11 +30,6 @@
|
||||
<tr ng-repeat="x in servers" ng-hide="serversPending" >
|
||||
<td>{{ x.name }}</td>
|
||||
<td><span class='text-secondary text-small'>{{ x.uri }}</span></td>
|
||||
<td>
|
||||
<div class='loader' ng-if="x.uiStatus == 0"></div>
|
||||
<div class='text-success' ng-if="x.uiStatus == 1"><i class='fa fa-check'></i>{{'settings_server.ok' | i18next}}</div>
|
||||
<div class='text-danger' ng-if="x.uiStatus == -1"><i class='fa fa-exclamation-triangle'></i>{{'settings_server.error' | i18next}}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class='loader' ng-if="x.backendStatus == 0"></div>
|
||||
<div class='text-success' ng-if="x.backendStatus == 1"><i class='fa fa-check'></i>{{'settings_server.ok' | i18next}}</div>
|
||||
@ -51,11 +45,6 @@
|
||||
<p class="text-center text-danger small">{{serverError}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="isAnyUIBad()">
|
||||
<td colspan="5">
|
||||
<p class="text-center text-danger small">{{'settings_server.ui_bad' | i18next}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="isAnyBackendBad()">
|
||||
<td colspan="5">
|
||||
<p class="text-center text-danger small">{{'settings_server.server_bad' | i18next}}</p>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@ module.exports = function ($http, $q) {
|
||||
getVersion: () => {
|
||||
return $http.get('/api/version').then((d) => { return d.data })
|
||||
},
|
||||
getPlexServers: () => {
|
||||
return $http.get('/api/plex-servers').then((d) => { return d.data })
|
||||
getPlexServers: async () => {
|
||||
return (await $http.get('/api/plex-servers')).data;
|
||||
},
|
||||
addPlexServer: (plexServer) => {
|
||||
return $http({
|
||||
@ -49,6 +49,15 @@ module.exports = function ($http, $q) {
|
||||
});
|
||||
return d.data;
|
||||
},
|
||||
getFromPlexProxy: async (serverName, path) => {
|
||||
let serverName64 = Buffer.from(serverName, 'utf-8').toString('base64');
|
||||
let tmp = await ($http({
|
||||
method: 'GET',
|
||||
url : `api/plex-server/${serverName64}${path}`,
|
||||
headers: { "Cache-Control": "no-cache"},
|
||||
}))
|
||||
return tmp.data.MediaContainer;
|
||||
},
|
||||
getPlexSettings: () => {
|
||||
return $http.get('/api/plex-settings').then((d) => { return d.data })
|
||||
},
|
||||
@ -87,6 +96,9 @@ module.exports = function ($http, $q) {
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
||||
}).then((d) => { return d.data })
|
||||
},
|
||||
getFFMpegPath: () => {
|
||||
return $http.get('/api/ffmpeg-info').then((d) => { return d.data })
|
||||
},
|
||||
getXmltvSettings: () => {
|
||||
return $http.get('/api/xmltv-settings').then((d) => { return d.data })
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const Plex = require('../../src/plex');
|
||||
|
||||
module.exports = function ($http, $window, $interval) {
|
||||
module.exports = function (dizquetv, $http, $window, $interval) {
|
||||
let exported = {
|
||||
login: async () => {
|
||||
const headers = {
|
||||
@ -108,14 +108,13 @@ module.exports = function ($http, $window, $interval) {
|
||||
},
|
||||
|
||||
getLibrary: async (server) => {
|
||||
var client = new Plex(server)
|
||||
const res = await client.Get('/library/sections')
|
||||
const res = await dizquetv.getFromPlexProxy(server.name, '/library/sections')
|
||||
var sections = []
|
||||
for (let i = 0, l = typeof res.Directory !== 'undefined' ? res.Directory.length : 0; i < l; i++)
|
||||
if (res.Directory[i].type === 'movie' || res.Directory[i].type === 'show' || res.Directory[i].type === 'artist' ) {
|
||||
var genres = []
|
||||
if (res.Directory[i].type === 'movie') {
|
||||
const genresRes = await client.Get(`/library/sections/${res.Directory[i].key}/genre`)
|
||||
const genresRes = await dizquetv.getFromPlexProxy(server.name,`/library/sections/${res.Directory[i].key}/genre`)
|
||||
for (let q = 0, k = typeof genresRes.Directory !== 'undefined' ? genresRes.Directory.length : 0; q < k; q++) {
|
||||
if (genresRes.Directory[q].type === 'genre') {
|
||||
genres.push({
|
||||
@ -138,8 +137,7 @@ module.exports = function ($http, $window, $interval) {
|
||||
return sections
|
||||
},
|
||||
getPlaylists: async (server) => {
|
||||
var client = new Plex(server)
|
||||
const res = await client.Get('/playlists')
|
||||
const res = await dizquetv.getFromPlexProxy(server.name, '/playlists');
|
||||
var playlists = []
|
||||
for (let i = 0, l = typeof res.Metadata !== 'undefined' ? res.Metadata.length : 0; i < l; i++)
|
||||
if (
|
||||
@ -157,8 +155,7 @@ module.exports = function ($http, $window, $interval) {
|
||||
return playlists
|
||||
},
|
||||
getStreams: async (server, key) => {
|
||||
var client = new Plex(server)
|
||||
return client.Get(key).then((res) => {
|
||||
return dizquetv.getFromPlexProxy(server.name, key).then((res) => {
|
||||
let streams = res.Metadata[0].Media[0].Part[0].Stream
|
||||
for (let i = 0, l = streams.length; i < l; i++) {
|
||||
if (typeof streams[i].key !== 'undefined') {
|
||||
@ -169,9 +166,9 @@ module.exports = function ($http, $window, $interval) {
|
||||
})
|
||||
},
|
||||
getNested: async (server, lib, includeCollections, errors) => {
|
||||
var client = new Plex(server)
|
||||
|
||||
const key = lib.key
|
||||
const res = await client.Get(key)
|
||||
const res = await dizquetv.getFromPlexProxy(server.name, key)
|
||||
|
||||
const size = (typeof(res.Metadata) !== 'undefined') ? res.Metadata.length : 0;
|
||||
var nested = []
|
||||
@ -191,7 +188,8 @@ module.exports = function ($http, $window, $interval) {
|
||||
albumKeys = Object.keys( albumKeys );
|
||||
await Promise.all( albumKeys.map( async(albumKey) => {
|
||||
try {
|
||||
let album = await client.Get(albumKey);
|
||||
let album = await dizquetv.getFromPlexProxy(
|
||||
server.name, albumKey);
|
||||
if ( (typeof(album)!=='undefined') && album.size == 1) {
|
||||
album = album.Metadata[0];
|
||||
}
|
||||
@ -287,7 +285,7 @@ module.exports = function ($http, $window, $interval) {
|
||||
let k = res.librarySectionID;
|
||||
|
||||
k = `/library/sections/${k}/collections`;
|
||||
let collections = await client.Get(k);
|
||||
let collections = await dizquetv.getFromPlexProxy(server.name, k);
|
||||
if ( typeof(collections.Metadata) === 'undefined') {
|
||||
collections.Metadata = [];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user