Cleanup the repo!

* Move web2 -> web -- this was an artifact from the original fork where
  I wanted to keep the original webapp running side by side. We've
expanded the scope a lot since then and it doesn't make sense to keep
this name
* Remove old/ directory - this was leftover from the fork as well
* Remove various files like PR template (we'll make a new one when we're
  ready) and CODE_OF_CONDUCT
This commit is contained in:
Christian Benincasa
2024-03-05 14:17:49 -05:00
parent 1aeb85abae
commit c7ea7ab1b4
214 changed files with 194 additions and 54370 deletions

View File

@@ -1,5 +0,0 @@
Starting to keep track of some breaking changes...
1. Many API structures have changed... will enumerate these
2. If using DizqueTV, it is required that the user update to the latest (last?) version of DizqueTV before migrating over to this version. This is because old DizqueTV code to migrate the now-deprecated database is deleted. This fork only knows how to read the latest version of the DizqueTV database.
3.

View File

@@ -1,76 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
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.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* 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
Examples of unacceptable behavior by participants include:
* 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
## 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.
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.
## Scope
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.
## Enforcement
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.
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 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@@ -15,7 +15,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY server/ ./server
COPY shared/ ./shared
COPY types ./types
COPY web2 ./web2
COPY web ./web
FROM sources AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
@@ -66,4 +66,4 @@ CMD [ "/tunarr/server/build/bundle.js" ]
### Full stack ###
FROM server AS full-stack
COPY --from=build-web /tunarr/web2/dist /tunarr/server/build/web
COPY --from=build-web /tunarr/web/dist /tunarr/server/build/web

View File

@@ -30,7 +30,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY server/ ./server
COPY shared/ ./shared
COPY types ./types
COPY web2 ./web2
COPY web ./web
FROM sources AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
@@ -74,4 +74,4 @@ CMD [ "/tunarr/server/build/bundle.js" ]
### Full stack ###
FROM server AS full-stack
COPY --from=build-web /tunarr/web2/dist /tunarr/server/build/web
COPY --from=build-web /tunarr/web/dist /tunarr/server/build/web

View File

@@ -1,14 +0,0 @@
FROM node:12.18-alpine3.12
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install && npm install -g browserify nexe@3.3.7
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
EXPOSE 8000
WORKDIR /home/node/app
ENTRYPOINT [ "./dizquetv" ]
COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/
RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg

View File

@@ -1,6 +0,0 @@
FROM node:12.18-alpine3.12
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install && npm install -g browserify nexe@3.3.7
COPY --from=vexorian/dizquetv:nexecache /var/nexe/* /var/nexe/
COPY . .

View File

@@ -1,14 +0,0 @@
FROM node:12.18-alpine3.12
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install && npm install -g browserify nexe@3.3.7
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
EXPOSE 8000
WORKDIR /home/node/app
ENTRYPOINT [ "./dizquetv" ]
COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/
RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg

View File

@@ -1,79 +0,0 @@
# dizqueTV
Create live TV channel streams from media on your Plex servers.
**dizqueTV** ( _dis·keˈtiːˈvi_ ) is a fork of the project previously-known as [pseudotv-plex](https://gitlab.com/DEFENDORe/pseudotv-plex) or [pseudotv](https://github.com/DEFENDORe/pseudotv). New repository because of lack of activity from the main repository and the name change is because projects with the old name already existed and were created long before this approach and it was causing confusion. You can migrate from pseudoTV 0.0.51 to dizqueTV by renaming the .pseudotv folder to .dizquetv and running the new executable (or doing a similar trick with the volumes used by the docker containers).
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png" width="200">
Configure your channels, programs, commercials and settings using the dizqueTV web UI.
Access your channels by adding the spoofed dizqueTV HDHomerun tuner to Plex, Jellyfin or emby or utilize the M3U Url with any 3rd party IPTV player app.
EPG (Guide Information) data is stored to `.dizquetv/xmltv.xml`
## Features
- A wide variety of options for the clients where you can play the TV channels, since it both spoofs a HDHR tuner and a IPTV channel list.
- Ease of setup for xteve and Plex playback by mocking a HDHR server.
- Configure your channels once, and play them just the same in any of the other devices.
- Customize your channels and what they play. Make them display their logo while they play. Play filler content (&quot;commercials&quot;, music videos, prerolls, channel branding videos) at specific times to pad time.
- Docker image and prepackage binaries for Windows, Linux and Mac.
- Supports nvidia for hardware encoding, including in docker.
- Select media (desired programs and commercials) across multiple Plex servers
- Includes a WEB TV Guide where you can even play channels in your desktop by using your local media player.
- Subtitle support.
- Auto deinterlace any Plex media not marked `"scanType": "progressive"`
- Can be configured to completely force Direct play, if you are ready for the caveats.
## Limitations
- If you want to play the TV channels in Plex using the spoofed HDHR, Plex pass is required.
- dizqueTV does not currently watch your Plex server for media updates/changes. You must manually remove and re-add your programs for any changes to take effect. Same goes for Plex server changes (changing IP, port, etc).. You&apos;ll have to update the server settings manually in that case.
- Most players (including Plex) will break after switching episodes if video / audio format is too different. dizqueTV can be configured to use ffmpeg transcoding to prevent this, but that costs resources.
- If you configure Plex DVR, it will always be recording and transcoding the channel&apos;s contents.
## Releases
- https://github.com/vexorian/dizquetv/releases
## Wiki
- For setup instructions, check [the wiki](https://github.com/vexorian/dizquetv/wiki)
## App Preview
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/channels.png" width="500">
<br/>
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/channel-config.png" width="500">
<br/>
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/plex-guide.png" width="500">
<br/>
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/plex-stream.png" width="500">
## Development
Building/Packaging Binaries: (uses `browserify`, `babel` and `pkg`)
```
npm run build
npm run compile
npm run package
```
Live Development: (using `nodemon` and `watchify`)
```
npm run dev-client
npm run dev-server
```
## Contribute
- Pull requests welcome but please read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Pull Request Template](pull_request_template.md) first.
- Tip Jar: https://buymeacoffee.com/vexorian
## License
- Original pseudotv-Plex code was released under [MIT license (c) 2020 Dan Ferguson](https://github.com/DEFENDORe/pseudotv/blob/665e71e24ee5e93d9c9c90545addb53fdc235ff6/LICENSE)
- dizqueTV's improvements are released under zlib license (c) 2020 Victor Hugo Soliz Kuncar

View File

@@ -1,62 +0,0 @@
<?xml version="1.0"?>
<Container version="2">
<Name>dizquetv</Name>
<Repository>vexorian/dizquetv:latest-nvidia</Repository>
<Registry>https://hub.docker.com/r/vexorian/dizquetv</Registry>
<Network>host</Network>
<MyIP/>
<Shell>bash</Shell>
<Privileged>false</Privileged>
<Support/>
<Project/>
<Overview>dizqueTV is a Plex DVR plugin. It allows you to host your own fake live tv service by dynamically streaming media from your Plex servers(s). Your channels and settings are all managed through the dizqueTV Web UI.&#xD;
&#xD;
dizqueTV will show up as a HDHomeRun device within Plex. When configuring your Plex Tuner, simply use the generatered ./.dizquetv/xmltv.xml file for EPG data. dizqueTV will automatically refresh your Plex server's EPG data and channel mappings (if specified to do so in settings) when configuring channels via the Web UI. Ensure your FFMPEG path is set correctly via the Web UI, and enjoy!</Overview>
<Category/>
<WebUI>http://[IP]:[PORT:8000]</WebUI>
<TemplateURL/>
<Icon>https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png</Icon>
<ExtraParams>--runtime=nvidia</ExtraParams>
<PostArgs/>
<CPUset/>
<DateInstalled>1589436589</DateInstalled>
<DonateText/>
<DonateLink/>
<Description>dizqueTV is a Plex DVR plugin. It allows you to host your own fake live tv service by dynamically streaming media from your Plex servers(s). Your channels and settings are all manged throught the dizqueTV Web UI.&#xD;
&#xD;
dizqueTV will show up as a HDHomeRun device within Plex. When configuring your Plex Tuner, simply use the generatered ./.dizque/xmltv.xml file for EPG data. dizqueTV will automatically refresh your Plex server's EPG data and channel mappings (if specified to do so in settings) when configuring channels via the Web UI. Ensure your FFMPEG path is set correctly via the Web UI, and enjoy!</Description>
<Networking>
<Mode>host</Mode>
<Publish>
<Port>
<HostPort>8000</HostPort>
<ContainerPort>8000</ContainerPort>
<Protocol>tcp</Protocol>
</Port>
</Publish>
</Networking>
<Data>
<Volume>
<HostDir>/mnt/user/appdata/dizquetv/</HostDir>
<ContainerDir>/home/node/app/.dizquetv</ContainerDir>
<Mode>rw</Mode>
</Volume>
</Data>
<Environment>
<Variable>
<Value>all</Value>
<Name>NVIDIA_VISIBLE_DEVICES</Name>
<Mode/>
</Variable>
<Variable>
<Value>all</Value>
<Name>NVIDIA_DRIVER_CAPABILITIES</Name>
<Mode/>
</Variable>
</Environment>
<Labels/>
<Config Name="Webui &amp;amp; HDHR" Target="8000" Default="" Mode="tcp" Description="Container Port: 8000" Type="Port" Display="always" Required="false" Mask="false">8000</Config>
<Config Name="NVIDIA_VISIBLE_DEVICES" Target="NVIDIA_VISIBLE_DEVICES" Default="" Mode="" Description="Container Variable: NVIDIA_VISIBLE_DEVICES" Type="Variable" Display="always" Required="false" Mask="false">all</Config>
<Config Name="NVIDIA_DRIVER_CAPABILITIES" Target="NVIDIA_DRIVER_CAPABILITIES" Default="" Mode="" Description="Container Variable: NVIDIA_DRIVER_CAPABILITIES" Type="Variable" Display="always" Required="false" Mask="false">all</Config>
<Config Name="Appdata" Target="/home/node/app/.dizquetv" Default="" Mode="rw" Description="Container Path: /home/node/app/.dizquetv" Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/dizquetv/</Config>
</Container>

View File

@@ -1,46 +0,0 @@
<?xml version="1.0"?>
<Container version="2">
<Name>dizquetv</Name>
<Repository>vexorian/dizquetv:latest</Repository>
<Registry>https://hub.docker.com/r/vexorian/dizquetv</Registry>
<Network>host</Network>
<MyIP/>
<Shell>bash</Shell>
<Privileged>false</Privileged>
<Support/>
<Project/>
<Overview>Create live TV channel streams from media on your Plex server(s).</Overview>
<Category/>
<WebUI>http://[IP]:[PORT:8000]</WebUI>
<TemplateURL/>
<Icon>https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png</Icon>
<ExtraParams></ExtraParams>
<PostArgs/>
<CPUset/>
<DateInstalled>1589436589</DateInstalled>
<DonateText/>
<DonateLink/>
<Description>Create live TV channel streams from media on your Plex server(s).</Description>
<Networking>
<Mode>host</Mode>
<Publish>
<Port>
<HostPort>8000</HostPort>
<ContainerPort>8000</ContainerPort>
<Protocol>tcp</Protocol>
</Port>
</Publish>
</Networking>
<Data>
<Volume>
<HostDir>/mnt/user/appdata/dizquetv/</HostDir>
<ContainerDir>/home/node/app/.dizquetv</ContainerDir>
<Mode>rw</Mode>
</Volume>
</Data>
<Environment>
</Environment>
<Labels/>
<Config Name="Webui &amp;amp; HDHR" Target="8000" Default="" Mode="tcp" Description="Container Port: 8000" Type="Port" Display="always" Required="false" Mask="false">8000</Config>
<Config Name="Appdata" Target="/home/node/app/.dizquetv" Default="" Mode="rw" Description="Container Path: /home/node/app/.dizquetv" Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/dizquetv/</Config>
</Container>

View File

@@ -1,35 +0,0 @@
#!/bin/sh
MODE=${1:-all}
WIN64=dizquetv-win-x64.exe
WIN32=dizquetv-win-x86.exe
MACOSX=dizquetv-macos-x64
LINUX64=${LINUXBUILD:-dizquetv-linux-x64}
if [ -d "$./dist" ]; then
rm -rf ./dist
fi
pnpm run -r build || exit 1
cp -R ./web ./dist/web
cp -R ./resources ./dist/
cp -R ./server/build ./dist/src
cd dist
if [ "$MODE" == "all" ]; then
npx nexe -i ./src/index.js --temp /tmp/nexe -r './**/*' -t windows-x64-12.18.2 --output $WIN64
mv $WIN64 ../
npx nexe -i ./src/index.js --temp /tmp/nexe -r './**/*' -t mac-x64-12.18.2 --output $MACOSX
mv $MACOSX ../
npx nexe -i ./src/index.js --temp /tmp/nexe -r './**/*' -t windows-x86-12.18.2 --output $WIN32
mv $WIN32 ../
fi
npx nexe -i ./src/index.js --temp /tmp/nexe -r './**/*' -t linux-x64-12.16.2 --output $LINUX64 || exit 1
echo dist/$LINUX64
if [ "$MODE" == "all" ]; then
mv ../$WIN64 ./
mv ../$WIN32 ./
mv ../$MACOSX ./
echo dist/$WIN64
echo dist/$MACOSX
echo dist/$WIN32
fi

View File

@@ -1,82 +0,0 @@
const angular = require('angular')
require('angular-router-browserify')(angular)
require('./ext/lazyload')(angular)
require('./ext/dragdrop')
require('./ext/angularjs-scroll-glue')
require('angular-vs-repeat');
var app = angular.module('myApp', ['ngRoute', 'vs-repeat', 'angularLazyImg', 'dndLists', 'luegg.directives'])
app.service('plex', require('./services/plex'))
app.service('dizquetv', require('./services/dizquetv'))
app.service('resolutionOptions', require('./services/resolution-options'))
app.service('getShowData', require('./services/get-show-data'))
app.service('commonProgramTools', require('./services/common-program-tools'))
app.directive('plexSettings', require('./directives/plex-settings'))
app.directive('ffmpegSettings', require('./directives/ffmpeg-settings'))
app.directive('xmltvSettings', require('./directives/xmltv-settings'))
app.directive('hdhrSettings', require('./directives/hdhr-settings'))
app.directive('plexLibrary', require('./directives/plex-library'))
app.directive('programConfig', require('./directives/program-config'))
app.directive('flexConfig', require('./directives/flex-config'))
app.directive('timeSlotsTimeEditor', require('./directives/time-slots-time-editor'))
app.directive('toastNotifications', require('./directives/toast-notifications'))
app.directive('fillerConfig', require('./directives/filler-config'))
app.directive('showConfig', require('./directives/show-config'))
app.directive('deleteFiller', require('./directives/delete-filler'))
app.directive('frequencyTweak', require('./directives/frequency-tweak'))
app.directive('removeShows', require('./directives/remove-shows'))
app.directive('channelRedirect', require('./directives/channel-redirect'))
app.directive('plexServerEdit', require('./directives/plex-server-edit'))
app.directive('channelConfig', require('./directives/channel-config'))
app.directive('timeSlotsScheduleEditor', require('./directives/time-slots-schedule-editor'))
app.directive('randomSlotsScheduleEditor', require('./directives/random-slots-schedule-editor'))
app.controller('settingsCtrl', require('./controllers/settings'))
app.controller('channelsCtrl', require('./controllers/channels'))
app.controller('versionCtrl', require('./controllers/version'))
app.controller('libraryCtrl', require('./controllers/library'))
app.controller('guideCtrl', require('./controllers/guide'))
app.controller('playerCtrl', require('./controllers/player'))
app.controller('fillerCtrl', require('./controllers/filler'))
app.controller('customShowsCtrl', require('./controllers/custom-shows'))
app.config(function ($routeProvider) {
$routeProvider
.when("/settings", {
templateUrl: "views/settings.html",
controller: 'settingsCtrl'
})
.when("/channels", {
templateUrl: "views/channels.html",
controller: 'channelsCtrl'
})
.when("/filler", {
templateUrl: "views/filler.html",
controller: 'fillerCtrl'
})
.when("/custom-shows", {
templateUrl: "views/custom-shows.html",
controller: 'customShowsCtrl'
})
.when("/library", {
templateUrl: "views/library.html",
controller: 'libraryCtrl'
})
.when("/guide", {
templateUrl: "views/guide.html",
controller: 'guideCtrl'
})
.when("/player", {
templateUrl: "views/player.html",
controller: 'playerCtrl'
})
.when("/version", {
templateUrl: "views/version.html",
controller: 'versionCtrl'
})
.otherwise({
redirectTo: "guide"
})
})

View File

@@ -1,99 +0,0 @@
module.exports = function ($scope, dizquetv) {
$scope.channels = [];
$scope.showChannelConfig = false;
$scope.selectedChannel = null;
$scope.selectedChannelIndex = -1;
$scope.refreshChannels = async () => {
$scope.channels = [{ number: 1, pending: true }];
let channelNumbers = await dizquetv.getChannelNumbers();
$scope.channels = channelNumbers.map((x) => {
return {
number: x,
pending: true,
};
});
$scope.$apply();
$scope.queryChannels();
};
$scope.refreshChannels();
$scope.queryChannels = () => {
for (let i = 0; i < $scope.channels.length; i++) {
$scope.queryChannel(i, $scope.channels[i]);
}
};
$scope.queryChannel = async (index, channel) => {
let ch = await dizquetv.getChannelDescription(channel.number);
ch.pending = false;
$scope.channels[index] = ch;
$scope.$apply();
};
$scope.removeChannel = async ($index, channel) => {
if (confirm('Are you sure to delete channel: ' + channel.name + '?')) {
$scope.channels[$index].pending = true;
await dizquetv.removeChannel(channel);
$scope.refreshChannels();
}
};
$scope.onChannelConfigDone = async (channel) => {
if ($scope.selectedChannelIndex != -1) {
$scope.channels[$scope.selectedChannelIndex].pending = false;
}
if (typeof channel !== 'undefined') {
if ($scope.selectedChannelIndex == -1) {
// add new channel
await dizquetv.addChannel(channel);
$scope.showChannelConfig = false;
$scope.refreshChannels();
} else if (
typeof $scope.originalChannelNumber !== 'undefined' &&
$scope.originalChannelNumber != channel.number
) {
//update + change channel number.
$scope.channels[$scope.selectedChannelIndex].pending = true;
await dizquetv.updateChannel(channel),
await dizquetv.removeChannel({
number: $scope.originalChannelNumber,
});
$scope.showChannelConfig = false;
$scope.$apply();
$scope.refreshChannels();
} else {
// update existing channel
$scope.channels[$scope.selectedChannelIndex].pending = true;
await dizquetv.updateChannel(channel);
$scope.showChannelConfig = false;
$scope.$apply();
$scope.refreshChannels();
}
} else {
$scope.showChannelConfig = false;
}
};
$scope.selectChannel = async (index) => {
if (index === -1 || $scope.channels[index].pending) {
$scope.originalChannelNumber = undefined;
$scope.selectedChannel = null;
$scope.selectedChannelIndex = -1;
$scope.showChannelConfig = true;
} else {
$scope.channels[index].pending = true;
let p = await Promise.all([
dizquetv.getChannelProgramless($scope.channels[index].number),
dizquetv.getChannelPrograms($scope.channels[index].number),
]);
let ch = p[0];
ch.programs = p[1];
let newObj = ch;
newObj.startTimeEpoch = new Date(newObj.startTime).getTime();
$scope.originalChannelNumber = newObj.number;
$scope.selectedChannel = newObj;
$scope.selectedChannelIndex = index;
$scope.showChannelConfig = true;
$scope.$apply();
}
};
};

View File

@@ -1,90 +0,0 @@
module.exports = function ($scope, $timeout, dizquetv) {
$scope.showss = []
$scope.showShowConfig = false
$scope.selectedShow = null
$scope.selectedShowIndex = -1
$scope.refreshShow = async () => {
$scope.shows = [ { id: '?', pending: true} ]
$timeout();
let shows = await dizquetv.getAllShowsInfo();
$scope.shows = shows;
$timeout();
}
$scope.refreshShow();
let feedToShowConfig = () => {};
let feedToDeleteShow = feedToShowConfig;
$scope.registerShowConfig = (feed) => {
feedToShowConfig = feed;
}
$scope.registerDeleteShow = (feed) => {
feedToDeleteShow = feed;
}
$scope.queryChannel = async (index, channel) => {
let ch = await dizquetv.getChannelDescription(channel.number);
ch.pending = false;
$scope.shows[index] = ch;
$scope.$apply();
}
$scope.onShowConfigDone = async (show) => {
if ($scope.selectedChannelIndex != -1) {
$scope.shows[ $scope.selectedChannelIndex ].pending = false;
}
if (typeof show !== 'undefined') {
// not canceled
if ($scope.selectedChannelIndex == -1) { // add new channel
await dizquetv.createShow(show);
} else {
$scope.shows[ $scope.selectedChannelIndex ].pending = true;
await dizquetv.updateShow(show.id, show);
}
await $scope.refreshShow();
}
}
$scope.selectShow = async (index) => {
try {
if ( (index != -1) && $scope.shows[index].pending) {
return;
}
$scope.selectedChannelIndex = index;
if (index === -1) {
feedToShowConfig();
} else {
$scope.shows[index].pending = true;
let f = await dizquetv.getShow($scope.shows[index].id);
feedToShowConfig(f);
$timeout();
}
} catch( err ) {
console.error("Could not fetch show.", err);
}
}
$scope.deleteShow = async (index) => {
try {
if ( $scope.shows[index].pending) {
return;
}
let show = $scope.shows[index];
if (confirm("Are you sure to delete show: " + show.name + "? This will NOT delete the show's programs from channels that are using.")) {
show.pending = true;
await dizquetv.deleteShow(show.id);
$timeout();
await $scope.refreshShow();
$timeout();
}
} catch (err) {
console.error("Could not delete show.", err);
}
}
}

View File

@@ -1,107 +0,0 @@
module.exports = function ($scope, $timeout, dizquetv) {
$scope.fillers = []
$scope.showFillerConfig = false
$scope.selectedFiller = null
$scope.selectedFillerIndex = -1
$scope.refreshFiller = async () => {
$scope.fillers = [ { id: '?', pending: true} ]
$timeout();
let fillers = await dizquetv.getAllFillersInfo();
$scope.fillers = fillers;
$timeout();
}
$scope.refreshFiller();
let feedToFillerConfig = () => {};
let feedToDeleteFiller = feedToFillerConfig;
$scope.registerFillerConfig = (feed) => {
feedToFillerConfig = feed;
}
$scope.registerDeleteFiller = (feed) => {
feedToDeleteFiller = feed;
}
$scope.queryChannel = async (index, channel) => {
let ch = await dizquetv.getChannelDescription(channel.number);
ch.pending = false;
$scope.fillers[index] = ch;
$scope.$apply();
}
$scope.onFillerConfigDone = async (filler) => {
if ($scope.selectedChannelIndex != -1) {
$scope.fillers[ $scope.selectedChannelIndex ].pending = false;
}
if (typeof filler !== 'undefined') {
// not canceled
if ($scope.selectedChannelIndex == -1) { // add new channel
await dizquetv.createFiller(filler);
} else {
$scope.fillers[ $scope.selectedChannelIndex ].pending = true;
await dizquetv.updateFiller(filler.id, filler);
}
await $scope.refreshFiller();
}
}
$scope.selectFiller = async (index) => {
try {
if ( (index != -1) && $scope.fillers[index].pending) {
return;
}
$scope.selectedChannelIndex = index;
if (index === -1) {
feedToFillerConfig();
} else {
$scope.fillers[index].pending = true;
let f = await dizquetv.getFiller($scope.fillers[index].id);
feedToFillerConfig(f);
$timeout();
}
} catch( err ) {
console.error("Could not fetch filler.", err);
}
}
$scope.deleteFiller = async (index) => {
try {
if ( $scope.fillers[index].pending) {
return;
}
$scope.deleteFillerIndex = index;
$scope.fillers[index].pending = true;
let id = $scope.fillers[index].id;
let channels = await dizquetv.getChannelsUsingFiller(id);
feedToDeleteFiller( {
id: id,
name: $scope.fillers[index].name,
channels : channels,
} );
$timeout();
} catch (err) {
console.error("Could not start delete filler dialog.", err);
}
}
$scope.onFillerDelete = async( id ) => {
try {
$scope.fillers[ $scope.deleteFillerIndex ].pending = false;
$timeout();
if (typeof(id) !== 'undefined') {
$scope.fillers[ $scope.deleteFillerIndex ].pending = true;
await dizquetv.deleteFiller(id);
$timeout();
await $scope.refreshFiller();
$timeout();
}
} catch (err) {
console.error("Error attempting to delete filler", err);
}
}
}

View File

@@ -1,370 +0,0 @@
const MINUTE = 60 * 1000;
module.exports = function ($scope, $timeout, dizquetv) {
$scope.offset = 0;
$scope.M = 60 * MINUTE;
$scope.zoomLevel = 3;
$scope.T = 190 * MINUTE;
$scope.before = 15 * MINUTE;
$scope.enableNext = false;
$scope.enableBack = false;
$scope.showNow = false;
$scope.nowPosition = 0;
$scope.refreshHandle = null;
const intl = new Intl.DateTimeFormat('default', {
hour12: true,
hour: 'numeric',
minute: 'numeric',
});
let hourMinute = (d) => {
return intl.format(d);
};
$scope.updateBasics = () => {
$scope.channelNumberWidth = 5;
$scope.channelIconWidth = 8;
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
//we want 1 minute = 1 colspan
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor(
$scope.channelWidth / $scope.colspanPercent,
);
$scope.channelNumberColspan = Math.floor(
$scope.channelNumberWidth / $scope.colspanPercent,
);
$scope.channelIconColspan =
$scope.channelColspan - $scope.channelNumberColspan;
$scope.totalSpan = Math.floor($scope.T / MINUTE);
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor(
$scope.channelWidth / $scope.colspanPercent,
);
$scope.channelNumberColspan = Math.floor(
$scope.channelNumberWidth / $scope.colspanPercent,
);
$scope.channelIconColspan =
$scope.channelColspan - $scope.channelNumberColspan;
};
$scope.updateBasics();
$scope.channelNumberWidth = 5;
$scope.channelIconWidth = 8;
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
//we want 1 minute = 1 colspan
$scope.applyLater = () => {
$timeout(() => $scope.$apply(), 0);
};
$scope.channelNumbers = [];
$scope.channels = {};
$scope.lastUpdate = -1;
$scope.updateJustNow = () => {
$scope.t1 = new Date().getTime();
if ($scope.t0 <= $scope.t1 && $scope.t1 < $scope.t0 + $scope.T) {
let n = ($scope.t1 - $scope.t0) / MINUTE;
$scope.nowPosition = ($scope.channelColspan + n) * $scope.colspanPercent;
if ($scope.nowPosition >= 50 && $scope.offset >= 0) {
$scope.offset = 0;
$scope.adjustZoom();
}
$scope.showNow = true;
} else {
$scope.showNow = false;
}
};
$scope.nowTimer = () => {
$scope.updateJustNow();
$timeout(() => $scope.nowTimer(), 10000);
};
$timeout(() => $scope.nowTimer(), 10000);
$scope.refreshManaged = async (skipStatus) => {
$scope.t1 = new Date().getTime();
$scope.t1 = $scope.t1 - ($scope.t1 % MINUTE);
$scope.t0 = $scope.t1 - $scope.before + $scope.offset;
$scope.title = 'TV Guide';
$scope.times = [];
$scope.updateJustNow();
let pending = 0;
let addDuration = (d) => {
let m = (pending + d) % MINUTE;
let r = pending + d - m;
pending = m;
return Math.floor(r / MINUTE);
};
let deleteIfZero = () => {
if (
$scope.times.length > 0 &&
$scope.times[$scope.times.length - 1].duration < 1
) {
$scope.times = $scope.times.slice(0, $scope.times.length - 1);
}
};
let rem = $scope.T;
let t = $scope.t0;
if (t % $scope.M != 0) {
let dif = $scope.M - (t % $scope.M);
$scope.times.push({
duration: addDuration(dif),
});
deleteIfZero();
t += dif;
rem -= dif;
}
while (rem > 0) {
let d = Math.min(rem, $scope.M);
$scope.times.push({
duration: addDuration(d),
label: hourMinute(new Date(t)),
});
t += d;
rem -= d;
}
if (skipStatus !== true) {
$scope.channelNumbers = [0];
$scope.channels = {};
$scope.channels[0] = {
loading: true,
};
$scope.applyLater();
console.log('getting status...');
let status = await dizquetv.getGuideStatus();
$scope.lastUpdate = new Date(status.lastUpdate).getTime();
console.log('got status: ' + JSON.stringify(status));
$scope.channelNumbers = status.channelNumbers;
$scope.channels = {};
}
for (let i = 0; i < $scope.channelNumbers.length; i++) {
if (typeof $scope.channels[$scope.channelNumbers[i]] === 'undefined') {
$scope.channels[$scope.channelNumbers[i]] = {};
}
$scope.channels[$scope.channelNumbers[i]].loading = true;
}
$scope.applyLater();
$scope.enableBack = false;
$scope.enableNext = false;
await Promise.all($scope.channelNumbers.map($scope.loadChannel));
setupTimer();
};
let cancelTimerIfExists = () => {
if ($scope.refreshHandle != null) {
$timeout.cancel($scope.refreshHandle);
}
};
$scope.$on('$locationChangeStart', () => {
console.log('$locationChangeStart');
cancelTimerIfExists();
});
let setupTimer = () => {
cancelTimerIfExists();
$scope.refreshHandle = $timeout(() => $scope.checkUpdates(), 60000);
};
$scope.adjustZoom = async () => {
switch ($scope.zoomLevel) {
case 1:
$scope.T = 50 * MINUTE;
$scope.M = 10 * MINUTE;
$scope.before = 5 * MINUTE;
break;
case 2:
$scope.T = 100 * MINUTE;
$scope.M = 15 * MINUTE;
$scope.before = 10 * MINUTE;
break;
case 3:
$scope.T = 190 * MINUTE;
$scope.M = 30 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 4:
$scope.T = 270 * MINUTE;
$scope.M = 60 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 5:
$scope.T = 380 * MINUTE;
$scope.M = 90 * MINUTE;
$scope.before = 15 * MINUTE;
break;
}
$scope.updateBasics();
await $scope.refresh(true);
};
$scope.zoomOut = async () => {
$scope.zoomLevel = Math.min(5, $scope.zoomLevel + 1);
await $scope.adjustZoom();
};
$scope.zoomIn = async () => {
$scope.zoomLevel = Math.max(1, $scope.zoomLevel - 1);
await $scope.adjustZoom();
};
$scope.zoomOutEnabled = () => {
return $scope.zoomLevel < 5;
};
$scope.zoomInEnabled = () => {
return $scope.zoomLevel > 1;
};
$scope.next = async () => {
$scope.offset += ($scope.M * 7) / 8;
await $scope.adjustZoom();
};
$scope.back = async () => {
$scope.offset -= ($scope.M * 7) / 8;
await $scope.adjustZoom();
};
$scope.backEnabled = () => {
return $scope.enableBack;
};
$scope.nextEnabled = () => {
return $scope.enableNext;
};
$scope.loadChannel = async (number) => {
console.log(`number=${number}`);
let d0 = new Date($scope.t0);
let d1 = new Date($scope.t0 + $scope.T);
let lineup = await dizquetv.getChannelLineup(number, d0, d1);
let ch = {
icon: lineup.icon,
number: lineup.number,
name: lineup.name,
altTitle: `${lineup.number} - ${lineup.name}`,
programs: [],
};
let pending = 0;
let totalAdded = 0;
let addDuration = (d) => {
totalAdded += d;
let m = (pending + d) % MINUTE;
let r = pending + d - m;
pending = m;
return Math.floor(r / MINUTE);
};
let deleteIfZero = () => {
if (
ch.programs.length > 0 &&
ch.programs[ch.programs.length - 1].duration < 1
) {
ch.programs = ch.programs.slice(0, ch.programs.length - 1);
}
};
for (let i = 0; i < lineup.programs.length; i++) {
let program = lineup.programs[i];
let ad = new Date(program.start);
let bd = new Date(program.stop);
let a = ad.getTime();
let b = bd.getTime();
let hasStart = true;
let hasStop = true;
if (a < $scope.t0) {
//cut-off
a = $scope.t0;
hasStart = false;
$scope.enableBack = true;
} else if (a > $scope.t0 && i == 0) {
ch.programs.push({
duration: addDuration(a - $scope.t0),
showTitle: '',
start: false,
end: true,
});
deleteIfZero();
}
if (b > $scope.t0 + $scope.T) {
b = $scope.t0 + $scope.T;
hasStop = false;
$scope.enableNext = true;
}
let subTitle = undefined;
let episodeTitle = undefined;
let altTitle = hourMinute(ad) + '-' + hourMinute(bd);
if (typeof program.title !== 'undefined') {
altTitle = altTitle + ' · ' + program.title;
}
if (typeof program.sub !== 'undefined') {
ps = '' + program.sub.season;
if (ps.length < 2) {
ps = '0' + ps;
}
pe = '' + program.sub.episode;
if (pe.length < 2) {
pe = '0' + pe;
}
subTitle = `S${ps} · E${pe}`;
altTitle = altTitle + ' ' + subTitle;
episodeTitle = program.sub.title;
} else if (typeof program.date === 'undefined') {
subTitle = '.';
} else {
subTitle = program.date.slice(0, 4);
}
ch.programs.push({
duration: addDuration(b - a),
altTitle: altTitle,
showTitle: program.title,
subTitle: subTitle,
episodeTitle: episodeTitle,
start: hasStart,
end: hasStop,
});
deleteIfZero();
}
if (totalAdded < $scope.T) {
ch.programs.push({
duration: addDuration($scope.T - totalAdded),
showTitle: '',
start: false,
end: true,
});
deleteIfZero();
}
$scope.channels[number] = ch;
$scope.applyLater();
};
$scope.refresh = async (skipStatus) => {
try {
await $scope.refreshManaged(skipStatus);
} catch (err) {
console.error('Refresh failed?', err);
}
};
$scope.adjustZoom();
$scope.refresh();
$scope.checkUpdates = async () => {
try {
console.log('get status ' + new Date());
let status = await dizquetv.getGuideStatus();
let t = new Date(status.lastUpdate).getTime();
if (t > $scope.lastUpdate) {
$scope.refreshManaged();
} else {
setupTimer();
}
} catch (err) {
console.error(err);
}
};
};

View File

@@ -1,2 +0,0 @@
module.exports = function () {
}

View File

@@ -1,73 +0,0 @@
module.exports = function ($scope, dizquetv, $timeout) {
$scope.loading = true;
$scope.channelOptions = [
{ id: undefined, description: "Select a channel" },
];
$scope.icons = {};
$scope.endpointOptions = [
{ id: "video", description: "/video - Channel mpegts" },
{ id: "m3u8", description: "/m3u8 - Playlist of individual videos" },
{ id: "radio", description: "/radio - Audio-only channel mpegts" },
];
$scope.selectedEndpoint = "video";
$scope.channel = undefined;
$scope.endpointButtonHref = () => {
if ( $scope.selectedEndpoint == "video") {
return `./media-player/${$scope.channel}.m3u`
} else if ( $scope.selectedEndpoint == "m3u8") {
return `./media-player/fast/${$scope.channel}.m3u`
} else if ( $scope.selectedEndpoint == "radio") {
return `./media-player/radio/${$scope.channel}.m3u`
}
}
$scope.buttonDisabled = () => {
return typeof($scope.channel) === 'undefined';
}
$scope.endpoint = () => {
if ( typeof($scope.channel) === 'undefined' ) {
return "--"
}
let path = "";
if ( $scope.selectedEndpoint == "video") {
path = `/video?channel=${$scope.channel}`
} else if ( $scope.selectedEndpoint == "m3u8") {
path = `/m3u8?channel=${$scope.channel}`
} else if ( $scope.selectedEndpoint == "radio") {
path= `/radio?channel=${$scope.channel}`
}
return window.location.href.replace("/#!/player", path);
}
let loadChannels = async() => {
let channelNumbers = await dizquetv.getChannelNumbers();
try {
await Promise.all( channelNumbers.map( async(x) => {
let desc = await dizquetv.getChannelDescription(x);
let option = {
id: x,
description: `${x} - ${desc.name}`,
};
$scope.channelOptions.push( option );
$scope.icons[x] = desc.icon;
}) );
$scope.channelOptions.sort( (a,b) => {
let za = ( (typeof(a.id) === undefined)?-1:a.id);
let zb = ( (typeof(b.id) === undefined)?-1:b.id);
return za - zb;
} );
$scope.loading = false;
$scope.$apply();
} catch (err) {
console.error(err);
}
$timeout( () => $scope.$apply(), 0);
}
loadChannels();
}

View File

@@ -1,5 +0,0 @@
module.exports = function ($scope, $location) {
$scope.selected = $location.hash()
if ($scope.selected === '')
$scope.selected = 'xmltv'
}

View File

@@ -1,11 +0,0 @@
module.exports = function ($scope, dizquetv) {
$scope.version = ""
$scope.ffmpegVersion = ""
dizquetv.getVersion().then((version) => {
$scope.version = version.dizquetv;
$scope.ffmpegVersion = version.ffmpeg;
$scope.nodejs = version.nodejs;
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +0,0 @@
module.exports = function ($timeout, dizquetv) {
return {
restrict: 'E',
templateUrl: 'templates/channel-redirect.html',
replace: true,
scope: {
formTitle: "=formTitle",
visible: "=visible",
program: "=program",
_onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.error = "";
scope.options = [];
scope.loading = true;
scope.$watch('program', () => {
if (typeof(scope.program) === 'undefined') {
return;
}
if ( isNaN(scope.program.duration) ) {
scope.program.duration = 15000;
}
scope.durationSeconds = Math.ceil( scope.program.duration / 1000.0 );;
})
scope.refreshChannels = async() => {
let channelNumbers = await dizquetv.getChannelNumbers();
try {
await Promise.all( channelNumbers.map( async(x) => {
let desc = await dizquetv.getChannelDescription(x);
let option = {
id: x,
description: `${x} - ${desc.name}`,
};
let i = 0;
while (i < scope.options.length) {
if (scope.options[i].id == x) {
scope.options[i] = option;
break;
}
i++;
}
if (i == scope.options.length) {
scope.options.push(option);
}
scope.$apply();
}) );
} catch (err) {
console.error(err);
}
scope.options.sort( (a,b) => a.id - b.id );
scope.loading = false;
$timeout( () => scope.$apply(), 0);
};
scope.refreshChannels();
scope.onCancel = () => {
scope.visible = false;
}
scope.onDone = () => {
scope.error = "";
if (typeof(scope.program.channel) === 'undefined') {
scope.error = "Please select a channel.";
}
if ( isNaN(scope.program.channel) ) {
scope.error = "Channel must be a number.";
}
if ( isNaN(scope.durationSeconds) ) {
scope.error = "Duration must be a number.";
}
if ( scope.error != "" ) {
$timeout( () => scope.error = "", 60000);
return;
}
scope.program.duration = scope.durationSeconds * 1000;
scope._onDone( scope.program );
scope.visible = false;
};
}
};
}

View File

@@ -1,32 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/delete-filler.html',
replace: true,
scope: {
linker: "=linker",
onExit: "=onExit"
},
link: function (scope, element, attrs) {
scope.name = '';
scope.channels = [];
scope.visible = false;
scope.linker( (filler) => {
scope.name = filler.name;
scope.id = filler.id;
scope.channels = filler.channels;
scope.visible = true;
} );
scope.finished = (cancelled) => {
scope.visible = false;
if (! cancelled) {
scope.onExit( scope.id );
} else {
scope.onExit();
}
}
}
};
}

View File

@@ -1,95 +0,0 @@
module.exports = function (dizquetv, resolutionOptions) {
return {
restrict: 'E',
templateUrl: 'templates/ffmpeg-settings.html',
replace: true,
scope: {},
link: function (scope, element, attrs) {
//add validations to ffmpeg settings, speciall commas in codec name
dizquetv.getFfmpegSettings().then((settings) => {
scope.settings = settings;
scope.resolutionString = `${scope.settings.targetResolution.widthPx}x${scope.settings.targetResolution.heightPx}`;
});
scope.updateSettings = (settings) => {
dizquetv.updateFfmpegSettings(settings).then((_settings) => {
scope.settings = _settings;
});
};
scope.resetSettings = (settings) => {
dizquetv.resetFfmpegSettings(settings).then((_settings) => {
scope.settings = _settings;
});
};
scope.isTranscodingNotNeeded = () => {
return (
typeof scope.settings === 'undefined' ||
!scope.settings.enableTranscoding
);
};
scope.hideIfNotAutoPlay = () => {
return scope.settings.enableAutoPlay != true;
};
scope.resolutionOptions = resolutionOptions.get();
scope.$watch('resolutionString', (value) => {
if (!value) return;
const [w, h] = value.split('x', 2);
if (scope.settings) {
scope.settings.targetResolution = {
widthPx: w,
heightPx: h,
};
}
});
scope.muxDelayOptions = [
{ id: '0', description: '0 Seconds' },
{ id: '1', description: '1 Seconds' },
{ id: '2', description: '2 Seconds' },
{ id: '3', description: '3 Seconds' },
{ id: '4', description: '4 Seconds' },
{ id: '5', description: '5 Seconds' },
{ id: '10', description: '10 Seconds' },
];
scope.errorScreens = [
{ value: 'pic', description: 'images/generic-error-screen.png' },
{ value: 'blank', description: 'Blank Screen' },
{ value: 'static', description: 'Static' },
{ value: 'testsrc', description: 'Test Pattern (color bars + timer)' },
{
value: 'text',
description: 'Detailed error (requires ffmpeg with drawtext)',
},
{ value: 'kill', description: 'Stop stream, show errors in logs' },
];
scope.errorAudios = [
{ value: 'whitenoise', description: 'White Noise' },
{ value: 'sine', description: 'Beep' },
{ value: 'silent', description: 'No Audio' },
];
scope.fpsOptions = [
{ id: 23.976, description: '23.976 frames per second' },
{ id: 24, description: '24 frames per second' },
{ id: 25, description: '25 frames per second' },
{ id: 29.97, description: '29.97 frames per second' },
{ id: 30, description: '30 frames per second' },
{ id: 50, description: '50 frames per second' },
{ id: 59.94, description: '59.94 frames per second' },
{ id: 60, description: '60 frames per second' },
{ id: 120, description: '120 frames per second' },
];
scope.scalingOptions = [
{ id: 'bicubic', description: 'bicubic (default)' },
{ id: 'fast_bilinear', description: 'fast_bilinear' },
{ id: 'lanczos', description: 'lanczos' },
{ id: 'spline', description: 'spline' },
];
scope.deinterlaceOptions = [
{ value: 'none', description: 'do not deinterlace' },
{ value: 'bwdif=0', description: 'bwdif send frame' },
{ value: 'bwdif=1', description: 'bwdif send field' },
{ value: 'w3fdif', description: 'w3fdif' },
{ value: 'yadif=0', description: 'yadif send frame' },
{ value: 'yadif=1', description: 'yadif send field' },
];
},
};
};

View File

@@ -1,185 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/filler-config.html',
replace: true,
scope: {
linker: "=linker",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.showTools = false;
scope.showPlexLibrary = false;
scope.content = [];
scope.visible = false;
scope.error = undefined;
function refreshContentIndexes() {
for (let i = 0; i < scope.content.length; i++) {
scope.content[i].$index = i;
}
}
scope.contentSplice = (a,b) => {
scope.content.splice(a,b)
refreshContentIndexes();
}
scope.dropFunction = (dropIndex, program) => {
let y = program.$index;
let z = dropIndex + scope.currentStartIndex - 1;
scope.content.splice(y, 1);
if (z >= y) {
z--;
}
scope.content.splice(z, 0, program );
refreshContentIndexes();
$timeout();
return false;
}
scope.setUpWatcher = function setupWatchers() {
this.$watch('vsRepeat.startIndex', function(val) {
scope.currentStartIndex = val;
});
};
scope.movedFunction = (index) => {
console.log("movedFunction(" + index + ")");
}
scope.linker( (filler) => {
if ( typeof(filler) === 'undefined') {
scope.name = "";
scope.content = [];
scope.id = undefined;
scope.title = "Create Filler List";
} else {
scope.name = filler.name;
scope.content = filler.content;
scope.id = filler.id;
scope.title = "Edit Filler List";
}
refreshContentIndexes();
scope.visible = true;
} );
scope.finished = (cancelled) => {
if (cancelled) {
scope.visible = false;
return scope.onDone();
}
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 (typeof(scope.error) !== 'undefined') {
$timeout( () => {
scope.error = undefined;
}, 30000);
return;
}
scope.visible = false;
scope.onDone( {
name: scope.name,
content: scope.content.map( (c) => {
delete c.$index
return c;
} ),
id: scope.id,
} );
}
scope.showList = () => {
return ! scope.showPlexLibrary;
}
scope.sortFillers = () => {
scope.content.sort( (a,b) => { return a.duration - b.duration } );
refreshContentIndexes();
}
scope.fillerRemoveAllFiller = () => {
scope.content = [];
refreshContentIndexes();
}
scope.fillerRemoveDuplicates = () => {
function getKey(p) {
return p.serverKey + "|" + p.plexFile;
}
let seen = {};
let newFiller = [];
for (let i = 0; i < scope.content.length; i++) {
let p = scope.content[i];
let k = getKey(p);
if ( typeof(seen[k]) === 'undefined') {
seen[k] = true;
newFiller.push(p);
}
}
scope.content = newFiller;
refreshContentIndexes();
}
scope.importPrograms = (selectedPrograms) => {
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
selectedPrograms[i].commercials = []
}
scope.content = scope.content.concat(selectedPrograms);
refreshContentIndexes();
scope.showPlexLibrary = false;
}
scope.durationString = (duration) => {
var date = new Date(0);
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
return date.toISOString().substr(11, 8);
}
let interpolate = ( () => {
let h = 60*60*1000 / 6;
let ix = [0, 1*h, 2*h, 4*h, 8*h, 24*h];
let iy = [0, 1.0, 1.25, 1.5, 1.75, 2.0];
let n = ix.length;
return (x) => {
for (let i = 0; i < n-1; i++) {
if( (ix[i] <= x) && ( (x < ix[i+1]) || i==n-2 ) ) {
return iy[i] + (iy[i+1] - iy[i]) * ( (x - ix[i]) / (ix[i+1] - ix[i]) );
}
}
}
} )();
scope.programSquareStyle = (program, dash) => {
let background = "rgb(255, 255, 255)";
let ems = Math.pow( Math.min(60*60*1000, program.duration), 0.7 );
ems = ems / Math.pow(1*60*1000., 0.7);
ems = Math.max( 0.25 , ems);
let top = Math.max(0.0, (1.75 - ems) / 2.0) ;
if (top == 0.0) {
top = "1px";
}
let solidOrDash = (dash? 'dashed' : 'solid');
let f = interpolate;
let w = 5.0;
let t = 4*60*60*1000;
let a = ( f(program.duration) *w) / f(t);
a = Math.min( w, Math.max(0.3, a) );
b = w - a + 0.01;
return {
'width': `${a}%`,
'height': '1.3em',
'margin-right': `${b}%`,
'background': background,
'border': `1px ${solidOrDash} black`,
'margin-top': top,
'margin-bottom': '1px',
};
}
}
};
}

View File

@@ -1,46 +0,0 @@
module.exports = function ($timeout, dizquetv) {
return {
restrict: 'E',
templateUrl: 'templates/flex-config.html',
replace: true,
scope: {
title: "@offlineTitle",
program: "=program",
visible: "=visible",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
let updateNext = true;
scope.$watch('program', () => {
try {
if ( (typeof(scope.program) === 'undefined') || (scope.program == null) ) {
updateNext = true;
return;
} else if (! updateNext) {
return;
}
updateNext = false;
scope.error = null;
} catch (err) {
console.error(err);
}
})
scope.finished = (prog) => {
scope.error = null;
if (isNaN(prog.durationSeconds) || prog.durationSeconds < 0 ) {
scope.error = { duration: 'Duration must be a positive integer' }
}
if (scope.error != null) {
$timeout(() => {
scope.error = null
}, 30000)
return
}
scope.onDone(JSON.parse(angular.toJson(prog)))
scope.program = null
}
}
};
}

View File

@@ -1,24 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/frequency-tweak.html',
replace: true,
scope: {
programs: "=programs",
visible: "=visible",
onDone: "=onDone",
modified: "=modified",
message: "=message",
},
link: function (scope, element, attrs) {
scope.setModified = () => {
scope.modified = true;
}
scope.finished = (programs) => {
let p = programs;
scope.programs = null;
scope.onDone(p);
}
}
};
}

View File

@@ -1,33 +0,0 @@
module.exports = function (dizquetv, $timeout) {
return {
restrict: 'E',
templateUrl: 'templates/hdhr-settings.html',
replace: true,
scope: {
},
link: function (scope, element, attrs) {
dizquetv.getHdhrSettings().then((settings) => {
scope.settings = settings
})
scope.updateSettings = (settings) => {
if (settings.tunerCount == null) {
scope.error = { tunerCount: "Please enter a valid number of tuners." }
} else if (settings.tunerCount <= 0) {
scope.error = { tunerCount: "Tuner count must be greater than 0." }
}
if (scope.error != null)
$timeout(() => {
scope.error = null
}, 3500)
dizquetv.updateHdhrSettings(settings).then((_settings) => {
scope.settings = _settings
})
}
scope.resetSettings = (settings) => {
dizquetv.resetHdhrSettings(settings).then((_settings) => {
scope.settings = _settings
})
}
}
}
}

View File

@@ -1,260 +0,0 @@
module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
return {
restrict: 'E',
templateUrl: 'templates/plex-library.html',
replace: true,
scope: {
onFinish: "=onFinish",
height: "=height",
visible: "=visible",
limit: "=limit",
},
link: function (scope, element, attrs) {
scope.errors=[];
if ( typeof(scope.limit) == 'undefined') {
scope.limit = 1000000000;
}
scope.customShows = [];
scope.origins = [];
scope.currentOrigin = undefined;
scope.pending = 0;
scope.allowedIndexes = [];
for (let i = -10; i <= -1; i++) {
scope.allowedIndexes.push(i);
}
scope.selection = []
scope.wait = (t) => {
return new Promise((resolve, reject) => {
$timeout(resolve,t);
});
}
scope.selectOrigin = function (origin) {
if ( origin.type === 'plex' ) {
scope.plexServer = origin.server;
updateLibrary(scope.plexServer);
} else {
scope.plexServer = undefined;
updateCustomShows();
}
}
scope._onFinish = (s) => {
if (s.length > scope.limit) {
if (scope.limit == 1) {
scope.error = "Please select only one clip.";
} else {
scope.error = `Please select at most ${scope.limit} clips.`;
}
} else {
scope.onFinish(s)
scope.selection = []
scope.visible = false
}
}
scope.selectItem = async (item, single) => {
await scope.wait(0);
scope.pending += 1;
try {
delete item.server;
item.serverKey = scope.plexServer.name;
scope.selection.push(JSON.parse(angular.toJson(item)))
} catch (err) {
let msg = "Unable to add item: " + item.key + " " + item.title;
scope.errors.push(msg);
console.error(msg, err);
} finally {
scope.pending -= 1;
}
if (single) {
scope.$apply()
}
}
scope.selectLibrary = async (library) => {
await scope.fillNestedIfNecessary(library);
let p = library.nested.length;
scope.pending += library.nested.length;
try {
for (let i = 0; i < library.nested.length; i++) {
//await scope.selectItem( library.nested[i] );
if (library.nested[i].type !== 'collection' && library.nested[i].type !== 'genre') {
await scope.selectShow( library.nested[i] );
}
scope.pending -= 1;
p -= 1;
}
} finally {
scope.pending -= p;
scope.$apply()
}
}
dizquetv.getPlexServers().then((servers) => {
if (servers.length === 0) {
scope.noServers = true
return
}
scope.origins = servers.map( (s) => {
return {
"type" : "plex",
"name" : `Plex - ${s.name}`,
"server": s,
}
} );
scope.currentOrigin = scope.origins[0];
scope.plexServer = scope.currentOrigin.server;
scope.origins.push( {
"type": "dizquetv",
"name" : "dizqueTV - Custom Shows",
} );
updateLibrary(scope.plexServer)
})
let updateLibrary = async(server) => {
let lib = await plex.getLibrary(server);
let play = await plex.getPlaylists(server);
play.forEach( p => {
p.type = "playlist";
} );
scope.$apply(() => {
scope.libraries = lib
if (play.length > 0)
scope.libraries.push({ title: "Playlists", key: "", icon: "", nested: play })
})
}
scope.fillNestedIfNecessary = async (x, isLibrary) => {
if (typeof(x.nested) === 'undefined') {
x.nested = await plex.getNested(scope.plexServer, x, isLibrary, scope.errors);
if (x.type === "collection" && x.collectionType === "show") {
let nested = x.nested;
x.nested = [];
for (let i = 0; i < nested.length; i++) {
let subNested = await plex.getNested(scope.plexServer, nested[i], false, scope.errors);
for (let j = 0; j < subNested.length; j++) {
subNested[j].title = nested[i].title + " - " + subNested[j].title;
x.nested.push( subNested[j] );
}
}
}
}
}
scope.getNested = (list, isLibrary) => {
$timeout(async () => {
await scope.fillNestedIfNecessary(list, isLibrary);
list.collapse = !list.collapse
scope.$apply()
}, 0)
}
scope.selectSeason = (season) => {
return new Promise((resolve, reject) => {
$timeout(async () => {
await scope.fillNestedIfNecessary(season);
let p = season.nested.length;
scope.pending += p;
try {
for (let i = 0, l = season.nested.length; i < l; i++) {
await scope.selectItem(season.nested[i], false)
scope.pending -= 1;
p -= 1;
}
resolve();
} catch (e) {
reject(e);
} finally {
scope.pending -= p;
scope.$apply()
}
}, 0)
})
}
scope.selectShow = (show) => {
return new Promise((resolve, reject) => {
$timeout(async () => {
await scope.fillNestedIfNecessary(show);
let p = show.nested.length;
scope.pending += p;
try {
for (let i = 0, l = show.nested.length; i < l; i++) {
await scope.selectSeason(show.nested[i])
scope.pending -= 1;
p -= 1;
}
resolve();
} catch (e) {
reject(e);
} finally {
scope.pending -= p;
scope.$apply()
}
}, 0)
})
}
scope.selectPlaylist = async (playlist) => {
return new Promise((resolve, reject) => {
$timeout(async () => {
await scope.fillNestedIfNecessary(playlist);
for (let i = 0, l = playlist.nested.length; i < l; i++)
await scope.selectItem(playlist.nested[i], false)
scope.$apply()
resolve()
}, 0)
})
}
scope.createShowIdentifier = (season, ep) => {
return 'S' + (season.toString().padStart(2, '0')) + 'E' + (ep.toString().padStart(2, '0'))
}
scope.addCustomShow = async(show) => {
scope.pending++;
try {
show = await dizquetv.getShow(show.id);
for (let i = 0; i < show.content.length; i++) {
let item = JSON.parse(angular.toJson( show.content[i] ));
item.customShowId = show.id;
item.customShowName = show.name;
item.customOrder = i;
scope.selection.push(item);
}
scope.$apply();
} finally {
scope.pending--;
}
}
scope.getProgramDisplayTitle = (x) => {
return commonProgramTools.getProgramDisplayTitle(x);
}
let updateCustomShows = async() => {
scope.customShows = await dizquetv.getAllShowsInfo();
scope.$apply();
}
scope.displayTitle = (show) => {
let r = "";
if (show.type === 'episode') {
r += show.showTitle + " - ";
if ( typeof(show.season) !== 'undefined' ) {
r += "S" + show.season.toString().padStart(2,'0');
}
if ( typeof(show.episode) !== 'undefined' ) {
r += "E" + show.episode.toString().padStart(2,'0');
}
}
if (r != "") {
r = r + " - ";
}
r += show.title;
if (
(show.type !== 'episode')
&&
(typeof(show.year) !== 'undefined')
) {
r += " (" + JSON.stringify(show.year) + ")";
}
return r;
}
}
};
}

View File

@@ -1,76 +0,0 @@
module.exports = function (dizquetv, $timeout) {
return {
restrict: 'E',
templateUrl: 'templates/plex-server-edit.html',
replace: true,
scope: {
state: "=state",
_onFinish: "=onFinish",
},
link: function (scope, element, attrs) {
scope.state.modified = false;
scope.loading = { show: false };
scope.setModified = () => {
scope.state.modified = true;
}
scope.onSave = async () => {
try {
scope.loading = { show: true };
await dizquetv.updatePlexServer(scope.state.server);
scope.state.modified = false;
scope.state.success = "The server was updated.";
scope.state.changesSaved = true;
scope.state.error = "";
} catch (err) {
scope.state.error = "There was an error updating the server";
scope.state.success = "";
console.error(scope.state.error, err);
} finally {
scope.loading = { show: false };
}
$timeout( () => { scope.$apply() } , 0 );
}
scope.onDelete = async () => {
try {
let channelReport = await dizquetv.removePlexServer(scope.state.server.name);
scope.state.channelReport = channelReport;
channelReport.sort( (a,b) => {
if (a.destroyedPrograms != b.destroyedPrograms) {
return (b.destroyedPrograms - a.destroyedPrograms);
} else {
return (a.channelNumber - b.channelNumber);
}
});
scope.state.success = "The server was deleted.";
scope.state.error = "";
scope.state.modified = false;
scope.state.changesSaved = true;
} catch (err) {
scope.state.error = "There was an error deleting the server.";
scope.state.success = "";
}
$timeout( () => { scope.$apply() } , 0 );
}
scope.onShowDelete = async () => {
scope.state.showDelete = true;
scope.deleteTime = (new Date()).getTime();
$timeout( () => {
if (scope.deleteTime + 29000 < (new Date()).getTime() ) {
scope.state.showDelete = false;
scope.$apply();
}
}, 30000);
}
scope.onFinish = () => {
scope.state.visible = false;
if (scope.state.changesSaved) {
scope._onFinish();
}
}
}
};
}

View File

@@ -1,306 +0,0 @@
module.exports = function (plex, dizquetv, $timeout) {
return {
restrict: 'E',
templateUrl: 'templates/plex-settings.html',
replace: true,
scope: {},
link: function (scope, element, attrs) {
scope.requestId = 0;
scope._serverToEdit = null;
scope._serverEditorState = {
visible: false,
};
scope.serversPending = true;
scope.channelReport = null;
scope.serverError = '';
scope.refreshServerList = async () => {
scope.serversPending = true;
let servers = await dizquetv.getPlexServers();
scope.serversPending = false;
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);
}
}
setTimeout(() => {
scope.$apply();
}, 31000);
scope.$apply();
};
scope.refreshServerList();
scope.editPlexServer = (server) => {
scope._serverEditorState = {
visible: true,
server: {
name: server.name,
uri: server.uri,
arGuide: server.arGuide,
arChannels: server.arChannels,
accessToken: server.accessToken,
},
};
};
scope.serverEditFinished = () => {
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) {
for (let i = 0; i < scope.servers.length; i++) {
let s = scope.servers[i];
if (
s.backendStatus == -1 ||
(s.backendStatus == 0 && s.backendPending + 30000 < t)
) {
return true;
}
}
}
return false;
};
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) {
// avoid updating for a previous instance of the row
scope.servers[i].backendStatus = s.status;
}
scope.$apply();
};
scope.findGoodConnection = async (server, connections) => {
return await Promise.any(
connections.map(async (connection) => {
let hypothethical = {
name: server.name,
accessToken: server.accessToken,
uri: connection.uri,
};
let q = await Promise.race([
new Promise((resolve, reject) =>
$timeout(() => {
resolve(-1);
}, 60000),
),
(async () => {
let s1 = await plex.check(hypothethical);
let s2 = (await dizquetv.checkNewPlexServer(hypothethical))
.status;
if (s1 == 1 && s2 == 1) {
return 1;
} else {
return -1;
}
})(),
]);
if (q === 1) {
return hypothethical;
} else {
throw Error('Not proper status');
}
}),
);
};
scope.getLocalConnections = (connections) => {
let r = [];
for (let i = 0; i < connections.length; i++) {
if (connections[i].local === true) {
r.push(connections[i]);
}
}
return r;
};
scope.getRemoteConnections = (connections) => {
let r = [];
for (let i = 0; i < connections.length; i++) {
if (connections[i].local !== true) {
r.push(connections[i]);
}
}
return r;
};
scope.shouldDisableSubtitles = () => {
return (
scope.settings &&
(scope.settings.forceDirectPlay ||
scope.settings.streamPath === 'direct')
);
};
scope.addPlexServer = async () => {
scope.isProcessing = true;
scope.serversPending = true;
scope.serverError = '';
let result = await plex.login();
scope.addingServer =
'Looking for servers in the Plex account, please wait...';
await Promise.all(
result.servers.map(async (server) => {
try {
let connections = scope.getLocalConnections(server.connections);
let connection = null;
try {
connection = await scope.findGoodConnection(
server,
connections,
);
} catch (err) {
connection = null;
}
if (connection == null) {
connections = scope.getRemoteConnections(server.connections);
try {
connection = await scope.findGoodConnection(
server,
connections,
);
} catch (err) {
connection = null;
}
}
if (connection == null) {
//pick a random one, really.
connections = scope.getLocalConnections(server.connections);
if (connections.length > 0) {
connection = connections[0];
} else {
connection = server.connections[0];
}
connection = {
name: server.name,
uri: connection.uri,
accessToken: server.accessToken,
};
}
connection.arGuide = false;
connection.arChannels = false; // should not be enabled unless dizqueTV tuner already added to plex
await dizquetv.addPlexServer(connection);
} catch (err) {
scope.serverError =
'Could not add Plex server: There was an error.';
console.error('error adding server', err);
}
}),
);
scope.addingServer = '';
scope.isProcessing = false;
scope.refreshServerList();
};
scope.$watch('settings', (value) => {
if (!value) return;
scope.maxPlayableResString = `${scope.settings.maxPlayableResolution.widthPx}x${scope.settings.maxPlayableResolution.heightPx}`;
scope.maxTranscodeResString = `${scope.settings.maxTranscodeResolution.widthPx}x${scope.settings.maxTranscodeResolution.heightPx}`;
});
scope.$watch('maxPlayableResString', (value) => {
if (!value) return;
if (!scope.settings) return;
const [w, h] = value.split('x', 2);
scope.settings.maxPlayableResolution = {
widthPx: w,
heightPx: h,
};
});
scope.$watch('maxTranscodeResString', (value) => {
if (!value) return;
if (!scope.settings) return;
const [w, h] = value.split('x', 2);
scope.settings.maxTranscodeResolution = {
widthPx: w,
heightPx: h,
};
});
dizquetv.getPlexSettings().then((settings) => {
scope.settings = settings;
});
scope.updateSettings = (settings) => {
dizquetv.updatePlexSettings(settings).then((_settings) => {
scope.settings = _settings;
});
};
scope.resetSettings = (settings) => {
dizquetv.resetPlexSettings(settings).then((_settings) => {
scope.settings = _settings;
});
};
scope.pathOptions = [
{ id: 'plex', description: 'Plex' },
{ id: 'direct', description: 'Direct' },
];
scope.hideIfNotPlexPath = () => {
return scope.settings && scope.settings.streamPath != 'plex';
};
scope.hideIfNotDirectPath = () => {
return scope.settings && scope.settings.streamPath != 'direct';
};
scope.maxAudioChannelsOptions = [
{ id: 1, description: '1.0' },
{ id: 2, description: '2.0' },
{ id: 3, description: '2.1' },
{ id: 4, description: '4.0' },
{ id: 5, description: '5.0' },
{ id: 6, description: '5.1' },
{ id: 7, description: '6.1' },
{ id: 8, description: '7.1' },
];
scope.resolutionOptions = [
{ id: '420x420', description: '420x420' },
{ id: '576x320', description: '576x320' },
{ id: '720x480', description: '720x480' },
{ id: '1024x768', description: '1024x768' },
{ id: '1280x720', description: '1280x720' },
{ id: '1920x1080', description: '1920x1080' },
{ id: '3840x2160', description: '3840x2160' },
];
scope.streamProtocols = [
{ id: 'http', description: 'HTTP' },
{ id: 'hls', description: 'HLS' },
];
scope.audioBoostOptions = [
{ id: 100, description: 'None' },
{ id: 120, description: 'Small' },
{ id: 140, description: 'Medium' },
{ id: 160, description: 'Large' },
{ id: 180, description: 'Huge' },
];
},
};
};

View File

@@ -1,38 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/program-config.html',
replace: true,
scope: {
program: "=program",
visible: "=visible",
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' }
if (scope.error != null) {
$timeout(() => {
scope.error = null
}, 3500)
return
}
scope.onDone(JSON.parse(angular.toJson(prog)))
scope.program = null
}
}
};
}

View File

@@ -1,322 +0,0 @@
module.exports = function ($timeout, dizquetv, getShowData) {
const MINUTE = 60*1000;
const HOUR = 60*MINUTE;
const DAY = 24*HOUR;
const WEEK = 7 * DAY;
return {
restrict: 'E',
templateUrl: 'templates/random-slots-schedule-editor.html',
replace: true,
scope: {
linker: "=linker",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.limit = 50000;
scope.visible = false;
scope.badTimes = false;
scope._editedTime = null;
let showsById;
let shows;
function reset() {
showsById = {};
shows = [];
scope.schedule = {
maxDays: 365,
flexPreference : "distribute",
padStyle: "slot",
randomDistribution: "uniform",
slots : [],
pad: 1,
}
}
reset();
function loadBackup(backup) {
scope.schedule = JSON.parse( JSON.stringify(backup) );
if (typeof(scope.schedule.pad) == 'undefined') {
scope.schedule.pad = 1;
}
let slots = scope.schedule.slots;
for (let i = 0; i < slots.length; i++) {
let found = false;
for (let j = 0; j < scope.showOptions.length; j++) {
if (slots[i].showId == scope.showOptions[j].id) {
found = true;
}
}
if (! found) {
slots[i].showId = "flex.";
slots[i].order = "shuffle";
}
}
if (typeof(scope.schedule.flexPreference) === 'undefined') {
scope.schedule.flexPreference = "distribute";
}
if (typeof(scope.schedule.padStyle) === 'undefined') {
scope.schedule.padStyle = "slot";
}
if (typeof(scope.schedule.randomDistribution) === 'undefined') {
scope.schedule.randomDistribution = "uniform";
}
scope.refreshSlots();
}
getTitle = (index) => {
let showId = scope.schedule.slots[index].showId;
for (let i = 0; i < scope.showOptions.length; i++) {
if (scope.showOptions[i].id == showId) {
return scope.showOptions[i].description;
}
}
return "Unknown";
}
scope.isWeekly = () => {
return (scope.schedule.period === WEEK);
};
scope.addSlot = () => {
scope.schedule.slots.push(
{
duration: 30 * MINUTE,
showId: "flex.",
order: "next",
cooldown : 0,
}
);
}
scope.timeColumnClass = () => {
return { "col-md-1": true};
}
scope.programColumnClass = () => {
return { "col-md-6": true};
};
scope.durationOptions = [
{ id: 5 * MINUTE , description: "5 Minutes" },
{ id: 10 * MINUTE , description: "10 Minutes" },
{ id: 15 * MINUTE , description: "15 Minutes" },
{ id: 20 * MINUTE , description: "20 Minutes" },
{ id: 25 * MINUTE , description: "25 Minutes" },
{ id: 30 * MINUTE , description: "30 Minutes" },
{ id: 45 * MINUTE , description: "45 Minutes" },
{ id: 1 * HOUR , description: "1 Hour" },
{ id: 90 * MINUTE , description: "90 Minutes" },
{ id: 100 * MINUTE , description: "100 Minutes" },
{ id: 2 * HOUR , description: "2 Hours" },
{ id: 3 * HOUR , description: "3 Hours" },
{ id: 4 * HOUR , description: "4 Hours" },
{ id: 5 * HOUR , description: "5 Hours" },
{ id: 6 * HOUR , description: "6 Hours" },
{ id: 8 * HOUR , description: "8 Hours" },
{ id: 10* HOUR , description: "10 Hours" },
{ id: 12* HOUR , description: "12 Hours" },
{ id: 1 * DAY , description: "1 Day" },
];
scope.cooldownOptions = [
{ id: 0 , description: "No cooldown" },
{ id: 1 * MINUTE , description: "1 Minute" },
{ id: 5 * MINUTE , description: "5 Minutes" },
{ id: 10 * MINUTE , description: "10 Minutes" },
{ id: 15 * MINUTE , description: "15 Minutes" },
{ id: 20 * MINUTE , description: "20 Minutes" },
{ id: 25 * MINUTE , description: "25 Minutes" },
{ id: 30 * MINUTE , description: "30 Minutes" },
{ id: 45 * MINUTE , description: "45 Minutes" },
{ id: 1 * HOUR , description: "1 Hour" },
{ id: 90 * MINUTE , description: "90 Minutes" },
{ id: 100 * MINUTE , description: "100 Minutes" },
{ id: 2 * HOUR , description: "2 Hours" },
{ id: 3 * HOUR , description: "3 Hours" },
{ id: 4 * HOUR , description: "4 Hours" },
{ id: 5 * HOUR , description: "5 Hours" },
{ id: 6 * HOUR , description: "6 Hours" },
{ id: 8 * HOUR , description: "8 Hours" },
{ id: 10* HOUR , description: "10 Hours" },
{ id: 12* HOUR , description: "12 Hours" },
{ id: 1 * DAY , description: "1 Day" },
{ id: 1 * DAY , description: "2 Days" },
{ id: 3 * DAY + 12 * HOUR , description: "3.5 Days" },
{ id: 7 * DAY , description: "1 Week" },
];
scope.flexOptions = [
{ id: "distribute", description: "Between videos" },
{ id: "end", description: "End of the slot" },
]
scope.distributionOptions = [
{ id: "uniform", description: "Uniform" },
{ id: "weighted", description: "Weighted" },
]
scope.padOptions = [
{id: 1, description: "Do not pad" },
{id: 1*MINUTE, description: "0:00, 0:01, 0:02, ..., 0:59" },
{id: 5*MINUTE, description: "0:00, 0:05, 0:10, ..., 0:55" },
{id: 10*60*1000, description: "0:00, 0:10, 0:20, ..., 0:50" },
{id: 15*60*1000, description: "0:00, 0:15, 0:30, ..., 0:45" },
{id: 30*60*1000, description: "0:00, 0:30" },
{id: 1*60*60*1000, description: "0:00" },
];
scope.padStyleOptions = [
{id: "episode" , description: "Pad Episodes" },
{id: "slot" , description: "Pad Slots" },
];
scope.showOptions = [];
scope.orderOptions = [
{ id: "next", description: "Play Next" },
{ id: "shuffle", description: "Shuffle" },
];
let doIt = async() => {
let res = await dizquetv.calculateRandomSlots(scope.programs, scope.schedule );
for (let i = 0; i < scope.schedule.slots.length; i++) {
delete scope.schedule.slots[i].weightPercentage;
}
res.schedule = scope.schedule;
return res;
}
let startDialog = (programs, limit, backup) => {
scope.limit = limit;
scope.programs = programs;
reset();
programs.forEach( (p) => {
let show = getShow(p);
if (show != null) {
if (typeof(showsById[show.id]) === 'undefined') {
showsById[show.id] = shows.length;
shows.push( show );
} else {
show = shows[ showsById[show.id] ];
}
}
} );
scope.showOptions = shows.map( (show) => { return show } );
scope.showOptions.push( {
id: "flex.",
description: "Flex",
} );
if (typeof(backup) !== 'undefined') {
loadBackup(backup);
}
scope.visible = true;
}
scope.linker( {
startDialog: startDialog,
} );
scope.finished = async (cancel) => {
scope.error = null;
if (!cancel) {
try {
scope.loading = true;
$timeout();
scope.onDone( await doIt() );
scope.visible = false;
} catch(err) {
console.error("Unable to generate channel lineup", err);
scope.error = "There was an error processing the schedule";
return;
} finally {
scope.loading = false;
$timeout();
}
} else {
scope.visible = false;
}
}
scope.deleteSlot = (index) => {
scope.schedule.slots.splice(index, 1);
}
scope.hasTimeError = (slot) => {
return typeof(slot.timeError) !== 'undefined';
}
scope.disableCreateLineup = () => {
if (scope.badTimes) {
return true;
}
if (typeof(scope.schedule.maxDays) === 'undefined') {
return true;
}
if (scope.schedule.slots.length == 0) {
return true;
}
return false;
}
scope.canShowSlot = (slot) => {
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
}
scope.refreshSlots = () => {
let sum = 0;
for (let i = 0; i < scope.schedule.slots.length; i++) {
sum += scope.schedule.slots[i].weight;
}
for (let i = 0; i < scope.schedule.slots.length; i++) {
if (scope.schedule.slots[i].showId == 'movie.') {
scope.schedule.slots[i].order = "shuffle";
}
if ( isNaN(scope.schedule.slots[i].cooldown) ) {
scope.schedule.slots[i].cooldown = 0;
}
scope.schedule.slots[i].weightPercentage
= (100 * scope.schedule.slots[i].weight / sum).toFixed(2) + "%";
}
$timeout();
}
scope.randomDistributionChanged = () => {
if (scope.schedule.randomDistribution === 'uniform') {
for (let i = 0; i < scope.schedule.slots.length; i++) {
scope.schedule.slots[i].weight = 1;
}
} else {
for (let i = 0; i < scope.schedule.slots.length; i++) {
scope.schedule.slots[i].weight = 300;
}
}
scope.refreshSlots();
}
}
};
function getShow(program) {
let d = getShowData(program);
if (! d.hasShow) {
return null;
} else {
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
}

View File

@@ -1,29 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/remove-shows.html',
replace: true,
scope: {
programInfos: "=programInfos",
visible: "=visible",
onDone: "=onDone",
deleted: "=deleted"
},
link: function (scope, element, attrs) {
scope.toggleShowDeletion = (programId) => {
const deletedIdx = scope.deleted.indexOf(programId);
if (deletedIdx === -1) {
scope.deleted.push(programId);
} else {
scope.deleted.splice(deletedIdx, 1);
}
}
scope.finished = () => {
const d = scope.deleted;
scope.programInfos = null;
scope.deleted = null;
scope.onDone(d);
}
}
};
}

View File

@@ -1,165 +0,0 @@
module.exports = function ($timeout, commonProgramTools) {
return {
restrict: 'E',
templateUrl: 'templates/show-config.html',
replace: true,
scope: {
linker: "=linker",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.showTools = false;
scope.showPlexLibrary = false;
scope.content = [];
scope.visible = false;
scope.error = undefined;
function refreshContentIndexes() {
for (let i = 0; i < scope.content.length; i++) {
scope.content[i].$index = i;
}
}
scope.contentSplice = (a,b) => {
scope.content.splice(a,b)
refreshContentIndexes();
}
scope.dropFunction = (dropIndex, program) => {
let y = program.$index;
let z = dropIndex + scope.currentStartIndex - 1;
scope.content.splice(y, 1);
if (z >= y) {
z--;
}
scope.content.splice(z, 0, program );
refreshContentIndexes();
$timeout();
return false;
}
scope.setUpWatcher = function setupWatchers() {
this.$watch('vsRepeat.startIndex', function(val) {
scope.currentStartIndex = val;
});
};
scope.movedFunction = (index) => {
console.log("movedFunction(" + index + ")");
}
scope.linker( (show) => {
if ( typeof(show) === 'undefined') {
scope.name = "";
scope.content = [];
scope.id = undefined;
scope.title = "Create Custom Show";
} else {
scope.name = show.name;
scope.content = show.content;
scope.id = show.id;
scope.title = "Edit Custom Show";
}
refreshContentIndexes();
scope.visible = true;
} );
scope.finished = (cancelled) => {
if (cancelled) {
scope.visible = false;
return scope.onDone();
}
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 (typeof(scope.error) !== 'undefined') {
$timeout( () => {
scope.error = undefined;
}, 30000);
return;
}
scope.visible = false;
scope.onDone( {
name: scope.name,
content: scope.content.map( (c) => {
delete c.$index
return c;
} ),
id: scope.id,
} );
}
scope.showList = () => {
return ! scope.showPlexLibrary;
}
scope.sortShows = () => {
scope.content = commonProgramTools.sortShows(scope.content);
refreshContentIndexes();
}
scope.sortByDate = () => {
scope.content = commonProgramTools.sortByDate(scope.content);
refreshContentIndexes();
}
scope.shuffleShows = () => {
scope.content = commonProgramTools.shuffle(scope.content);
refreshContentIndexes();
}
scope.showRemoveAllShow = () => {
scope.content = [];
refreshContentIndexes();
}
scope.showRemoveDuplicates = () => {
scope.content = commonProgramTools.removeDuplicates(scope.content);
refreshContentIndexes();
}
scope.getProgramDisplayTitle = (x) => {
return commonProgramTools.getProgramDisplayTitle(x);
}
scope.removeSpecials = () => {
scope.content = commonProgramTools.removeSpecials(scope.content);
refreshContentIndexes();
}
scope.importPrograms = (selectedPrograms) => {
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
selectedPrograms[i].commercials = []
}
scope.content = scope.content.concat(selectedPrograms);
refreshContentIndexes();
scope.showPlexLibrary = false;
}
scope.durationString = (duration) => {
var date = new Date(0);
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
return date.toISOString().substr(11, 8);
}
let interpolate = ( () => {
let h = 60*60*1000 / 6;
let ix = [0, 1*h, 2*h, 4*h, 8*h, 24*h];
let iy = [0, 1.0, 1.25, 1.5, 1.75, 2.0];
let n = ix.length;
return (x) => {
for (let i = 0; i < n-1; i++) {
if( (ix[i] <= x) && ( (x < ix[i+1]) || i==n-2 ) ) {
return iy[i] + (iy[i+1] - iy[i]) * ( (x - ix[i]) / (ix[i+1] - ix[i]) );
}
}
}
} )();
scope.programSquareStyle = (x) => {
return commonProgramTools.programSquareStyle(x);
}
}
};
}

View File

@@ -1,354 +0,0 @@
module.exports = function ($timeout, dizquetv, getShowData) {
const DAY = 24 * 60 * 60 * 1000;
const WEEK = 7 * DAY;
const WEEK_DAYS = [
'Thursday',
'Friday',
'Saturday',
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
];
return {
restrict: 'E',
templateUrl: 'templates/time-slots-schedule-editor.html',
replace: true,
scope: {
linker: '=linker',
onDone: '=onDone',
},
link: function (scope, element, attrs) {
scope.limit = 50000;
scope.visible = false;
scope.fake = { time: -1 };
scope.badTimes = false;
scope._editedTime = null;
let showsById;
let shows;
function reset() {
showsById = {};
shows = [];
scope.schedule = {
period: DAY,
lateness: 0,
maxDays: 365,
flexPreference: 'distribute',
slots: [],
pad: 1,
fake: { time: -1 },
};
}
reset();
function loadBackup(backup) {
scope.schedule = JSON.parse(JSON.stringify(backup));
if (typeof scope.schedule.pad == 'undefined') {
scope.schedule.pad = 1;
}
let slots = scope.schedule.slots;
for (let i = 0; i < slots.length; i++) {
let found = false;
for (let j = 0; j < scope.showOptions.length; j++) {
if (slots[i].showId == scope.showOptions[j].id) {
found = true;
}
}
if (!found) {
slots[i].showId = 'flex.';
slots[i].order = 'shuffle';
}
}
if (typeof scope.schedule.flexPreference === 'undefined') {
scope.schedule.flexPreference = 'distribute';
}
if (typeof scope.schedule.period === 'undefined') {
scope.schedule.period = DAY;
}
scope.schedule.fake = {
time: -1,
};
}
let getTitle = (index) => {
let showId = scope.schedule.slots[index].showId;
for (let i = 0; i < scope.showOptions.length; i++) {
if (scope.showOptions[i].id == showId) {
return scope.showOptions[i].description;
}
}
return 'Uknown';
};
scope.isWeekly = () => {
return scope.schedule.period === WEEK;
};
scope.periodChanged = () => {
if (scope.isWeekly()) {
//From daily to weekly
let l = scope.schedule.slots.length;
for (let i = 0; i < l; i++) {
let t = scope.schedule.slots[i].time;
scope.schedule.slots[i].time = t % DAY;
for (let j = 1; j < 7; j++) {
//clone the slot for every day of the week
let c = JSON.parse(angular.toJson(scope.schedule.slots[i]));
c.time += j * DAY;
scope.schedule.slots.push(c);
}
}
} else {
//From weekly to daily
let newSlots = [];
let seen = {};
for (let i = 0; i < scope.schedule.slots.length; i++) {
let slot = scope.schedule.slots[i];
let t = slot.time % DAY;
if (seen[t] !== true) {
seen[t] = true;
newSlots.push(slot);
}
}
scope.schedule.slots = newSlots;
}
scope.refreshSlots();
};
scope.editTime = (index) => {
let t = scope.schedule.slots[index].time;
scope._editedTime = {
time: t,
index: index,
isWeekly: scope.isWeekly(),
title: getTitle(index),
};
};
scope.finishedTimeEdit = (slot) => {
scope.schedule.slots[slot.index].time = slot.time;
scope.refreshSlots();
};
scope.addSlot = () => {
scope._addedTime = {
time: 0,
index: -1,
isWeekly: scope.isWeekly(),
title: 'New time slot',
};
};
scope.finishedAddingTime = (slot) => {
scope.schedule.slots.push({
time: slot.time,
showId: 'flex.',
order: 'next',
});
scope.refreshSlots();
};
scope.displayTime = (t) => {
if (scope.isWeekly()) {
let w = Math.floor(t / DAY);
let t2 = t % DAY;
return WEEK_DAYS[w].substring(0, 3) + ' ' + niceLookingTime(t2);
} else {
return niceLookingTime(t);
}
};
scope.timeColumnClass = () => {
let r = {};
if (scope.isWeekly()) {
r['col-md-3'] = true;
} else {
r['col-md-2'] = true;
}
return r;
};
scope.programColumnClass = () => {
let r = {};
if (scope.isWeekly()) {
r['col-md-6'] = true;
} else {
r['col-md-7'] = true;
}
return r;
};
scope.periodOptions = [
{ id: DAY, description: 'Daily' },
{ id: WEEK, description: 'Weekly' },
];
scope.latenessOptions = [
{ id: 0, description: 'Do not allow' },
{ id: 5 * 60 * 1000, description: '5 minutes' },
{ id: 10 * 60 * 1000, description: '10 minutes' },
{ id: 15 * 60 * 1000, description: '15 minutes' },
{ id: 1 * 60 * 60 * 1000, description: '1 hour' },
{ id: 2 * 60 * 60 * 1000, description: '2 hours' },
{ id: 3 * 60 * 60 * 1000, description: '3 hours' },
{ id: 4 * 60 * 60 * 1000, description: '4 hours' },
{ id: 8 * 60 * 60 * 1000, description: '8 hours' },
{ id: 24 * 60 * 60 * 1000, description: "I don't care about lateness" },
];
scope.flexOptions = [
{ id: 'distribute', description: 'Between videos' },
{ id: 'end', description: 'End of the slot' },
];
scope.padOptions = [
{ id: 1, description: 'Do not pad' },
{ id: 5 * 60 * 1000, description: '0:00, 0:05, 0:10, ..., 0:55' },
{ id: 10 * 60 * 1000, description: '0:00, 0:10, 0:20, ..., 0:50' },
{ id: 15 * 60 * 1000, description: '0:00, 0:15, 0:30, ..., 0:45' },
{ id: 30 * 60 * 1000, description: '0:00, 0:30' },
{ id: 1 * 60 * 60 * 1000, description: '0:00' },
];
scope.showOptions = [];
scope.orderOptions = [
{ id: 'next', description: 'Play Next' },
{ id: 'shuffle', description: 'Shuffle' },
];
let doIt = async () => {
scope.schedule.timeZoneOffset = new Date().getTimezoneOffset();
console.log(scope.programs, scope.schedule);
let res = await dizquetv.calculateTimeSlots(
scope.programs,
scope.schedule,
);
res.schedule = scope.schedule;
delete res.schedule.fake;
return res;
};
let startDialog = (programs, limit, backup) => {
scope.limit = limit;
scope.programs = programs;
reset();
programs.forEach((p) => {
let show = getShow(p);
if (show != null) {
if (typeof showsById[show.id] === 'undefined') {
showsById[show.id] = shows.length;
shows.push(show);
} else {
show = shows[showsById[show.id]];
}
}
});
scope.showOptions = shows.map((show) => {
return show;
});
scope.showOptions.push({
id: 'flex.',
description: 'Flex',
});
if (typeof backup !== 'undefined') {
loadBackup(backup);
}
scope.visible = true;
};
scope.linker({
startDialog: startDialog,
});
scope.finished = async (cancel) => {
scope.error = null;
if (!cancel) {
try {
scope.loading = true;
$timeout();
scope.onDone(await doIt());
scope.visible = false;
} catch (err) {
console.error('Unable to generate channel lineup', err);
scope.error = 'There was an error processing the schedule';
return;
} finally {
scope.loading = false;
$timeout();
}
} else {
scope.visible = false;
}
};
scope.deleteSlot = (index) => {
scope.schedule.slots.splice(index, 1);
};
scope.hasTimeError = (slot) => {
return typeof slot.timeError !== 'undefined';
};
scope.disableCreateLineup = () => {
if (scope.badTimes) {
return true;
}
if (typeof scope.schedule.maxDays === 'undefined') {
return true;
}
if (scope.schedule.slots.length == 0) {
return true;
}
return false;
};
scope.canShowSlot = (slot) => {
return slot.showId != 'flex.' && !slot.showId.startsWith('redirect.');
};
scope.refreshSlots = () => {
scope.badTimes = false;
//"Bubble sort ought to be enough for anybody"
for (let i = 0; i < scope.schedule.slots.length; i++) {
for (let j = i + 1; j < scope.schedule.slots.length; j++) {
if (scope.schedule.slots[j].time < scope.schedule.slots[i].time) {
let x = scope.schedule.slots[i];
scope.schedule.slots[i] = scope.schedule.slots[j];
scope.schedule.slots[j] = x;
}
}
if (scope.schedule.slots[i].showId == 'movie.') {
scope.schedule.slots[i].order = 'shuffle';
}
}
for (let i = 0; i < scope.schedule.slots.length; i++) {
if (
(i > 0 &&
scope.schedule.slots[i].time ==
scope.schedule.slots[i - 1].time) ||
(i + 1 < scope.schedule.slots.length &&
scope.schedule.slots[i].time == scope.schedule.slots[i + 1].time)
) {
scope.badTimes = true;
scope.schedule.slots[i].timeError = 'Please select a unique time.';
} else {
delete scope.schedule.slots[i].timeError;
}
}
$timeout();
};
},
};
function getShow(program) {
let d = getShowData(program);
if (!d.hasShow) {
return null;
} else {
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
};
function niceLookingTime(t) {
let d = new Date(t);
d.setMilliseconds(0);
return d.toLocaleTimeString([], { timeZone: 'UTC' });
}

View File

@@ -1,106 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/time-slots-time-editor.html',
replace: true,
scope: {
title: "@dialogTitle",
slot: "=slot",
visible: "=visible",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
let updateNext = true;
scope.w = 0;
scope.h = 0;
scope.m = 0;
scope.s = 0;
scope.weekDayOptions = [
{ id: 0, description : "Thursday" } ,
{ id: 1, description : "Friday" } ,
{ id: 2, description : "Saturday" } ,
{ id: 3, description : "Sunday" } ,
{ id: 4, description : "Monday" } ,
{ id: 5, description : "Tuesday" } ,
{ id: 6, description : "Wednesday" } ,
];
scope.hourOptions = [];
for (let i = 0; i < 24; i++) {
scope.hourOptions.push( {
id: i,
description: pad(i),
} );
}
scope.minuteOptions = [];
let mods = [ 15, 5, 1 ];
mods.forEach( x => {
for (let i = 0; i < 60; i+= x) {
scope.minuteOptions.push( {
id: i,
description: pad(i),
} );
}
} );
function pad(x) {
let s = "" + x;
if (s.length < 2) {
s = "0" + s;
}
return s;
}
scope.$watch('slot', () => {
try {
if ( (typeof(scope.slot) === 'undefined') || (scope.slot == null) ) {
updateNext = true;
return;
} else if (! updateNext) {
return;
}
updateNext = false;
scope.error = null;
t = Math.floor( scope.slot.time % (24 * 60 * 60 * 1000) / 1000 );
let s = t % 60;
let m = ( (t - s) / 60 ) % 60;
let h = (t - m*60 - s) / 3600;
let w = Math.floor( scope.slot.time / (24 * 60 * 60 * 1000) ) % 7;
scope.slot.h = h;
scope.slot.m = m;
scope.slot.s = s;
scope.slot.w = w;
} catch (err) {
console.error(err);
}
})
scope.finished = (slot) => {
scope.error = null;
if (isNaN(slot.h) || slot.h < 0 || slot.h > 23 ) {
scope.error = { t: 'Invalid hour of the day' }
}
if (isNaN(slot.m) || slot.m < 0 || slot.m > 59 ) {
scope.error = { t: 'Invalid minutes' }
}
if (isNaN(slot.s) || slot.s < 0 || slot.s > 59 ) {
scope.error = { t: 'Invalid seconds' }
}
if (isNaN(slot.w) || slot.w < 0 || slot.w > 6 ) {
scope.error = { t: 'Invalid day' }
}
if (scope.error != null) {
$timeout(() => {
scope.error = null
}, 30000)
return
}
slot.time = slot.w*24*60*60*1000 + slot.h*60*60*1000 + slot.m*60*1000+ slot.s*1000;
scope.onDone(JSON.parse(angular.toJson(slot)))
scope.slot = null
}
}
};
}

View File

@@ -1,121 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/toast-notifications.html',
replace: true,
scope: {
},
link: function (scope, element, attrs) {
const FADE_IN_START = 100;
const FADE_IN_END = 1000;
const FADE_OUT_START = 10000;
const TOTAL_DURATION = 11000;
scope.toasts = [];
let eventSource = null;
let timerHandle = null;
let refreshHandle = null;
let setResetTimer = () => {
if (timerHandle != null) {
clearTimeout( timerHandle );
}
timerHandle = setTimeout( () => {
scope.setup();
} , 10000);
};
let updateAfter = (wait) => {
if (refreshHandle != null) {
$timeout.cancel( refreshHandle );
}
refreshHandle = $timeout( ()=> updater(), wait );
};
let updater = () => {
let wait = 10000;
let updatedToasts = [];
try {
let t = (new Date()).getTime();
for (let i = 0; i < scope.toasts.length; i++) {
let toast = scope.toasts[i];
let diff = t - toast.time;
if (diff < TOTAL_DURATION) {
if (diff < FADE_IN_START) {
toast.clazz = { "about-to-fade-in" : true }
wait = Math.min( wait, FADE_IN_START - diff );
} else if (diff < FADE_IN_END) {
toast.clazz = { "fade-in" : true }
wait = Math.min( wait, FADE_IN_END - diff );
} else if (diff < FADE_OUT_START) {
toast.clazz = {}
wait = Math.min( wait, FADE_OUT_START - diff );
} else {
toast.clazz = { "fade-out" : true }
wait = Math.min( wait, TOTAL_DURATION - diff );
}
toast.clazz[toast.deco] = true;
updatedToasts.push(toast);
}
}
} catch (err) {
console.error("error", err);
}
scope.toasts = updatedToasts;
updateAfter(wait);
};
let addToast = (toast) => {
toast.time = (new Date()).getTime();
toast.clazz= { "about-to-fade-in": true };
toast.clazz[toast.deco] = true;
scope.toasts.push(toast);
$timeout( () => updateAfter(0) );
};
let getDeco = (data) => {
return "bg-" + data.level;
}
scope.setup = () => {
if (eventSource != null) {
eventSource.close();
eventSource = null;
}
setResetTimer();
eventSource = new EventSource("api/events");
eventSource.addEventListener("heartbeat", () => {
setResetTimer();
} );
let normalEvent = (title) => {
return (event) => {
let data = JSON.parse(event.data);
addToast ( {
title : title,
text : data.message,
deco: getDeco(data)
} )
};
};
eventSource.addEventListener('settings-update', normalEvent("Settings Update") );
eventSource.addEventListener('xmltv', normalEvent("TV Guide") );
eventSource.addEventListener('lifecycle', normalEvent("Server") );
};
scope.destroy = (index) => {
scope.toasts.splice(index,1);
}
scope.setup();
}
};
}

View File

@@ -1,25 +0,0 @@
module.exports = function (dizquetv) {
return {
restrict: 'E',
templateUrl: 'templates/xmltv-settings.html',
replace: true,
scope: {},
link: function (scope, element, attrs) {
dizquetv.getXmltvSettings().then((settings) => {
console.log(settings);
scope.settings = settings;
});
scope.updateSettings = (settings) => {
console.log(settings);
dizquetv.updateXmltvSettings(settings).then((_settings) => {
scope.settings = _settings;
});
};
scope.resetSettings = (settings) => {
dizquetv.resetXmltvSettings(settings).then((_settings) => {
scope.settings = _settings;
});
};
},
};
};

View File

@@ -1,164 +0,0 @@
/* angularjs Scroll Glue
* version 2.1.0
* https://github.com/Luegg/angularjs-scroll-glue
* An AngularJs directive that automatically scrolls to the bottom of an element on changes in it's scope.
*/
// Allow module to be loaded via require when using common js. e.g. npm
if(typeof module === "object" && module.exports){
module.exports = 'luegg.directives';
}
(function(angular, undefined){
'use strict';
function createActivationState($parse, attr, scope){
function unboundState(initValue){
var activated = initValue;
return {
getValue: function(){
return activated;
},
setValue: function(value){
activated = value;
}
};
}
function oneWayBindingState(getter, scope){
return {
getValue: function(){
return getter(scope);
},
setValue: function(){}
};
}
function twoWayBindingState(getter, setter, scope){
return {
getValue: function(){
return getter(scope);
},
setValue: function(value){
if(value !== getter(scope)){
scope.$apply(function(){
setter(scope, value);
});
}
}
};
}
if(attr !== ""){
var getter = $parse(attr);
if(getter.assign !== undefined){
return twoWayBindingState(getter, getter.assign, scope);
} else {
return oneWayBindingState(getter, scope);
}
} else {
return unboundState(true);
}
}
function createDirective(module, attrName, direction){
module.directive(attrName, ['$parse', '$window', '$timeout', function($parse, $window, $timeout){
return {
priority: 1,
restrict: 'A',
link: function(scope, $el, attrs){
var el = $el[0],
activationState = createActivationState($parse, attrs[attrName], scope);
function scrollIfGlued() {
if(activationState.getValue() && !direction.isAttached(el)){
// Ensures scroll after angular template digest
$timeout(function() {
direction.scroll(el);
});
}
}
function onScroll() {
activationState.setValue(direction.isAttached(el));
}
$timeout(scrollIfGlued, 0, false);
if (!$el[0].hasAttribute('force-glue')) {
$el.on('scroll', onScroll);
}
var hasAnchor = false;
angular.forEach($el.children(), function(child) {
if (child.hasAttribute('scroll-glue-anchor')) {
hasAnchor = true;
scope.$watch(function() { return child.offsetHeight }, function() {
scrollIfGlued();
});
}
});
if (!hasAnchor) {
scope.$watch(scrollIfGlued);
$window.addEventListener('resize', scrollIfGlued, false);
}
// Remove listeners on directive destroy
$el.on('$destroy', function() {
$el.unbind('scroll', onScroll);
});
scope.$on('$destroy', function() {
$window.removeEventListener('resize', scrollIfGlued, false);
});
}
};
}]);
}
var bottom = {
isAttached: function(el){
// + 1 catches off by one errors in chrome
return el.scrollTop + el.clientHeight + 1 >= el.scrollHeight;
},
scroll: function(el){
el.scrollTop = el.scrollHeight;
}
};
var top = {
isAttached: function(el){
return el.scrollTop <= 1;
},
scroll: function(el){
el.scrollTop = 0;
}
};
var right = {
isAttached: function(el){
return el.scrollLeft + el.clientWidth + 1 >= el.scrollWidth;
},
scroll: function(el){
el.scrollLeft = el.scrollWidth;
}
};
var left = {
isAttached: function(el){
return el.scrollLeft <= 1;
},
scroll: function(el){
el.scrollLeft = 0;
}
};
var module = angular.module('luegg.directives', []);
createDirective(module, 'scrollGlue', bottom);
createDirective(module, 'scrollGlueTop', top);
createDirective(module, 'scrollGlueBottom', bottom);
createDirective(module, 'scrollGlueLeft', left);
createDirective(module, 'scrollGlueRight', right);
}(angular));

View File

@@ -1,650 +0,0 @@
/**
* angular-drag-and-drop-lists v2.1.0
*
* Copyright (c) 2014 Marcel Juenemann marcel@juenemann.cc
* Copyright (c) 2014-2017 Google Inc.
* https://github.com/marceljuenemann/angular-drag-and-drop-lists
*
* License: MIT
*/
(function(dndLists) {
// In standard-compliant browsers we use a custom mime type and also encode the dnd-type in it.
// However, IE and Edge only support a limited number of mime types. The workarounds are described
// in https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design
var MIME_TYPE = 'application/x-dnd';
var EDGE_MIME_TYPE = 'application/json';
var MSIE_MIME_TYPE = 'Text';
// All valid HTML5 drop effects, in the order in which we prefer to use them.
var ALL_EFFECTS = ['move', 'copy', 'link'];
/**
* Use the dnd-draggable attribute to make your element draggable
*
* Attributes:
* - dnd-draggable Required attribute. The value has to be an object that represents the data
* of the element. In case of a drag and drop operation the object will be
* serialized and unserialized on the receiving end.
* - dnd-effect-allowed Use this attribute to limit the operations that can be performed. Valid
* options are "move", "copy" and "link", as well as "all", "copyMove",
* "copyLink" and "linkMove". The semantics of these operations are up to you
* and have to be implemented using the callbacks described below. If you
* allow multiple options, the user can choose between them by using the
* modifier keys (OS specific). The cursor will be changed accordingly,
* expect for IE and Edge, where this is not supported.
* - dnd-type Use this attribute if you have different kinds of items in your
* application and you want to limit which items can be dropped into which
* lists. Combine with dnd-allowed-types on the dnd-list(s). This attribute
* must be a lower case string. Upper case characters can be used, but will
* be converted to lower case automatically.
* - dnd-disable-if You can use this attribute to dynamically disable the draggability of the
* element. This is useful if you have certain list items that you don't want
* to be draggable, or if you want to disable drag & drop completely without
* having two different code branches (e.g. only allow for admins).
*
* Callbacks:
* - dnd-dragstart Callback that is invoked when the element was dragged. The original
* dragstart event will be provided in the local event variable.
* - dnd-moved Callback that is invoked when the element was moved. Usually you will
* remove your element from the original list in this callback, since the
* directive is not doing that for you automatically. The original dragend
* event will be provided in the local event variable.
* - dnd-copied Same as dnd-moved, just that it is called when the element was copied
* instead of moved, so you probably want to implement a different logic.
* - dnd-linked Same as dnd-moved, just that it is called when the element was linked
* instead of moved, so you probably want to implement a different logic.
* - dnd-canceled Callback that is invoked if the element was dragged, but the operation was
* canceled and the element was not dropped. The original dragend event will
* be provided in the local event variable.
* - dnd-dragend Callback that is invoked when the drag operation ended. Available local
* variables are event and dropEffect.
* - dnd-selected Callback that is invoked when the element was clicked but not dragged.
* The original click event will be provided in the local event variable.
* - dnd-callback Custom callback that is passed to dropzone callbacks and can be used to
* communicate between source and target scopes. The dropzone can pass user
* defined variables to this callback.
*
* CSS classes:
* - dndDragging This class will be added to the element while the element is being
* dragged. It will affect both the element you see while dragging and the
* source element that stays at it's position. Do not try to hide the source
* element with this class, because that will abort the drag operation.
* - dndDraggingSource This class will be added to the element after the drag operation was
* started, meaning it only affects the original element that is still at
* it's source position, and not the "element" that the user is dragging with
* his mouse pointer.
*/
dndLists.directive('dndDraggable', ['$parse', '$timeout', function($parse, $timeout) {
return function(scope, element, attr) {
// Set the HTML5 draggable attribute on the element.
element.attr("draggable", "true");
// If the dnd-disable-if attribute is set, we have to watch that.
if (attr.dndDisableIf) {
scope.$watch(attr.dndDisableIf, function(disabled) {
element.attr("draggable", !disabled);
});
}
/**
* When the drag operation is started we have to prepare the dataTransfer object,
* which is the primary way we communicate with the target element
*/
element.on('dragstart', function(event) {
event = event.originalEvent || event;
// Check whether the element is draggable, since dragstart might be triggered on a child.
if (element.attr('draggable') == 'false') return true;
// Initialize global state.
dndState.isDragging = true;
dndState.itemType = attr.dndType && scope.$eval(attr.dndType).toLowerCase();
// Set the allowed drop effects. See below for special IE handling.
dndState.dropEffect = "none";
dndState.effectAllowed = attr.dndEffectAllowed || ALL_EFFECTS[0];
event.dataTransfer.effectAllowed = dndState.effectAllowed;
// Internet Explorer and Microsoft Edge don't support custom mime types, see design doc:
// https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design
var item = scope.$eval(attr.dndDraggable);
var mimeType = MIME_TYPE + (dndState.itemType ? ('-' + dndState.itemType) : '');
try {
event.dataTransfer.setData(mimeType, angular.toJson(item));
} catch (e) {
// Setting a custom MIME type did not work, we are probably in IE or Edge.
var data = angular.toJson({item: item, type: dndState.itemType});
try {
event.dataTransfer.setData(EDGE_MIME_TYPE, data);
} catch (e) {
// We are in Internet Explorer and can only use the Text MIME type. Also note that IE
// does not allow changing the cursor in the dragover event, therefore we have to choose
// the one we want to display now by setting effectAllowed.
var effectsAllowed = filterEffects(ALL_EFFECTS, dndState.effectAllowed);
event.dataTransfer.effectAllowed = effectsAllowed[0];
event.dataTransfer.setData(MSIE_MIME_TYPE, data);
}
}
// Add CSS classes. See documentation above.
element.addClass("dndDragging");
$timeout(function() { element.addClass("dndDraggingSource"); }, 0);
// Try setting a proper drag image if triggered on a dnd-handle (won't work in IE).
if (event._dndHandle && event.dataTransfer.setDragImage) {
event.dataTransfer.setDragImage(element[0], 0, 0);
}
// Invoke dragstart callback and prepare extra callback for dropzone.
$parse(attr.dndDragstart)(scope, {event: event});
if (attr.dndCallback) {
var callback = $parse(attr.dndCallback);
dndState.callback = function(params) { return callback(scope, params || {}); };
}
event.stopPropagation();
});
/**
* The dragend event is triggered when the element was dropped or when the drag
* operation was aborted (e.g. hit escape button). Depending on the executed action
* we will invoke the callbacks specified with the dnd-moved or dnd-copied attribute.
*/
element.on('dragend', function(event) {
event = event.originalEvent || event;
// Invoke callbacks. Usually we would use event.dataTransfer.dropEffect to determine
// the used effect, but Chrome has not implemented that field correctly. On Windows
// it always sets it to 'none', while Chrome on Linux sometimes sets it to something
// else when it's supposed to send 'none' (drag operation aborted).
scope.$apply(function() {
var dropEffect = dndState.dropEffect;
var cb = {copy: 'dndCopied', link: 'dndLinked', move: 'dndMoved', none: 'dndCanceled'};
$parse(attr[cb[dropEffect]])(scope, {event: event});
$parse(attr.dndDragend)(scope, {event: event, dropEffect: dropEffect});
});
// Clean up
dndState.isDragging = false;
dndState.callback = undefined;
element.removeClass("dndDragging");
element.removeClass("dndDraggingSource");
event.stopPropagation();
// In IE9 it is possible that the timeout from dragstart triggers after the dragend handler.
$timeout(function() { element.removeClass("dndDraggingSource"); }, 0);
});
/**
* When the element is clicked we invoke the callback function
* specified with the dnd-selected attribute.
*/
element.on('click', function(event) {
if (!attr.dndSelected) return;
event = event.originalEvent || event;
scope.$apply(function() {
$parse(attr.dndSelected)(scope, {event: event});
});
// Prevent triggering dndSelected in parent elements.
event.stopPropagation();
});
/**
* Workaround to make element draggable in IE9
*/
element.on('selectstart', function() {
if (this.dragDrop) this.dragDrop();
});
};
}]);
/**
* Use the dnd-list attribute to make your list element a dropzone. Usually you will add a single
* li element as child with the ng-repeat directive. If you don't do that, we will not be able to
* position the dropped element correctly. If you want your list to be sortable, also add the
* dnd-draggable directive to your li element(s).
*
* Attributes:
* - dnd-list Required attribute. The value has to be the array in which the data of
* the dropped element should be inserted. The value can be blank if used
* with a custom dnd-drop handler that always returns true.
* - dnd-allowed-types Optional array of allowed item types. When used, only items that had a
* matching dnd-type attribute will be dropable. Upper case characters will
* automatically be converted to lower case.
* - dnd-effect-allowed Optional string expression that limits the drop effects that can be
* performed in the list. See dnd-effect-allowed on dnd-draggable for more
* details on allowed options. The default value is all.
* - dnd-disable-if Optional boolean expresssion. When it evaluates to true, no dropping
* into the list is possible. Note that this also disables rearranging
* items inside the list.
* - dnd-horizontal-list Optional boolean expresssion. When it evaluates to true, the positioning
* algorithm will use the left and right halfs of the list items instead of
* the upper and lower halfs.
* - dnd-external-sources Optional boolean expression. When it evaluates to true, the list accepts
* drops from sources outside of the current browser tab. This allows to
* drag and drop accross different browser tabs. The only major browser
* that does not support this is currently Microsoft Edge.
*
* Callbacks:
* - dnd-dragover Optional expression that is invoked when an element is dragged over the
* list. If the expression is set, but does not return true, the element is
* not allowed to be dropped. The following variables will be available:
* - event: The original dragover event sent by the browser.
* - index: The position in the list at which the element would be dropped.
* - type: The dnd-type set on the dnd-draggable, or undefined if non was
* set. Will be null for drops from external sources in IE and Edge,
* since we don't know the type in those cases.
* - dropEffect: One of move, copy or link, see dnd-effect-allowed.
* - external: Whether the element was dragged from an external source.
* - callback: If dnd-callback was set on the source element, this is a
* function reference to the callback. The callback can be invoked with
* custom variables like this: callback({var1: value1, var2: value2}).
* The callback will be executed on the scope of the source element. If
* dnd-external-sources was set and external is true, this callback will
* not be available.
* - dnd-drop Optional expression that is invoked when an element is dropped on the
* list. The same variables as for dnd-dragover will be available, with the
* exception that type is always known and therefore never null. There
* will also be an item variable, which is the transferred object. The
* return value determines the further handling of the drop:
* - falsy: The drop will be canceled and the element won't be inserted.
* - true: Signalises that the drop is allowed, but the dnd-drop
* callback already took care of inserting the element.
* - otherwise: All other return values will be treated as the object to
* insert into the array. In most cases you want to simply return the
* item parameter, but there are no restrictions on what you can return.
* - dnd-inserted Optional expression that is invoked after a drop if the element was
* actually inserted into the list. The same local variables as for
* dnd-drop will be available. Note that for reorderings inside the same
* list the old element will still be in the list due to the fact that
* dnd-moved was not called yet.
*
* CSS classes:
* - dndPlaceholder When an element is dragged over the list, a new placeholder child
* element will be added. This element is of type li and has the class
* dndPlaceholder set. Alternatively, you can define your own placeholder
* by creating a child element with dndPlaceholder class.
* - dndDragover Will be added to the list while an element is dragged over the list.
*/
dndLists.directive('dndList', ['$parse', function($parse) {
return function(scope, element, attr) {
// While an element is dragged over the list, this placeholder element is inserted
// at the location where the element would be inserted after dropping.
var placeholder = getPlaceholderElement();
placeholder.remove();
var placeholderNode = placeholder[0];
var listNode = element[0];
var listSettings = {};
/**
* The dragenter event is fired when a dragged element or text selection enters a valid drop
* target. According to the spec, we either need to have a dropzone attribute or listen on
* dragenter events and call preventDefault(). It should be noted though that no browser seems
* to enforce this behaviour.
*/
element.on('dragenter', function (event) {
event = event.originalEvent || event;
// Calculate list properties, so that we don't have to repeat this on every dragover event.
var types = attr.dndAllowedTypes && scope.$eval(attr.dndAllowedTypes);
listSettings = {
allowedTypes: angular.isArray(types) && types.join('|').toLowerCase().split('|'),
disabled: attr.dndDisableIf && scope.$eval(attr.dndDisableIf),
externalSources: attr.dndExternalSources && scope.$eval(attr.dndExternalSources),
horizontal: attr.dndHorizontalList && scope.$eval(attr.dndHorizontalList)
};
var mimeType = getMimeType(event.dataTransfer.types);
if (!mimeType || !isDropAllowed(getItemType(mimeType))) return true;
event.preventDefault();
});
/**
* The dragover event is triggered "every few hundred milliseconds" while an element
* is being dragged over our list, or over an child element.
*/
element.on('dragover', function(event) {
event = event.originalEvent || event;
// Check whether the drop is allowed and determine mime type.
var mimeType = getMimeType(event.dataTransfer.types);
var itemType = getItemType(mimeType);
if (!mimeType || !isDropAllowed(itemType)) return true;
// Make sure the placeholder is shown, which is especially important if the list is empty.
if (placeholderNode.parentNode != listNode) {
element.append(placeholder);
}
if (event.target != listNode) {
// Try to find the node direct directly below the list node.
var listItemNode = event.target;
while (listItemNode.parentNode != listNode && listItemNode.parentNode) {
listItemNode = listItemNode.parentNode;
}
if (listItemNode.parentNode == listNode && listItemNode != placeholderNode) {
// If the mouse pointer is in the upper half of the list item element,
// we position the placeholder before the list item, otherwise after it.
var rect = listItemNode.getBoundingClientRect();
if (listSettings.horizontal) {
var isFirstHalf = event.clientX < rect.left + rect.width / 2;
} else {
var isFirstHalf = event.clientY < rect.top + rect.height / 2;
}
listNode.insertBefore(placeholderNode,
isFirstHalf ? listItemNode : listItemNode.nextSibling);
}
}
// In IE we set a fake effectAllowed in dragstart to get the correct cursor, we therefore
// ignore the effectAllowed passed in dataTransfer. We must also not access dataTransfer for
// drops from external sources, as that throws an exception.
var ignoreDataTransfer = mimeType == MSIE_MIME_TYPE;
var dropEffect = getDropEffect(event, ignoreDataTransfer);
if (dropEffect == 'none') return stopDragover();
// At this point we invoke the callback, which still can disallow the drop.
// We can't do this earlier because we want to pass the index of the placeholder.
if (attr.dndDragover && !invokeCallback(attr.dndDragover, event, dropEffect, itemType)) {
return stopDragover();
}
// Set dropEffect to modify the cursor shown by the browser, unless we're in IE, where this
// is not supported. This must be done after preventDefault in Firefox.
event.preventDefault();
if (!ignoreDataTransfer) {
event.dataTransfer.dropEffect = dropEffect;
}
element.addClass("dndDragover");
event.stopPropagation();
return false;
});
/**
* When the element is dropped, we use the position of the placeholder element as the
* position where we insert the transferred data. This assumes that the list has exactly
* one child element per array element.
*/
element.on('drop', function(event) {
event = event.originalEvent || event;
// Check whether the drop is allowed and determine mime type.
var mimeType = getMimeType(event.dataTransfer.types);
var itemType = getItemType(mimeType);
if (!mimeType || !isDropAllowed(itemType)) return true;
// The default behavior in Firefox is to interpret the dropped element as URL and
// forward to it. We want to prevent that even if our drop is aborted.
event.preventDefault();
// Unserialize the data that was serialized in dragstart.
try {
var data = JSON.parse(event.dataTransfer.getData(mimeType));
} catch(e) {
return stopDragover();
}
// Drops with invalid types from external sources might not have been filtered out yet.
if (mimeType == MSIE_MIME_TYPE || mimeType == EDGE_MIME_TYPE) {
itemType = data.type || undefined;
data = data.item;
if (!isDropAllowed(itemType)) return stopDragover();
}
// Special handling for internal IE drops, see dragover handler.
var ignoreDataTransfer = mimeType == MSIE_MIME_TYPE;
var dropEffect = getDropEffect(event, ignoreDataTransfer);
if (dropEffect == 'none') return stopDragover();
// Invoke the callback, which can transform the transferredObject and even abort the drop.
var index = getPlaceholderIndex();
if (attr.dndDrop) {
data = invokeCallback(attr.dndDrop, event, dropEffect, itemType, index, data);
if (!data) return stopDragover();
}
// The drop is definitely going to happen now, store the dropEffect.
dndState.dropEffect = dropEffect;
if (!ignoreDataTransfer) {
event.dataTransfer.dropEffect = dropEffect;
}
// Insert the object into the array, unless dnd-drop took care of that (returned true).
if (data !== true) {
scope.$apply(function() {
scope.$eval(attr.dndList).splice(index, 0, data);
});
}
invokeCallback(attr.dndInserted, event, dropEffect, itemType, index, data);
// Clean up
stopDragover();
event.stopPropagation();
return false;
});
/**
* We have to remove the placeholder when the element is no longer dragged over our list. The
* problem is that the dragleave event is not only fired when the element leaves our list,
* but also when it leaves a child element. Therefore, we determine whether the mouse cursor
* is still pointing to an element inside the list or not.
*/
element.on('dragleave', function(event) {
event = event.originalEvent || event;
var newTarget = document.elementFromPoint(event.clientX, event.clientY);
if (listNode.contains(newTarget) && !event._dndPhShown) {
// Signalize to potential parent lists that a placeholder is already shown.
event._dndPhShown = true;
} else {
stopDragover();
}
});
/**
* Given the types array from the DataTransfer object, returns the first valid mime type.
* A type is valid if it starts with MIME_TYPE, or it equals MSIE_MIME_TYPE or EDGE_MIME_TYPE.
*/
function getMimeType(types) {
if (!types) return MSIE_MIME_TYPE; // IE 9 workaround.
for (var i = 0; i < types.length; i++) {
if (types[i] == MSIE_MIME_TYPE || types[i] == EDGE_MIME_TYPE ||
types[i].substr(0, MIME_TYPE.length) == MIME_TYPE) {
return types[i];
}
}
return null;
}
/**
* Determines the type of the item from the dndState, or from the mime type for items from
* external sources. Returns undefined if no item type was set and null if the item type could
* not be determined.
*/
function getItemType(mimeType) {
if (dndState.isDragging) return dndState.itemType || undefined;
if (mimeType == MSIE_MIME_TYPE || mimeType == EDGE_MIME_TYPE) return null;
return (mimeType && mimeType.substr(MIME_TYPE.length + 1)) || undefined;
}
/**
* Checks various conditions that must be fulfilled for a drop to be allowed, including the
* dnd-allowed-types attribute. If the item Type is unknown (null), the drop will be allowed.
*/
function isDropAllowed(itemType) {
if (listSettings.disabled) return false;
if (!listSettings.externalSources && !dndState.isDragging) return false;
if (!listSettings.allowedTypes || itemType === null) return true;
return itemType && listSettings.allowedTypes.indexOf(itemType) != -1;
}
/**
* Determines which drop effect to use for the given event. In Internet Explorer we have to
* ignore the effectAllowed field on dataTransfer, since we set a fake value in dragstart.
* In those cases we rely on dndState to filter effects. Read the design doc for more details:
* https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design
*/
function getDropEffect(event, ignoreDataTransfer) {
var effects = ALL_EFFECTS;
if (!ignoreDataTransfer) {
effects = filterEffects(effects, event.dataTransfer.effectAllowed);
}
if (dndState.isDragging) {
effects = filterEffects(effects, dndState.effectAllowed);
}
if (attr.dndEffectAllowed) {
effects = filterEffects(effects, attr.dndEffectAllowed);
}
// MacOS automatically filters dataTransfer.effectAllowed depending on the modifier keys,
// therefore the following modifier keys will only affect other operating systems.
if (!effects.length) {
return 'none';
} else if (event.ctrlKey && effects.indexOf('copy') != -1) {
return 'copy';
} else if (event.altKey && effects.indexOf('link') != -1) {
return 'link';
} else {
return effects[0];
}
}
/**
* Small helper function that cleans up if we aborted a drop.
*/
function stopDragover() {
placeholder.remove();
element.removeClass("dndDragover");
return true;
}
/**
* Invokes a callback with some interesting parameters and returns the callbacks return value.
*/
function invokeCallback(expression, event, dropEffect, itemType, index, item) {
return $parse(expression)(scope, {
callback: dndState.callback,
dropEffect: dropEffect,
event: event,
external: !dndState.isDragging,
index: index !== undefined ? index : getPlaceholderIndex(),
item: item || undefined,
type: itemType
});
}
/**
* We use the position of the placeholder node to determine at which position of the array the
* object needs to be inserted
*/
function getPlaceholderIndex() {
return Array.prototype.indexOf.call(listNode.children, placeholderNode);
}
/**
* Tries to find a child element that has the dndPlaceholder class set. If none was found, a
* new li element is created.
*/
function getPlaceholderElement() {
var placeholder;
angular.forEach(element.children(), function(childNode) {
var child = angular.element(childNode);
if (child.hasClass('dndPlaceholder')) {
placeholder = child;
}
});
return placeholder || angular.element("<li class='dndPlaceholder'></li>");
}
};
}]);
/**
* Use the dnd-nodrag attribute inside of dnd-draggable elements to prevent them from starting
* drag operations. This is especially useful if you want to use input elements inside of
* dnd-draggable elements or create specific handle elements. Note: This directive does not work
* in Internet Explorer 9.
*/
dndLists.directive('dndNodrag', function() {
return function(scope, element, attr) {
// Set as draggable so that we can cancel the events explicitly
element.attr("draggable", "true");
/**
* Since the element is draggable, the browser's default operation is to drag it on dragstart.
* We will prevent that and also stop the event from bubbling up.
*/
element.on('dragstart', function(event) {
event = event.originalEvent || event;
if (!event._dndHandle) {
// If a child element already reacted to dragstart and set a dataTransfer object, we will
// allow that. For example, this is the case for user selections inside of input elements.
if (!(event.dataTransfer.types && event.dataTransfer.types.length)) {
event.preventDefault();
}
event.stopPropagation();
}
});
/**
* Stop propagation of dragend events, otherwise dnd-moved might be triggered and the element
* would be removed.
*/
element.on('dragend', function(event) {
event = event.originalEvent || event;
if (!event._dndHandle) {
event.stopPropagation();
}
});
};
});
/**
* Use the dnd-handle directive within a dnd-nodrag element in order to allow dragging with that
* element after all. Therefore, by combining dnd-nodrag and dnd-handle you can allow
* dnd-draggable elements to only be dragged via specific "handle" elements. Note that Internet
* Explorer will show the handle element as drag image instead of the dnd-draggable element. You
* can work around this by styling the handle element differently when it is being dragged. Use
* the CSS selector .dndDragging:not(.dndDraggingSource) [dnd-handle] for that.
*/
dndLists.directive('dndHandle', function() {
return function(scope, element, attr) {
element.attr("draggable", "true");
element.on('dragstart dragend', function(event) {
event = event.originalEvent || event;
event._dndHandle = true;
});
};
});
/**
* Filters an array of drop effects using a HTML5 effectAllowed string.
*/
function filterEffects(effects, effectAllowed) {
if (effectAllowed == 'all') return effects;
return effects.filter(function(effect) {
return effectAllowed.toLowerCase().indexOf(effect) != -1;
});
}
/**
* For some features we need to maintain global state. This is done here, with these fields:
* - callback: A callback function set at dragstart that is passed to internal dropzone handlers.
* - dropEffect: Set in dragstart to "none" and to the actual value in the drop handler. We don't
* rely on the dropEffect passed by the browser, since there are various bugs in Chrome and
* Safari, and Internet Explorer defaults to copy if effectAllowed is copyMove.
* - effectAllowed: Set in dragstart based on dnd-effect-allowed. This is needed for IE because
* setting effectAllowed on dataTransfer might result in an undesired cursor.
* - isDragging: True between dragstart and dragend. Falsy for drops from external sources.
* - itemType: The item type of the dragged element set via dnd-type. This is needed because IE
* and Edge don't support custom mime types that we can use to transfer this information.
*/
var dndState = {};
})(angular.module('dndLists', []));

View File

@@ -1,274 +0,0 @@
module.exports = function (angular) {
/*
* angular-lazy-load
*
* Copyright(c) 2014 Paweł Wszoła <wszola.p@gmail.com>
* MIT Licensed
*
*/
/**
* @author Paweł Wszoła (wszola.p@gmail.com)
*
*/
angular.module('angularLazyImg', []);
angular.module('angularLazyImg').factory('LazyImgMagic', [
'$window', '$rootScope', 'lazyImgConfig', 'lazyImgHelpers',
function($window, $rootScope, lazyImgConfig, lazyImgHelpers){
'use strict';
var winDimensions, $win, images, isListening, options;
var checkImagesT, saveWinOffsetT, containers;
images = [];
isListening = false;
options = lazyImgConfig.getOptions();
$win = angular.element($window);
winDimensions = lazyImgHelpers.getWinDimensions();
saveWinOffsetT = lazyImgHelpers.throttle(function(){
winDimensions = lazyImgHelpers.getWinDimensions();
}, 60);
options.container = options.containers || options.container;
containers = options.container ? [].concat(options.container) : [$win];
function checkImages(){
for(var i = images.length - 1; i >= 0; i--){
var image = images[i];
if(image && lazyImgHelpers.isElementInView(image.$elem[0], options.offset, winDimensions)){
loadImage(image);
images.splice(i, 1);
}
}
if(!images.length){ stopListening(); }
}
checkImagesT = lazyImgHelpers.throttle(checkImages, 30);
function listen(param){
containers.forEach(function (container) {
container[param]('scroll', checkImagesT);
container[param]('touchmove', checkImagesT);
});
$win[param]('resize', checkImagesT);
$win[param]('resize', saveWinOffsetT);
}
function startListening(){
isListening = true;
setTimeout(function(){
checkImages();
listen('on');
}, 1);
}
function stopListening(){
isListening = false;
listen('off');
}
function removeImage(image){
var index = images.indexOf(image);
if(index !== -1) {
images.splice(index, 1);
}
}
function loadImage(photo){
var img = new Image();
img.onerror = function(){
if(options.errorClass){
photo.$elem.addClass(options.errorClass);
}
if(photo.errorSrc){
setPhotoSrc(photo.$elem, photo.errorSrc);
}
$rootScope.$apply(function () {
$rootScope.$emit('lazyImg:error', photo);
options.onError(photo);
});
};
img.onload = function(){
setPhotoSrc(photo.$elem, photo.src);
if(options.successClass){
photo.$elem.addClass(options.successClass);
}
$rootScope.$apply(function () {
$rootScope.$emit('lazyImg:success', photo);
options.onSuccess(photo);
});
};
img.src = photo.src;
}
function setPhotoSrc($elem, src){
if ($elem[0].nodeName.toLowerCase() === 'img') {
$elem[0].src = src;
} else {
$elem.css('background-image', 'url("' + src + '")');
}
}
// PHOTO
function Photo($elem){
this.$elem = $elem;
}
Photo.prototype.setSource = function(source){
this.src = source;
images.unshift(this);
startListening();
};
Photo.prototype.setErrorSource = function(errorSource){
this.errorSrc = errorSource;
};
Photo.prototype.removeImage = function(){
removeImage(this);
if(!images.length){ stopListening(); }
};
Photo.prototype.checkImages = checkImages;
Photo.addContainer = function (container) {
stopListening();
containers.push(container);
startListening();
};
Photo.removeContainer = function (container) {
stopListening();
containers.splice(containers.indexOf(container), 1);
startListening();
};
return Photo;
}
]);
angular.module('angularLazyImg').provider('lazyImgConfig', function() {
'use strict';
this.options = {
offset : 100,
errorClass : null,
successClass : null,
onError : function(){},
onSuccess : function(){}
};
this.$get = function() {
var options = this.options;
return {
getOptions: function() {
return options;
}
};
};
this.setOptions = function(options) {
angular.extend(this.options, options);
};
});
angular.module('angularLazyImg').factory('lazyImgHelpers', [
'$window', function($window){
'use strict';
function getWinDimensions(){
return {
height: $window.innerHeight,
width: $window.innerWidth
};
}
function isElementInView(elem, offset, winDimensions) {
var rect = elem.getBoundingClientRect();
return (
// check if any part of element is in view extented by an offset
(rect.left <= winDimensions.width + offset) &&
(rect.right >= 0 - offset) &&
(rect.top <= winDimensions.height + offset) &&
(rect.bottom >= 0 - offset)
);
}
// http://remysharp.com/2010/07/21/throttling-function-calls/
function throttle(fn, threshhold, scope) {
var last, deferTimer;
return function () {
var context = scope || this;
var now = +new Date(),
args = arguments;
if (last && now < last + threshhold) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
};
}
return {
isElementInView: isElementInView,
getWinDimensions: getWinDimensions,
throttle: throttle
};
}
]);
angular.module('angularLazyImg')
.directive('lazyImg', [
'$rootScope', '$log', 'LazyImgMagic', function ($rootScope, $log, LazyImgMagic) {
'use strict';
function link(scope, element, attributes) {
scope.lazyImage = new LazyImgMagic(element);
scope.lazyImage.setErrorSource(attributes.lazyImgError);
var deregister = attributes.$observe('lazyImg', function (newSource) {
if (newSource) {
deregister();
scope.lazyImage.setSource(newSource);
}
});
var eventsDeregister = $rootScope.$on('lazyImg:refresh', function () {
scope.lazyImage.checkImages();
});
scope.$on('$destroy', function () {
scope.lazyImage.removeImage();
eventsDeregister();
});
}
return {
link: link,
restrict: 'A'
};
}
])
.directive('lazyImgContainer', [
'LazyImgMagic', function (LazyImgMagic) {
'use strict';
function link(scope, element) {
LazyImgMagic.addContainer(element);
scope.$on('$destroy', function () {
LazyImgMagic.removeContainer(element);
});
}
return {
link: link,
restrict: 'A'
};
}
]);
}

View File

@@ -1,28 +0,0 @@
{
"name": "web",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "browserify ./app.js -o ./public/bundle.js",
"dev": "watchify ./app.js -o ./public/bundle.js"
},
"keywords": [],
"author": "",
"license": "Zlib",
"dependencies": {
"angular": "^1.6.10",
"angular-router-browserify": "0.0.2",
"angular-vs-repeat": "2.0.13"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.9.5",
"browserify": "^16.5.1",
"watchify": "^4.0.0"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,51 +0,0 @@
<html>
<head>
<title>dizqueTV</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/favicon.svg" ></link>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
<link href="style.css" rel="stylesheet">
<link href="custom.css" rel="stylesheet">
<script src="version.js"></script>
<script src="bundle.js"></script>
</head>
<body ng-app="myApp" style="min-width: 340px;">
<div class="container-fluid">
<h1>
<a href="#!/guide"><img id='dizquetv-logo' src="images/dizquetv.png" alt="logo" ></a>
dizqueTV
<small class="pull-right" style="padding: 5px;">
<a href="https://github.com/vexorian/dizquetv" title='Git Repository'>
<span class="fab fa-github text-sm"></span>
</a>
</small>
<small class="pull-right" style="padding: 5px;">
<a href="https://www.reddit.com/r/dizqueTV" title='Subreddit' >
<span class="fab fa-reddit"></span>
</a>
</small>
<small class="pull-right" style="padding: 5px;">
<a href="https://discord.gg/bgD9XdDvZE" title='Discord' >
<span class="fab fa-discord"></span>
</a>
</small>
</h1>
<a href="#!/guide">Guide</a> - <a href="#!/channels">Channels</a> - <a href="#!/library">Library</a> - <a href="#!/player">Player</a> - <a href="#!/settings">Settings</a> - <a href="#!/version">Version</a>
<span class="pull-right">
<span style="margin-right: 15px;">
<a href="/api/xmltv.xml">XMLTV <span class="far fa-file-code"></span></a>
</span>
<span>
<a href="/api/channels.m3u">M3U <span class="far fa-file-video"></span></a>
</span>
</span>
<hr></hr>
<div ng-view></div>
<toast-notifications></toast-notifications>
</div>
</body>
</html>

View File

@@ -1,393 +0,0 @@
:root {
--guide-text : #F0F0f0;
--guide-header-even: #423cd4ff;
--guide-header-odd: #262198ff;
--guide-color-a: #212121;
--guide-color-b: #515151;
--guide-color-c: #313131;
--guide-color-d: #414141;
}
.pull-right { float: right; }
.modal-semi-body {
padding: 1rem;
flex: 1 1 auto;
}
.plex-panel {
margin: 0;
padding: 0;
overflow-y: scroll;
}
.flex-container {
display: flex;
align-items: center;
}
.flex-pull-right {
margin-left: auto;
padding-right: 0.2em
}
.fa-plus-circle {
color: #daa104;
}
.fa-plus-circle {
color: #000;
}
.list-group.list-group-root .list-group-item {
border-radius: 0;
border-width: 1px 0 0 0;
padding: 0;
margin: 0;
cursor: pointer;
}
.list-group.list-group-root .list-group-item img {
height: 45px;
}
.list-group.list-group-root .list-group-item {
border-radius: 0;
border-width: 1px 0 0 0;
padding: 0;
margin: 0;
}
.list-group.list-group-root .list-group-item div .tab {
width: 25px;
display: inline-block;
text-align: center;
cursor: pointer;
}
.program-start {
margin-right: 2.5em;
display: inline-block;
vertical-align: top;
/*color: rgb(96,96,96);*/
color: #0c5c68;
font-size: 80%;
font-weight: 400;
font-family: monospace;
white-space: nowrap;
}
.program-row {
align-items: start;
}
.programming-counter {
white-space: nowrap;
margin-right: 1em;
font-size: 80%;
}
.programming-counter > span {
font-weight: 300;
}
.programming-counter > b {
font-weight: 400;
}
.btn-programming-tools {
padding: .25rem .5rem;
font-size: .875rem;
line-height: 1.0;
margin-right: 0.5rem;
}
.loader {
width: 1em;
height: 1em;
border: 0.3em solid #f3f3f3;
border-radius: 50%;
display: inline-block;
border-top: 0.25em solid #3498db;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
}
table.tvguide {
table-layout: fixed;
}
.tvguide th.hour {
padding-left: 0;
overflow: hidden;
white-space: nowrap;
}
.tvguide .program {
padding-left: 0.2em;
/*border-top: 1px solid black;
border-bottom: 1px solid black ;*/
overflow: hidden;
}
.tvguide .program-with-start {
}
.tvguide .program-with-end {
/*border-right: 1px solid black;*/
}
.tvguide .program .show-title {
white-space: nowrap;
font-weight: 400;
}
.tvguide .program .sub-title {
white-space: nowrap;
font-weight: 300;
}
.tvguide button {
padding-left: 0.1em;
padding-right: 0.1em;
}
.tvguide th button {
min-width: 20%;
max-width: 24%;
}
.tvguide th {
position: sticky;
top: 0;
bottom: 0;
/*border-bottom: 1px solid black;*/
}
.tvguide th.guidenav {
padding-left: 5px;
padding-right: 0;
}
.tvguide td, .tvguide th {
color: var(--guide-text);
border-top: 0;
height: 3.5em;
padding-top: 0;
padding-bottom: 0;
vertical-align: middle;
overflow: hidden;
}
.tvguide th {
height: 1.8em;
}
.tvguide td.channel-number, .tvguide td.channelLoading {
vertical-align: middle;
padding:0;
}
.tvguide td.channel-number div {
text-align:center;
width:100%;
padding:0;
}
.tvguide td.channel-icon {
align-items: center;
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0.2em;
text-align:center;
vertical-align: middle;
}
.tvguide td.channel-icon img {
max-height: 95%;
max-width:99%
}
.tvguide th.even {
background: var(--guide-header-even);
}
.tvguide th.odd {
background: var(--guide-header-odd);
}
.tvguide tr.odd td.even {
background: var(--guide-color-a);
}
.tvguide tr.odd td.odd {
background: var(--guide-color-b);
}
.tvguide tr.even td.odd {
background: var(--guide-color-c);
}
.tvguide tr.even td.even {
background: var(--guide-color-d) ;
}
.tvguide td .play-channel {
top:25%;
left:12.5%;
width:75%;
height:75%
}
.tv-guide-now {
width:0.2em;
height: 100%;
position: absolute;
background: #FFFF0040;
top:0;
}
.stealth-channel td img {
opacity: 0.5;
}
.flex-filler-percent {
text-align: right;
}
.filler-list .list-group-item, .program-row, .show-list .list-group-item, .program-row {
min-height: 1.5em;
}
.filler-list .list-group-item .title, .program-row .title, .show-list .list-group-item .title, .program-row .title {
margin-right: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.show-row .program-start {
width: 2em;
}
div.channel-tools {
max-height: 20em;
overflow-y: scroll;
overflow-x: hidden;
margin-bottom: 1.5rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
border-top: 1px solid #888;
border-bottom: 1px solid #888;
}
div.channel-tools p {
font-size: 0.5rem;
margin-top: 0.01rem;
}
div.programming-panes {
padding-top: 0;
padding-bottom: 0;
}
div.programming-panes div.reverse {
flex-direction: row-reverse;
}
div.programming-panes div.programming-pane {
overflow-y: auto;
padding-top: 0;
padding-bottom: 0;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
div.programming-programs div.list-group-item {
height: 1.5rem;
}
.channel-editor-modal-big {
width:1200px;
min-width: 98%;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.program-row:nth-child(odd), .show-row:nth-child(odd), .filler-row:nth-child(odd), .channel-row:nth-child(odd) {
background-color: #eeeeee;
}
.tools-pane button {
text-overflow: ellipsis;
overflow: hidden;
}
.tools-pane button:not(.btn-danger),
.tools-pane .input-group-text,
.tools-pane select {
border: 1px solid #999999 !important;
}
.tools-pane input,
.tools-pane select {
font-size: 14px;
}
.tools-pane select {
text-align: center;
border-radius: 0;
padding: 0 16px 0 0;
height: initial;
}
.tools-pane select:first-of-type {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.tools-pane .input-group-prepend + button {
border-left: 0;
}
.tools-pane input.form-control {
border-color: #999999;
}
.watermark-preview {
background: linear-gradient(180deg, rgb(90, 90, 90) 0%, rgb(110, 110, 110) 35%, rgb(130, 130, 130) 100%, rgb(150, 150, 150) 100%);
border: 2px solid black;
}
.watermark-preview .alternate-aspect {
background : rgba(255,255,255, 0.1);
}
.dizque-toast {
margin-top: 0.2rem;
padding: 0.5rem;
background: #FFFFFF;
border: 1px solid rgba(0,0,0,.1);
border-radius: .25rem;
color: #FFFFFF;
}
.dizque-toast.bg-warning {
color: black
}
.about-to-fade-in {
opacity: 0.00;
transition: opacity 1.00s ease-in-out;
-moz-transition: opacity 1.00s ease-in-out;
-webkit-transition: opacity 1.00s ease-in-out;
}
.fade-in {
opacity: 0.95;
transition: opacity 1.00s ease-in-out;
-moz-transition: opacity 1.00s ease-in-out;
-webkit-transition: opacity 1.00s ease-in-out;
}
.fade-out {
transition: opacity 1.00s ease-in-out;
-moz-transition: opacity 1.00s ease-in-out;
-webkit-transition: opacity 1.00s ease-in-out;
opacity: 0.0;
}
#dizquetv-logo {
width: 1em;
height: 1em;
margin-bottom: 0.25em;
}

View File

@@ -1,866 +0,0 @@
<div>
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div ng-class="{'channel-editor-modal-big':showShuffleOptions, 'modal-dialog-scrollable': (tab !== 'programming') }" class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<ul class="nav nav-tabs" >
<li class="nav-item" ng-repeat="x in tabOptions" >
<span class="nav-link btn btn-link {{ tab === x.id ? 'active' : ''}}" ng-click="setTab(x.id)">
<span ng-if="error.tab===x.id" class='text-danger'>
<i class='fas fa-exclamation-triangle' ></i> {{x.name}}
</span>
<span ng-if="error.tab!==x.id">
{{x.name}}
</span>
</span>
</li>
</ul>
<h5 class="modal-title">
{{ getTitle() }}
</h5>
</div>
<!-- ============= TAB: PROPERTIES ========================= -->
<div class="modal-body" ng-if="tab == 'basic'">
<div class='form-group'>
<label class='form-label' >Channel Number:</label>
<input type="text" class='form-control' type='number' ng-model="channel.number" id='channelNumber' aria-describedby="channelNumberHelp"></input>
<small id='channelNumberHelp' class="text-danger" for='channelNumber'>{{error.number}}</small>
</div>
<div class='form-group'>
<label class='form-label' >Channel Name:</label>
<input type="text" class='form-control' ng-model="channel.name" id='channelName' aria-describedby="channelNameHelp"></input>
<small id='channelNumberHelp' class="text-danger" for='channelNumber'>{{error.name}}</small>
</div>
<div class='form-group'>
<label class='form-label' >Channel Group:</label>
<input type="text" class='form-control' ng-model="channel.groupTitle" id='groupTitle' placeholder="dizqueTV" aria-describedby="groupTitleHelp"></input>
<small id='groupTitleHelp' class="text-muted" for='channelNumber'>This is used by iptv clients to categorize the channels. You can leave it as dizqueTV if you don&apos;t need this sort of classification.</small>
</div>
<div>
<span class="pull-right text-danger">{{error.icon}}</span>
<label for="channelIcon" class="small">Channel Icon</label>
<div class="input-group mb-1">
<input name="channelIcon" id="channelIcon" class="form-control form-control-sm" type="url" ng-model="channel.icon.path"></input>
<div class="input-group-append">
<input type="file"
accept="image/*"
class="form-control-file"
onchange="angular.element(this).scope().logoOnChange(event)"
name="logo"
id="logo">
</input>
</div>
</div>
<br></br>
<div>
<h6>Preview</h6>
<img ng-if="channel.icon && channel.icon.path !== ''" ng-src="{{channel.icon.path}}" alt="{{channel.name}}" style="max-height: 120px;"></img>
<span ng-if="!channel.icon || channel.icons.path === ''">{{channel.name}}</span>
</div>
</div>
</div>
<!--
============= TAB: PROGRAMMING =========================
-->
<div ng-show="tab == 'programming'" class='modal-semi-body'>
<div class='form-group row' >
<label for="channelStartTime" class="small col-form-label col-md-auto">Programming Start:</label>
<div class='col-md-auto'>
<input id="channelStartTime" class="form-control form-control-sm col-md-auto" type="datetime-local" ng-model="startTime" aria-describedby="startTimeHelp"></input>
<small class="text-danger" id='startTimeHelp'>{{error.startTime}}</small>
</div>
<label for="channelEndTime" class="small col-form-label col-md-auto">Programming End:</label>
<div class='col-md-auto'>
<input id="channelEndTime" class="form-control form-control-sm col-md-auto" type="datetime-local" ng-model="endTime" ng-disabled="true" aria-describedby="endTimeHelp"></input>
</div>
<div class='col-md-auto'>
<small class="text-muted form-text" id='endTimeHelp'>Programming will restart from the beginning.</small>
</div>
</div>
<div>
<div class="flex-container">
<div class='programming-counter' style='order:4'>
<span class="small"><b>Total:</b> {{channel.programs.length}}</span>
</div>
<div class='programming-counter' ng-show='hasFlex' style='order:4'>
<span class="small"><b>Filler Lists:</b> {{channel.fillerCollections.length}}</span>
</div>
<div class='programming-counter' ng-show='hasFlex' style='order:4'>
<span class="small"><b>Fallback:</b> {{describeFallback()}}</span>
</div>
<div class='flex-pull-right' ng-style="{order: (reverseTools?3:4) }"></div>
<div class="btn-group-toggle" data-toggle="buttons" ng-show='showShuffleOptions' ng-style="{order: (reverseTools?2:4) }"
title='{{ showHelp.check ? "Hide" : "Show" }} Tool Help'
>
<label class='btn btn-sm {{showHelp.check ? "btn-primary":"btn-outline-primary"}} btn-programming-tools'>
<input type="checkbox" ng-model='showHelp.check' ><i class='fas fa-question-circle'></i></input>
</label>
</div>
<div ng-style="{order: (reverseTools?1:4) }" >
<button class="btn btn-sm btn-outline-info btn-programming-tools"
ng-click="programmingZoomIn()"
title="Higher"
>
<span class="fa fa-arrow-circle-down"></span>
</button>
</div>
<div ng-style="{order: (reverseTools?1:4) }" >
<button class="btn btn-sm btn-outline-info btn-programming-tools"
ng-click="programmingZoomOut()"
title="Shorter"
>
<span class="fa fa-arrow-circle-up"></span>
</button>
</div>
<div ng-show='showShuffleOptions' ng-style="{order: (reverseTools?1:4) }" >
<button class="btn btn-sm btn-outline-info btn-programming-tools"
ng-click="toggleToolsDirection()"
>
<span
class="fa {{ reverseTools ? 'fa-long-arrow-alt-right' : 'fa-long-arrow-alt-left'}}"></span>
</button>
</div>
<div ng-style="{order: (reverseTools?0:4) }" >
<button class="btn btn-sm btn-secondary btn-programming-tools"
ng-click="toggleTools()"
>
<span
class="fa {{ showShuffleOptions ? 'fa-chevron-down' : 'fa-chevron-right'}}"></span>&nbsp;&nbsp;Tools
</button>
</div>
<div style='margin-left:0; order:4'>
<span class="text-danger small">{{error.programs}}</span>
<button class="btn btn-sm btn-primary" ng-click="showPlexLibrary()">
<span class="fa fa-plus"></span>
</button>
</div>
</div>
</div>
</div>
<div class="modal-body programming-panes" ng-show="tab == 'programming'"
ng-style="{'max-height':programmingHeight()}"
>
<div class='row' ng-class="{'reverse': reverseTools }" >
<div vs-repeat="options" ng-class="{'programming-pane': true, 'col':true, 'd-block': showShuffleOptions, 'd-sm-none': showShuffleOptions, 'd-md-block' : showShuffleOptions, container: true, 'list-group': true, 'list-group-root': true, 'programming-programs': true}" ng-show="hasPrograms()"
dnd-drop="dropFunction(index , item)"
dnd-list=""
ng-init="setUpWatcher()"
ng-if="true"
ng-style="{'max-height':programmingHeight()}"
>
<div ng-repeat="x in channel.programs track by x.$index"
ng-click="selectProgram(x.$index)"
class="list-group-item flex-container program-row" dnd-draggable="x" dnd-moved="" dnd-effect-allowed="move"
>
<div class="program-start">
{{ dateForGuide(x.startTimeMs) }}
</div>
<div ng-style="programSquareStyle(x)"></div>
<div ng-hidden="x.isOffline" class='title' >
{{ getProgramDisplayTitle(x) }}
</div>
<div style="font-weight:ligther" ng-show="x.isOffline" class='title' >
<i ng-if="x.type !== 'redirect' " >Flex</i>
<span ng-if="x.type === 'redirect' " ><i>Redirect to channel:</i> <b>{{x.channel}}</b></span>
</div>
<div class="flex-pull-right"></div>
<button class="btn btn-sm btn-link" ng-click="removeItem(x.$index); $event.stopPropagation()">
<i class="text-danger fa fa fa-trash-alt"></i>
</button>
</div>
</div>
<div ng-class='{"col-sm-4": showShuffleOptions, "col-md-8": showShuffleOptions, "col-lg-7": showShuffleOptions, "col-xl-6": showShuffleOptions, "col" : !showShuffleOptions }' ng-if="! hasPrograms()">
<small class='text-info'>There are no programs in the channel, use the <i class='fas fa-plus'></i> button to add programs from your media library or use the Tools to add Flex time or a Channel Redirect</small>
</div>
<div class='col-md-4 col-sm-12 col-xl-6 col-lg-5 programming-pane tools-pane' ng-show="showShuffleOptions"
ng-style="{'max-height':programmingHeight()}"
>
<div class="row">
<div class="col-xl-6 col-md-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<input type="number" class="form-control form-control-sm" placeholder="Desired number of consecutive TV shows." min="1" max="10" ng-model="blockCount" style="width:5em">
</input>
</div>
<div class="input-group-prepend">
<div class="input-group-text" style="padding: 0;">
<label class="small" for="randomizeBlockShuffle" style="margin-bottom: 2px;">&nbsp;Randomize&nbsp;&nbsp;</label>
<input id="randomizeBlockShuffle" type="checkbox" ng-model="randomizeBlockShuffle"></input>
&nbsp;
</div>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="blockShuffle(blockCount, randomizeBlockShuffle)">
<i class='fa fa-random' title='Block Shuffle' ></i> Block Shuffle
</button>
</div>
<p ng-show='showHelp.check'>Alternates TV shows in blocks of episodes. You can pick the number of episodes per show in each block and if the order of shows in each block should be randomized. Movies are moved to the bottom.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="randomShuffle()" aria-describedby="randomShuffleHelp" title='Random Shuffle'>
<i class='fa fa-random'></i> Random Shuffle
</button>
</div>
<p for="randomShuffleHelp" class='form-label' ng-show='showHelp.check'>
Completely randomizes the order of programs.
</p>
</div>
<div class='col-xl-3 col-lg-6' style="padding: 5px;" ng-show="hasPrograms()" >
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="cyclicShuffle()" title='Cyclic Shuffle' >
<i class='fa fa-random'></i> Cyclic Shuffle
</button>
</div>
<p ng-show='showHelp.check'>Like Random Shuffle, but tries to preserve the sequence of episodes for each TV show. If a TV show has multiple instances of its episodes, they are also cycled appropriately.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="replicaCount" style="width:5em">
</div>
<button ng-disabled="!(replicaCount &gt;= 2)" class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="replicate(replicaCount)" title='Replicate' >
<i class='fas fa-recycle'></i> Replicate
</button>
</div>
<p ng-show='showHelp.check'>Makes multiple copies of the schedule and plays them in sequence. Normally this isn&apos;t necessary, because dizqueTV will always play the schedule back from the beginning when it finishes. But creating replicas is a useful intermediary step sometimes before applying other transformations. Note that because very large channels can be problematic, the number of replicas will be limited to avoid creating really large channels.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="randomReplicaCount" style="width:5em">
</div>
<button ng-disabled="!(randomReplicaCount &gt;= 2)" class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="shuffleReplicate(randomReplicaCount)" title='Replicate &amp; Shuffle' >
<i class='fas fa-dice'></i> Replicate &amp; Shuffle
</button>
</div>
<p ng-show='showHelp.check'>Like &quot;Replicate&quot;, it will make multiple copies of the programming. In addition it will shuffle the programs, but it will make sure not to have too small a distance between two identical programs.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div class='input-group'>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortShows()" title='Sort TV Shows' >
<i class='fa fa-sort-alpha-down'></i> Sort TV Shows
</button>
</div>
<p ng-show='showHelp.check'>Sorts the list by TV Show and the episodes in each TV show by their season/episode number.
Movies are moved to the bottom of the schedule.
</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()" title='Sort Release Dates' >
<i class='fa fa-sort-numeric-down'></i> Sort Release Dates
</button>
</div>
<p ng-show='showHelp.check'>Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="equalizeShows()" title='Balance Shows' >
<i class='fa fa-balance-scale'></i> Balance Shows
</button>
</div>
<p ng-show='showHelp.check'>Will replicate some TV shows or delete duplicates of other TV shows in an effort to make it so the total durations of all episodes of each episode are as similar as possible. It&apos;s usually impossible to make the shows perfectly balanced without creating a really high number of duplicates, but it will try to get close. Movies are treated as a single show.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div class='input-group'>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="startFrequencyTweak()" title='Tweak Weights...'>
<i class='fa fa-balance-scale'></i> Tweak Weights...
</button>
</div>
<p ng-show='showHelp.check'>Similar to Balance TV Shows, but this allows you to pick the weights for each of the shows, so you can decide that some shows should be less frequent than other shows. It has similar caveats as &quot;Balance Shows&quot;.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;">
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addOffline()" title='Add Flex...'>
<i class='fa fa-plus'></i> Add Flex...
</button>
</div>
<p ng-show='showHelp.check'>Programs a Flex time slot. Normally you&apos;d use pad times, restrict times or add breaks to add a large quantity of Flex times at once, but this exists for more specific cases.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" ng-model="nightStart"
ng-options="o.id as o.description for o in nightStartHours" ></select>
<select class="custom-select" ng-model="nightEnd"
ng-options="o.id as o.description for o in nightEndHours" ></select>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="nightChannel(nightStart, nightEnd)" ng-disabled="nightStart==-1 || nightEnd==-1" title='Restrict Hours' >
<i class='far fa-moon'></i> Restrict Hours
</button>
</div>
<p ng-show='showHelp.check'>The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" ng-model="paddingOption"
ng-options="o as o.description for o in paddingOptions" ></select>
</div>
<button ng-disabled="disablePadding()" class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="padTimes(paddingOption.id, paddingOption.allow5)" title='Pad Times' >
<i class='far fa-clock'></i> Pad Times
</button>
</div>
<p ng-show='showHelp.check'>Adds Flex breaks after each TV episode or movie to ensure that the program starts at one of the allowed minute marks. For example, you can use this to ensure that all your programs start at either XX:00 times or XX:30 times. Removes any existing Flex periods before adding the new ones. This button might be disabled if the channel is already too large.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" style="width:5em" ng-model="breakAfter"
ng-options="o.id as o.description for o in breakAfterOptions" ></select>
<select class="custom-select" style="width:5em" ng-model="minBreakSize"
ng-options="o.id as o.description for o in minBreakSizeOptions" ></select>
<select class="custom-select" style="width:5em" ng-model="maxBreakSize"
ng-options="o.id as o.description for o in maxBreakSizeOptions" ></select>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addBreaks(breakAfter, minBreakSize, maxBreakSize)" ng-disabled="breaksDisabled()" title='Add Breaks' >
<i class='fa fa-coffee'></i> Add Breaks
</button>
</div>
<p ng-show='showHelp.check'>Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes. This button might be disabled if the channel is already too large.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" ng-model="rerunStart"
ng-options="o.id as o.description for o in rerunStartHours">
</select>
<select class="custom-select" ng-model="rerunBlockSize"
ng-options="o.id as o.description for o in rerunBlockSizes">
</select>
<select class="custom-select" ng-model="rerunRepeats"
ng-options="o.id as o.description for o in rerunRepeatOptions">
</select>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="doReruns(rerunStart, rerunBlockSize, rerunRepeats)" ng-disabled="rerunsDisabled()" title='Reruns' >
<i class='far fa-clone'></i> Reruns
</button>
</div>
<p ng-show='showHelp.check'>Divides the programming in blocks of 6, 8 or 12 hours then repeats each of the blocks the specified number of times. For example, you can make a channel that plays exactly the same channels in the morning and in the afternoon. This button might be disabled if the channel is already too large.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group" >
<div class="input-group-prepend">
<button class="btn btn-sm btn-secondary form-control form-control-sm" type="button" ng-click="savePositions()">
<i class='fa fa-file-import'></i> Save
</button>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="recoverPositions()" ng-disabled='cannotRecoverPositions()' title='Recover Episode Positions' >
<i class='fa fa-file-export'></i> Recover Episode Positions
</button>
</div>
<p ng-show='showHelp.check'>The &quot;Save&quot; button saves the current episodes that are next to be played for each tv show. Then whenever you click the &quot;Recover Episode Popsitions&quot; button, episodes will be rearranged cyclically and they will start with the saved positions. So you can maintain episode sequences even after modifying the channel. If there are any new TV shows, they will start at their current positions. Movies and specials won&apos;t change positions.
</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" >
<div class='input-group'>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addRedirect()" title='Add Redirect...' >
<i class='fas fa-external-link-alt'></i> Add Redirect...
</button>
</div>
<p ng-show='showHelp.check'>Adds a channel redirect. During this period of time, the channel will redirect to another channel.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<div class='loader' ng-hide='channelsDownloaded'></div>
<select class="custom-select" ng-show='channelsDownloaded' style='width:5em;' ng-model="atNightChannelNumber"
ng-options="o.id as o.description for o in knownChannels" ></select>
<select class="custom-select" ng-model="atNightStart"
ng-options="o.id as o.description for o in nightStartHours" ></select>
<select class="custom-select" ng-model="atNightEnd"
ng-options="o.id as o.description for o in nightEndHours" ></select>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="nightChannel(atNightEnd, atNightStart, atNightChannelNumber)" ng-disabled="atNightChannelNumber==-1 || atNightStart==-1 || atNightEnd==-1" title='&quot;Channel at Night&quot;' >
<i class='far fa-moon'></i> &quot;Channel at Night&quot;
</button>
</div>
<p ng-show='showHelp.check'>Will redirect to another channel while between the selected hours.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<button class='btn btn-sm btn-warning form-control' ng-click="slideAllPrograms(-slide.value)"
ng-disabled="slide.value == -1"
title="Rewind"
>
<i class='fas fa-backward'></i> Rewind
</button>
</div>
<select class="custom-select" ng-model="slide.value"
ng-options="o.id as o.description for o in slide.options" ></select>
<div class="input-group-append">
<button class='btn btn-sm btn-warning form-control' ng-click="slideAllPrograms(slide.value)"
ng-disabled="slide.value == -1"
title="Fast-Forward"
>
<i class='fas fa-forward'></i> Fast-Forward
</button>
</div>
</div>
<p ng-show='showHelp.check'>Slides the whole schedule. The &quot;Fast-Forward&quot; button will advance the stream by the specified amount of time. The &quot;Rewind&quot; button does the opposite.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<button class='btn btn-sm btn-warning form-control' ng-click="onTimeSlotsButtonClick()"
title="Time Slots..."
>
<i class='fas fa-blender'></i> Time Slots...
</button>
</div>
<p ng-show='showHelp.check'>This allows to schedul specific shows to run at specific time slots of the day or a week. It&apos;s recommended you first populate the channel with the episodes from the shows you want to play and/or other content like movies and redirects.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<button class='btn btn-sm btn-warning form-control' ng-click="onRandomSlotsButtonClick()"
title="Random Slots..."
>
<i class='fas fa-flask'></i> Random Slots...
</button>
</div>
<p ng-show='showHelp.check'>This is similar to Time Slots, but instead of time sections, you pick a probability to play each tv show and the length of the block.</p>
</div>
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms()">
<div class='input-group'>
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeDuplicates()" title='Remove Duplicates' >
<i class='fa fa-trash-alt'></i> Duplicates
</button>
</div>
<p ng-show='showHelp.check'>Removes repeated videos.</p>
</div>
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms()">
<div class='input-group'>
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeOffline()" title='Remove Flex' >
<i class='fa fa-trash-alt'></i> Flex
</button>
</div>
<p ng-show='showHelp.check'>Removes any Flex periods from the schedule.</p>
</div>
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms()">
<div class='input-group'>
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSpecials()" title='Remove Specials' >
<i class='fa fa-trash-alt'></i> Specials
</button>
</div>
<p ng-show='showHelp.check'>Removes any specials from the schedule. Specials are episodes with season &quot;00&quot;.</p>
</div>
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms()">
<div class='input-group'>
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="startRemoveShows()" title='Remove Show(s)...' >
<i class='fa fa-trash-alt'></i> Show(s)...
</button>
</div>
<p ng-show='showHelp.check'>Allows you to pick specific shows to remove from the channel.</p>
</div>
<div class="col" style="padding: 5px;" ng-show="hasPrograms()">
<div class='input-group'>
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSchedule()" title='Remove All' >
<i class='fa fa-trash-alt'></i> All
</button>
</div>
<p ng-show='showHelp.check'>Wipes out the schedule so that you can start over.</p>
</div>
</div>
</div>
</div>
</div>
<!--
============= TAB: FLEX =========================
-->
<div class="modal-body" ng-if="tab == 'flex'">
<div class='text-center text-info' ng-show='!hasFlex'>
<i class='fas fa-info-circle' ></i> Use programming tools like &quot;Add Padding&quot;, &quot;Add Breaks&quot; or &quot;Add Flex&quot; to add &quot;Flex&quot; time slots to the channel&apos;s programming.
</div>
<div class='row'>
<div class="col-md-12">
<label for="offlineMode" class="small">Fallback Mode:</label>
<div class="input-group mb-1">
<select class="form-control form-control-sm" id="offlineMode" ng-model="channel.offlineMode">
<option value="pic">Picture</option>
<option value="clip">Clip from Library</option>
</select>
</div>
</div>
</div>
<div ng-show="channel.offlineMode == 'clip'" >
<p class="text-center text-info">Pick a video clip that will be used for fallback when there's no appropriate
filler available for the time duration. It's recommended to use countdown or looping videos for this. <span class="text-danger">{{error.fallback}}</span></p>
</div>
<div class='row' ng-show="channel.offlineMode == 'pic'" >
<div class="col-md-3">
<img ng-src="{{channel.offlinePicture}}" alt="Fallback preview" style="max-height: 120px;"></img>
</div>
<div class="col-md-9">
<div>
<label for="offlinePicture" class="small">
Picture: <span class="text-danger pull-right">{{error.picture}}</span></label>
<input name="offlinePicture" id="offlinePicture" class="form-control form-control-sm" type="url" ng-model="channel.offlinePicture"></input>
</div>
<div>
<label for="offlineSound" class="small">Sound Track:<span class="text-danger pull-right">{{error.sound}}</span></label>
<input name="offlineSound" id="offlineSound" class="form-control form-control-sm" type="url" ng-model="channel.offlineSoundtrack" placeholder="URL to a sound track that will loop during the offline screen, leave empty for silence."></input>
</div>
</div>
</div>
<div class='row' ng-show="channel.offlineMode == 'pic'" >
<div class='col-md-12'><p class="text-center text-info">This picture is used in case there are no filler clips available with a shorter length than the Flex time duration. Requires ffmpeg transcoding.</p></div>
</div>
<div ng-show="channel.offlineMode == 'clip'">
<div class="list-group list-group-root" dnd-list="channel.fallback">
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in channel.fallback" dnd-draggable="x" dnd-moved="channel.fallback.splice($index, 1)" dnd-effect-allowed="move">
<div class="program-start" >
{{durationString(x.duration)}}
</div>
<div ng-style="programSquareStyle(x, true)"></div>
<div style="margin-right: 5px;">
<strong>Fallback:</strong> {{x.title}}
</div>
<div class="flex-pull-right">
<button class="btn btn-sm btn-link" ng-click="channel.fallback.splice($index,1)">
<i class="text-danger fa fa-trash-alt" ></i>
</button>
</div>
</div>
</div>
<div ng-show="channel.fallback.length === 0">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="openFallbackLibrary()">Pick fallback</button>
</div>
<hr style='margin-top:0'></hr>
</div>
<div>
<h5 style="margin-top: 10px;">Filler</h5>
<div>
<label>Minimum time before replaying a filler (Minutes): </label>
<input type="number" class="form-control form-control-sm" ng-model="channel.fillerRepeatCooldownMinutes" ng-pattern="/^([1-9][0-9]*)$/" min='0' max='10080'></input>
<span class="text-danger pull-right">{{error.blockRepeats}}</span>
</div>
<div>
<input id="overlayDiableIcon" type="checkbox" ng-model="channel.disableFillerOverlay">&nbsp;
<label class="small" for="overlayDisableIcon" style="margin-bottom: 4px;">&nbsp;Disable channel watermark when playing filler&nbsp;&nbsp;</label>
</div>
<hr></hr>
<h6>Filler Lists</h6>
<div id='fillerContainer'>
<br></br>
<div class="form-row" ng-repeat = "x in channel.fillerCollections" track-by = "$index">
<div class='form-group col-md-5'>
<label ng-if="$index==0" for="fillerselect{{$index}}">List</label>
<select
id="fillerselect{{$index}}" class="custom-select form-control"
ng-model="x.id" ng-options="o.id as o.name for o in x.options"
ng-change="refreshFillerStuff()"
>
</select>
</div>
<div class='form-group col-md-2' ng-if="x.id !== &apos;none&apos; " >
<label ng-if="$index==0" for="cooldown{{$index}}">Cooldown (minutes)</label>
<input class='form-control' id="cooldown{{$index}}" type='number' ng-model='x.cooldownMinutes' ng-pattern="/^([0-9][0-9]*)$/"
min='0' max='10080'
data-toggle="tooltip" data-placement="bottom" title="The channel won&apos;t pick a video from this list if it played something from this list less than this amount of minutes ago."
> </input>
</div>
<div class='form-group col-md-2' ng-if="x.id === &apos;none&apos; " >
</div>
<div class='form-group col-md-3' ng-if="x.id !== &apos;none&apos; &amp;&amp; channel.fillerCollections.length &gt; 2 " >
<label ng-if="$index==0" for="fillerrange{{$index}}">Weight</label>
<input class='form-control-range custom-range' id="fillerrange{{$index}}" type='range' ng-model='x.weight' min=1 max=600
data-toggle="tooltip" data-placement="bottom" title="Lists with more weight will be picked more frequently."
ng-change="refreshFillerStuff()"
>
</input>
</div>
<div class='form-group col-md-4' ng-if="x.id === &apos;none&apos; || channel.fillerCollections.length &lt;= 2 " >
</div>
<div class='form-group col-md-1' ng-if="x.id !== &apos;none&apos; &amp;&amp; channel.fillerCollections.length &gt; 2" >
<label ng-if="$index==0" for="fillerp{{$index}}">%</label>
<input class='form-control flex-filler-percent' id="fillerp{{$index}}" type='text' ng-model='x.percentage'
data-toggle="tooltip" data-placement="bottom" title="This is the overall probability this list might be picked, assuming all lists are available." readonly
>
</input>
</div>
<div class='form-group col-md-1' ng-if="x.id !== &apos;none&apos;" >
<label ng-if="$index==0" for="delete{{$index}}">-</label>
<button id='delete{{$index}}' class='btn btn-link form-control' ng-click='deleteFillerList($index)' >
<i class='text-danger fa fa-trash-alt'></i>
</button>
</div>
</div>
<hr></hr >
<p class="text-center text-info">Videos from the filler list will be randomly picked to play unless there are cooldown restrictions to place or if no videos are short enough for the remaining Flex time. Use the Filler tab at the main page to create Filler Lists. If no videos are available, the fallback will be used instead.</p>
</div>
</div>
</div>
<!--
============= TAB: EPG =========================
-->
<div class="modal-body" ng-if="tab == 'epg'">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="stealth" aria-describedby="stealthHelp" ng-model='channel.stealth'>
<label class="form-check-label" for="stealth">Stealth Mode</label>
<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>
<br></br>
<div class='form-group' ng-show='! channel.stealth'>
<label class='form-label' >Placeholder program title:</label>
<input type="text" class='form-control' ng-model="channel.guideFlexPlaceholder" placeholder="Leave empty so that it uses the channel&apos;s name" id='guideFlex' aria-describedby="guideFlexHelp"></input>
<small id='guideFlexHelp' class="text-muted" for='guideFlex'>This is the name of the fake program that will appear in the TV guide when there are no programs to display in that time slot guide. E.g when a large Flex block is scheduled.</small>
</div>
<div class='form-group' ng-show='! channel.stealth'>
<label class='form-label'>Minimum program duration to appear in the TV guide (seconds): </label>
<input type="number" class="form-control" ng-model="channel.guideMinimumDurationSeconds" ng-pattern="/^([0-9][0-9]*)$/" min='0' max='36288000' id='guideFlexTime' aria-describedby="guideFlexTimeHelp"></input>
<small id='guideFlexTimeHelp' class="text-muted" for='guideFlexTime'>Programs shorter than this value will be treated the same as Flex time. Meaning that the TV Guide will try to meld them with the previous program or display the block of programs as the &quot;place holder program&quot; if they make a large continuous group. Use 0 to disable this feature or use a large value to make the channel report only the placeholder program and not the real programming.</small>
</div>
</div>
<!--
============= TAB: ffmpeg =========================
-->
<div class="modal-body" ng-if="tab == 'ffmpeg'">
<small class='text-info'>These features require ffmpeg transcoding to be enabled in FFmpeg settings</small>
<hr></hr>
<h6>Channel Watermark</h6>
<div class='form-check'>
<input class="form-check-input" type="checkbox" ng-model="channel.watermark.enabled" id="overlayCheck"></input>
<label class="form-check-label" for="overlayCheck">
Enable Watermark
</label>
<small class='text-muted form-ext' >Renders a channel icon (also known as bug or Digital On-screen Graphic) on top of the channel&apos;s stream.</small>
</div>
<div ng-show="channel.watermark.enabled" class='row' >
<div class='col-md-3 col-lg-4 col-xl-5'>
<h7>Preview</h7>
<div ng-style='getWatermarkPreviewOuter()' class='watermark-preview'>
<div ng-style='getWatermarkPreviewRectangle(4,3)' class='alternate-aspect' ></div>
<div ng-style='getWatermarkPreviewRectangle(16,9)' class='alternate-aspect' ></div>
<img src='{{ getWatermarkSrc() }}' ng-style='getWatermarkPreviewInner()'></img>
</div>
</div>
<div class='col'>
<small class='text-danger' ng-show='error.watermark'>{{ error.watermark }}</small>
<div class='form-group'>
<label for="overlayURL">
Watermark Picture URL:
</label>
<input id='overlayURL' class='form-control' type='url' ng-model='channel.watermark.url' placeholder="Leave empty to use the channel&apos;s icon.">
</input>
<div class="input-group-append">
<input type="file"
accept="image/*"
class="form-control-file"
onchange="angular.element(this).scope().watermarkOnChange(event)"
name="logo"
id="logo">
</input>
</div>
</div>
<div class="form-group">
<label for="watermarkPosition">Position:</label>
<select class="form-control custom-select" id="watermarkPosition" ng-model="channel.watermark.position">
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
<div class='form-row'>
<div class='form-group col-sm-auto'>
<label for="watermarkWidth" >
Width %:
</label>
<input id='watermarkWidth' class='form-control' type='number' ng-model='channel.watermark.width' min="0" max="100" step="any" ng-disabled='channel.watermark.fixedSize' >
</input>
</div>
<div class='form-group col-sm-auto'>
<label for="watermarkHorizontal">
Horizontal Margin %:
</label>
<input id='watermarkHorizontal' class='form-control' type='number' ng-model='channel.watermark.horizontalMargin' min="0" max="100" step="any">
</input>
</div>
<div class='form-group col-sm-auto'>
<label for="watermarkVertical">
Vertical Margin %:
</label>
<input id='watermarkVertical' class='form-control' type='number' ng-model='channel.watermark.verticalMargin' min="0" max="100" step="any" >
</input>
</div>
</div>
<br>
<div class='form-check col-sm-auto'>
<input class="form-check-input" type="checkbox" ng-model="channel.watermark.fixedSize" id="overlayFixed"></input>
<label class="form-check-label" for="overlayFixed">
Disable Image Scaling
</label>
<small class='text-muted form-text' >The image will be rendered at its actual size without applying any scaling to it.</small>
</div>
<div class='form-check col-sm-auto'>
<input class="form-check-input" type="checkbox" ng-model="channel.watermark.animated" id="overlayAnimated"></input>
<label class="form-check-label" for="overlayAnimated">
Animated Image
</label>
<small class='text-muted form-text' >Tick this if and only if the watermark is an animated GIF or PNG. It will make it loop or not loop according to the image&apos;s configuration. If the image is not animated, there will be playback errors.</small>
</div>
<br>
<div class='form-group'>
<label for="overlayDuration">
Overlay Duration (seconds) (0 = permanent):
</label>
<input id='overlayDuration' class='form-control' type='number' ng-model='channel.watermark.duration' min=0>
</input>
</div>
</div>
</div>
<hr></hr>
<h6>Transcoding settings</h6>
<div class='row'>
<div class="form-group col-sm-auto">
<label for="channelResolution">Channel Resolution:</label>
<select class="form-control custom-select" id="channelResolution" ng-model="channel.transcoding.targetResolution"
ng-options="o.id as o.description for o in resolutionOptions"
>
</select>
</div>
<div class="form-group col-sm-auto">
<label for="channelBitrate">Video Bitrate (K):</label>
<input id='channelBitrate' class='form-control' type='number' ng-model='channel.transcoding.videoBitrate' min=0 placeholder='{{videoRateDefault}}'>
</input>
<small class='text-muted form-text'>Leave unassigned to use the global setting</small>
</div>
<div class="form-group col-sm-auto">
<label for="channelBufsize">Video Buffer Size (K):</label>
<input id='channelBufsize' class='form-control' type='number' ng-model='channel.transcoding.videoBufSize' min=0 placeholder='{{videoBufSizeDefault}}'>
</input>
<small class='text-muted form-text'>Leave unassigned to use the global setting</small>
</div>
</div>
</div>
<div class="modal-footer">
<span class="pull-right text-danger" ng-show="error.any"> <i class='fa fa-exclamation-triangle'></i> There were errors. Please review the form.</span>
<span class="pull-right text-info" ng-show='! hasPrograms() && (tab != "programming")'> <i class='fas fa-info-circle'></i> Use the &quot;Programming&quot; tab to add programs to the channel.</span>
<div class="text-right">
<button class="btn btn-sm btn-link" ng-click="_onDone()">
Cancel
</button>
<button class="btn btn-sm btn-primary" ng-click="_onDone(channel)" ng-disabled='! hasPrograms()'>
{{ isNewChannel ? 'Add Channel' : 'Update Channel' }}
</button>
</div>
</div>
</div>
</div>
</div>
<program-config program="_selectedProgram" on-done="finshedProgramEdit"></program-config>
<flex-config offline-title="Modify Flex Time" program="_selectedOffline" on-done="finishedOfflineEdit"></flex-config>
<frequency-tweak programs="_programFrequencies" message="_frequencyMessage" modified="_frequencyModified" on-done="tweakFrequencies"></frequency-tweak>
<remove-shows program-infos="_removablePrograms" on-done="removeShows" deleted="_deletedProgramNames"></remove-shows>
<flex-config offline-title="Add Flex Time" program="_addingOffline" on-done="finishedAddingOffline"></flex-config>
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>
<channel-redirect visible="_displayRedirect" on-done="finishRedirect" form-title="_redirectTitle" program="_selectedRedirect" ></channel-redirect>
<time-slots-schedule-editor linker="registerTimeSlots" on-done="onTimeSlotsDone"></time-slots-schedule-editor>
<random-slots-schedule-editor linker="registerRandomSlots" on-done="onRandomSlotsDone"></random-slots-schedule-editor>
</div>

View File

@@ -1,35 +0,0 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">{{ formTitle }}</h5>
</div>
</div>
<div class="modal-body container">
<div class="form-group">
<label for="duration">Duration (seconds):</label>
<input id="duration" class="form-control" ng-model="durationSeconds" type="text" placeholder="{{state.server.name}}"></input>
</div>
<div ng-if="state.channelReport == null" class="form-group">
<label for="channel">Redirect to channel:</label>
<select id="channel" class="form-control" ng-model="program.channel"
ng-options="o.id as o.description for o in options" ></select>
<div class="loader" ng-if="loading"></div>
</div>
</div>
<div class="modal-footer">
<span class="text-danger">{{ error }}</span>
<button type="button" class="btn btn-sm btn-link" ng-click="onCancel()" >Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="onDone()" >Save</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,38 +0,0 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Delete Filler</h5>
</div>
<div style='padding-left: 1rem; padding-right: 1rem' >
<p>Are you sure you want to delete Filler List: {{name}}?</p>
<p ng-if='channels.length &gt; 0'>
The filler is currently in use by these channels:
</p>
</div>
</div>
<div class="modal-body container list-group list-group-root" vs-repeat="options" dnd-list="content">
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in channels">
<div style='margin-left: 3rem' >
<span class='text-secondary'>{{x.number}} - {{x.name}}</span>
</div>
</div>
</div>
<div class="modal-footer">
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="finished(false);"><i class='fas fa-trash-alt'></i> Delete Filler</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,203 +0,0 @@
<div>
<h5>FFMPEG Settings
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
Update
</button>
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
Reset Options
</button>
</h5>
<h6>FFMPEG Executable Path (eg: C:\ffmpeg\bin\ffmpeg.exe || /usr/bin/ffmpeg)</h6>
<input type="text" class="form-control form-control-sm" ria-describedby="ffmpegHelp" ng-model="settings.ffmpegExecutablePath"></input>
<small id="ffmpegHelp" class="form-text text-muted">FFMPEG version 4.2+ required. Check by opening the version tab</small>
<hr></hr>
<h6>Miscellaneous Options</h6>
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<div class="form-group">
<label>Threads</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.numThreads"></input>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label>Logging</label>
<br>
<input id="logFfmpeg" type="checkbox" ng-model="settings.enableLogging"></input>
<label for="logFfmpeg">Log FFMPEG to console</label>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label>Video Buffer</label>
<select ng-model="settings.concatMuxDelay" ria-describedby="concatMuxDelayHelp"
ng-options="o.id as o.description for o in muxDelayOptions" ></select>
<small id="concatMuxDelayHelp" class="form-text text-muted">Note: If you experience playback issues upon stream start, try increasing this.</small>
</div>
</div>
</div>
<hr></hr>
<h6>Transcoding Features</h6>
<div class="row">
<div class="col-sm-9">
<input id="enableFFMPEGTranscoding" type="checkbox" ng-model="settings.enableTranscoding" ></input>
<label for="enableFFMPEGTranscoding">Enable FFMPEG Transcoding</label>
<small class="form-text text-muted">Transcoding is required for some features like channel overlay and measures to prevent issues when switching episodes. The trade-off is quality loss and additional computing resource requirements.
</small>
</div>
</div>
<br ></br>
<div class="form-group" ng-hide="isTranscodingNotNeeded()" >
<div class="row">
<div class="col-sm-9">
<label>Preferred Resolution</label>
<select ng-model="resolutionString" aria-describedby="concatMuxDelayHelp" ng-options="o.id as o.description for o in resolutionOptions" ></select>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<label>Video Encoder</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.videoEncoder" ria-describedby="videoEncoderHelp"></input>
<small id="videoEncoderHelp" class="form-text text-muted">Some possible values are:</small>
<small id="videoEncoderHelp" class="form-text text-muted">h264 with Intel Quick Sync: h264_qsv</small>
<small id="videoEncoderHelp" class="form-text text-muted">MPEG2 with Intel Quick Sync: mpeg2_qsv</small>
<small id="videoEncoderHelp" class="form-text text-muted">NVIDIA: h264_nvenc</small>
<small id="videoEncoderHelp" class="form-text text-muted">MPEG2: mpeg2video (default)</small>
<small id="videoEncoderHelp" class="form-text text-muted">H264: libx264</small>
<small id="videoEncoderHelp" class="form-text text-muted">MacOS: h264_videotoolbox</small>
</div>
<div class="col-sm-1" ></div>
<div class="col-sm-4">
<label>Audio Encoder</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.audioEncoder" ria-describedby="audioEncoderHelp"></input>
<small id="audioEncoderHelp" class="form-text text-muted">Some possible values are:</small>
<small id="audioEncoderHelp" class="form-text text-muted">aac</small>
<small id="audioEncoderHelp" class="form-text text-muted">ac3 (default), ac3_fixed</small>
<small id="audioEncoderHelp" class="form-text text-muted">flac</small>
<small id="audioEncoderHelp" class="form-text text-muted">libmp3lame</small>
</div>
</div>
<br ></br>
<div class="form-group">
<label>Video Bitrate (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.videoBitrate"></input>
<br ></br>
<label>Video Buffer Size (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.videoBufferSize"></input>
<br ></br>
<label>Max Frame Rate</label>
<select class='form-control custom-select' ng-model="settings.maxFPS" ria-describedby="fpsHelp"
ng-options="o.id as o.description for o in fpsOptions" ></select>
<small id='fpsHelp' class='form-text text-muted'>Will transcode videos that have FPS higher than this.</small>
<br ></br>
<label>Scaling Algorithm</label>
<select class='form-control custom-select' ng-model="settings.scalingAlgorithm" ria-describedby="scalingHelp"
ng-options="o.id as o.description for o in scalingOptions" ></select>
<small id='scalingHelp' class='form-text text-muted'>Scaling algorithm to use when the transcoder needs to change the video size.</small>
<br ></br>
<label>Deinterlace Filter</label>
<select class='form-control custom-select' ng-model="settings.deinterlaceFilter" ria-describedby="deinterlaceHelp"
ng-options="o.value as o.description for o in deinterlaceOptions" ></select>
<small id='deinterlaceHelp' class='form-text text-muted'>Deinterlace filter to use when video is interlaced. This is only needed when Plex transcoding is not used.</small>
</div>
<div class="form-group">
<label>Audio Bitrate (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioBitrate"></input>
<br ></br>
<label>Audio Buffer Size (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioBufferSize"></input>
<br ></br>
<label>Audio Volume (%)</label>
<input type="number" ria-describedby="volumeHelp" class="form-control form-control-sm" ng-model="settings.audioVolumePercent"></input>
<small id="volumeHelp" class="form-text text-muted">Values higher than 100 will boost the audio.</small>
<br ></br>
<label>Audio Channels</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioChannels"></input>
<br ></br>
<label>Audio Sample Rate (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioSampleRate"></input>
</div>
<div class="form-group">
<div>
<label>Error Screen:</label>
<select ng-model="settings.errorScreen" ria-describedby="errorHelp"
ng-options="o.value as o.description for o in errorScreens" ></select>
<label>Audio:</label>
<select ng-model="settings.errorAudio" ria-describedby="errorHelp"
ng-options="o.value as o.description for o in errorAudios" ></select>
</div>
<small id="errorHelp" class="form-text text-muted">If there are issues playing a video, dizqueTV will try to use an error screen as a placeholder while retrying loading the video every 60 seconds.</small>
</div>
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<input id="enableNormalizeResolution" type="checkbox" ng-model="settings.normalizeResolution" ng-disabled="isTranscodingNotNeeded()" ></input>
<label for="enableNormalizeResolution">Normalize Resolution</label>
<small class="form-text text-muted">Some clients experience issues when the video stream changes resolution. This option will make dizqueTV convert all videos to the preferred resolution selected above. Otherwise, the preferred resolution will be used as a maximum resolution for transcoding.
</small>
</div>
</div>
</div>
<br ></br>
<div class="row">
<div class="col-sm-9">
<div class="form-group row">
<div class="col-sm-4">
<input id="enableNormalizeVideoCodec" type="checkbox" ng-model="settings.normalizeVideoCodec" ng-disabled="isTranscodingNotNeeded()" ></input>
<label for="enableNormalizeVideoCodec">Normalize Video Codec</label>
</div>
<div class="col-sm-4">
<input id="enableNormalizeAudioCodec" type="checkbox" ng-model="settings.normalizeAudioCodec" ng-disabled="isTranscodingNotNeeded()" ></input>
<label for="enableNormalizeAudioCodec">Normalize Audio Codec</label>
</div>
</div>
<small class="form-text text-muted">Some clients experience issues when the stream's codecs change. Enable these so that any videos with different codecs than the ones specified above are forcefully transcoded.
</small>
</div>
</div>
<br ></br>
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<input id="enableAlignAudio" type="checkbox" ng-model="settings.normalizeAudio" ng-disabled="isTranscodingNotNeeded()" ></input>
<label for="enableAlignAudio">Normalize Audio</label>
<small class="form-text text-muted">This will force the preferred number of audio channels and sample rate, in addition it will align the lengths of the audio and video channels. This will prevent audio-related episode transition issues in many clients. Audio will always be transcoded.
</small>
</div>
</div>
</div>
<br ></br>
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<input id="disableOverlay" type="checkbox" ng-model="settings.disableChannelOverlay" ng-disabled="isTranscodingNotNeeded()" ></input>
<label for="disableOverlay">Disable Channel Watermark Globally</label>
<small class="form-text text-muted">Toggling this option will disable channel watermarks regardless of channel settings.
</small>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,101 +0,0 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">{{title}}</h5>
</div>
<div style='padding-left: 1rem; padding-right: 1rem' >
<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>
<h6 style="margin-top: 10px;">Clips</h6>
<div class="flex-container">
<div class="programming-counter small" ng-show="content.length > 0">
<span class="small"><b>Total:</b> {{content.length}}</span>
</div>
<div class='flex-pull-right' ></div>
<div>
<button class="btn btn-sm btn-secondary btn-programming-tools"
ng-click="showTools = !showTools"
ng-show="content.length !== 0">
<span
class="fa {{ showTools ? 'fa-chevron-down' : 'fa-chevron-right'}}"></span>&nbsp;&nbsp;Tools
</button>
</div>
<div>
<button class="btn btn-sm btn-primary" ng-click="showPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
</div>
</div>
<div ng-show="showTools">
<div class="row">
<div class="input-group col-md-3" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortFillers()">
<i class='fa fa-sort-amount-down-alt'></i> Sort Lengths
</button>
</div>
<div class="input-group col-md-3" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveDuplicates()">
<i class='fa fa-trash-alt'></i> Remove Duplicates
</button>
</div>
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveAllFiller()">
<i class='fa fa-trash-alt'></i> Remove All Filler
</button>
</div>
</div>
</div>
<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 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()"
dnd-drop="dropFunction(index , item)"
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"
"
dnd-effect-allowed="move"
dnd-moved="movedFunction(x.$index)"
>
<div class="program-start" >
{{durationString(x.duration)}}
</div>
<div ng-style="programSquareStyle(x, false)" ></div>
<div class="title" >
{{x.title}}
</div>
<div class="flex-pull-right">
<button class="btn btn-sm btn-link" ng-click="contentSplice(x.$index,1)">
<i class="text-danger fa fa-trash-alt" ></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Done</button>
</div>
</div>
</div>
</div>
<plex-library limit=1000000000 height="300" visible="showPlexLibrary" on-finish="importPrograms"></plex-library>
</div>

View File

@@ -1,27 +0,0 @@
<div ng-show="program">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">{{title}}</h5>
</div>
</div>
<div class="modal-body container">
<label>Duration (Seconds): <span class="text-danger pull-right">{{error.duration}}</span></label>
<input type="number" class="form-control form-control-sm" ng-model="program.durationSeconds" ng-pattern="/^([1-9][0-9]*)$/"></input>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="program = null">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(program);">Done</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,36 +0,0 @@
<div ng-show="programs != null">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Tweak TV Shows Weights</h5>
</div>
</div>
<div class="modal-body container">
<div class="row" ng-repeat="x in programs">
<div class='col-sm-5 col-md-3'>
<input style="width:100%" class='custom-range' type="range" ng-model="x.weight"
min="1" max="24" ng-change="setModified()"></input>
</div>
<div class='col-sm-7 col-md-9'>
<b ng-show='x.specialCategory'>{{x.displayName}}</b>
<span ng-show='! x.specialCategory'>{{x.displayName}}</span>
</div>
</div>
</div>
<div class="modal-footer">
<span ng-hide="modified" class="pull-right text-info small">{{message}}</span>
<button type="button" class="btn btn-sm btn-link" ng-click="programs = null" ng-show="modified">Cancel</button>
<button type="button" class="btn btn-sm btn-link" ng-click="programs = null" ng-show="!modified">Close</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(programs);" ng-show="modified" >Apply</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,28 +0,0 @@
<div>
<h5>HDHR Settings
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
Update
</button>
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
Reset Options
</button>
</h5>
<br></br>
<div class="row">
<div class="col-sm-6">
<h6>Auto-Discovery</h6>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="ad" ng-model="settings.autoDiscovery">
<label class="form-check-label" for="ad">Enable SSDP server</label>
</div>
<p class="text-center text-info">* Restart required</p>
</div>
<div class="col-sm-6">
<h6>Tuner Count
<span class="pull-right text-danger">{{error.tunerCount}}</span>
</h6>
<input type="number" class="form-control form-control-sm" ng-model="settings.tunerCount"></input>
</div>
</div>
</div>

View File

@@ -1,139 +0,0 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content" ng-if="noServers">
<div class="modal-header">
<h5 class="modal-title">Library</h5>
</div>
<div class="model-body">
<br></br>
<br></br>
<br></br>
<br></br>
<p class="text-center">Configure your Plex Server(s) in <a href="/#!/settings#plex">Settings</a></p>
<br></br>
<br></br>
<br></br>
<br></br>
<br></br>
</div>
</div>
<div class="modal-content" ng-if="!noServers">
<div class="modal-header">
<h5 class="modal-title">Library</h5>
<span class="pull-right">
<label class="small" for="displayImages">Thumbnails</label>&nbsp;
<input id="displayImages" type="checkbox" ng-model="displayImages" ></input>&nbsp;
</span>
</div>
<div class="modal-body">
<select class="form-control form-control-sm custom-select" ng-model="currentOrigin"
ng-options="x.name for x in origins" ng-change="selectOrigin(currentOrigin)"></select>
<hr ></hr>
<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>
<li class="list-group-item" ng-repeat="a in libraries">
<div class="flex-container {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
<span class="fa {{ a.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
<img ng-if="displayImages" lazy-img="{{a.icon}}" ></img>
<span>{{ displayTitle(a) }}</span><!-- Library -->
<span ng-if="a.type === 'show' || a.type === 'movie' || a.type === 'artist'" class="flex-pull-right" ng-click='$event.stopPropagation(); selectLibrary(a)'>
<span class="fa fa-plus btn"></span>
</span>
</div>
<ul ng-if="a.collapse" class="list-group">
<li class="list-group-item {{ b.type !== 'movie' ? 'list-group-item-secondary' : 'list-group-item-video' }}"
ng-repeat="b in a.nested">
<div class="flex-container"
ng-click="b.type !== 'movie' ? getNested(b) : selectItem(b, true)">
<span ng-if="b.type === 'movie'" class="fa fa-plus-circle tab"></span>
<span ng-if="b.type !== 'movie'" class="tab"></span>
<span ng-if="b.type !== 'movie'" class="fa {{ b.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
<img ng-if="displayImages" lazy-img="{{ b.type === 'episode' ? b.episodeIcon : b.icon }}" ></img>
{{ displayTitle(b) }}
<span ng-if="b.type === 'movie'" class="flex-pull-right">
{{b.durationStr}}
</span>
<span ng-if="b.type === 'playlist'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectPlaylist(b);">
<span class="fa fa-plus btn"></span>
</span>
<span ng-if="b.type === 'show' || b.type === 'collection' || b.type === 'genre' || b.type === 'artist'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectShow(b);">
<span class="fa fa-plus btn"></span>
</span>
</div>
<ul ng-if="b.collapse" class="list-group">
<li ng-repeat="c in b.nested"
class="list-group-item {{ c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track' ? 'list-group-item-dark' : 'list-group-item-video' }}">
<div class="flex-container"
ng-click="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track' ? getNested(c) : selectItem(c, true)">
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track'"
class="fa fa-plus-circle tab"></span>
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
class="tab"></span>
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
class="tab"></span>
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
class="fa {{ c.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
<img ng-if="displayImages" lazy-img="{{c.type === 'episode' ? c.episodeIcon : c.icon }}" ></img>
{{ displayTitle(c) }}
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track' "
class="flex-pull-right">
{{c.durationStr}}
</span>
<span ng-if="c.type === 'season' || c.type === 'album'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectSeason(c);">
<span class="fa fa-plus btn"></span>
</span>
</div>
<ul ng-if="c.collapse" class="list-group">
<li class="list-group-item list-group-item-video"
ng-repeat="d in c.nested">
<div class="flex-container" ng-click="selectItem(d, true)">
<span class="fa fa-plus-circle tab"></span>
<img ng-if="displayImages" lazy-img="{{d.episodeIcon}}" ></img>
{{ displayTitle(d) }}
<span class="flex-pull-right">{{d.durationStr}}</span>
<!-- Episode -->
</div>
</li>
</ul>
</li>
</ul>
</li>
</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>
<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>
<span>{{x.name}} ({{x.count}})</span>
</div>
</li>
</ul>
<hr></hr>
<div class="loader" ng-if="pending &gt; 0" ></div> <h6 style='display:inline-block'>Selected Items</h6>
<div class="text-info small" ng-show='selection.length &gt; 10'>{{ selection.length }} elements added in total. Only the last 10 elements are displayed:</div>
<div class="text-danger small" ng-repeat="e in errors track by $index">{{ e }}</div>
<ul class="list-group list-group-root" style="height: 180px; overflow-y: scroll" dnd-list="selection" scroll-glue>
<div ng-if="selection.length === 0">Select media items from your plex library above.</div>
<li ng-if="selection.length + x &gt;= 0" class="list-group-item" ng-repeat="x in allowedIndexes" style="cursor:default;" dnd-draggable="x" dnd-moved="selection.splice(selection.length + x, 1)" dnd-effect-allowed="move">
{{ getProgramDisplayTitle(selection[selection.length + x]) }}
<button class="pull-right btn btn-sm btn-link" ng-click="selection.splice(selection.length + x,1)">
<span class="text-danger fa fa-trash-alt" ></span>
</button>
</li>
</ul>
</div>
<div class='text-danger'>{{error}}</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="_onFinish([])">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="_onFinish(selection);">Done</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,99 +0,0 @@
<div ng-show="state.visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Plex Server</h5>
</div>
</div>
<div class="modal-body container" >
<div ng-if="state.channelReport == null" class="form-group">
<label for="serverName">Name:</label>
<input class="form-control" type="text" placeholder="{{state.server.name}}" readonly></input>
</div>
<div ng-if="state.channelReport == null" class="form-group">
<label for="uri">Server URI:</label>
<input type="text" class="form-control" id="uri" ng-model = "state.server.uri" ng-change="setModified()"></input>
</div>
<div ng-if="state.channelReport == null" class="form-group">
<label ng-if="!state.accessVisible" for="accessToken">User Access Token</label>
<label ng-if="state.accessVisible" for="accessToken2">User Access Token (Do not share this token with strangers)</label>
<div class="input-group">
<input ng-if="!state.accessVisible" type="password" class="form-control" id="accessToken" ng-model = "state.server.accessToken" ng-change="setModified()"></input>
<input ng-if="state.accessVisible" type="text" class="form-control" id="accessToken2" ng-model = "state.server.accessToken" ng-change="setModified()" aria-describedby="tokenHelp"></input>
<div class="input-group-append">
<button class="btn btn-secondary form-control form-control-sm" type="button" ng-click="state.accessVisible = ! state.accessVisible">
<i ng-hide='state.accessVisible' class='fa fa-eye'></i>
<i ng-show='state.accessVisible' class='fa fa-asterisk'></i>
</button>
</div>
</div>
</div>
<div ng-if="state.channelReport == null" class="form-check">
<input class="form-check-input" type="checkbox" value="" id="arGuide" ng-model="state.server.arGuide" ng-change="setModified()">
<label class="form-check-label" for="arGuide">
Send Guide Updates
</label>
</div>
<div ng-if="state.channelReport == null" class="form-check">
<input class="form-check-input" type="checkbox" value="" id="arChannels" ng-model="state.server.arChannels" ng-change="setModified()">
<label class="form-check-label" for="arChannels">
Send Channel Updates
</label>
</div>
<hr ng-if="state.channelReport == null" ng-hide="state.showDelete"></ht>
<div ng-if="state.channelReport == null" ng-hide="state.showDelete" class="form-group">
<button class="btn btn-link" ng-click="onShowDelete()">
<span class="text-danger"><i class="fa fa-trash-alt"></i> Delete server...</span>
</button>
</div>
<div ng-if="state.channelReport == null" ng-show="state.showDelete" class="card" style="width: 100%;">
<div class="card-body">
<h5 class="card-title">
Delete Server
</h5>
<p class="card-text">If you delete a plex server, all the existing programs that reference to it will be
replaced with Flex time. Fillers that reference to the server will be removed. This operation cannot be undone.</p>
</div>
<button ng-if="state.channelReport == null" type="button" class="btn btn-sm btn-danger" ng-click="onDelete();" ><i class='fa fa-trash-alt'></i> Delete</button>
</div>
<div ng-if="state.channelReport != null" class="card" style="width: 100%;">
<div class="card-body">
<h5 class="card-title">Server deleted</h5>
<table class='table'>
<tr ng-repeat="x in state.channelReport" ng-if="x.destroyedPrograms &gt; 0">
<td>{{x.channelNumber}}</td>
<td>{{x.channelName}}</td>
<td>{{x.destroyedPrograms}} program{{ (x.destroyedPrograms&gt;1?&quot;s&quot;:&quot;&quot;) }} removed.</td>
</tr>
</table>
</div>
</div>
</div>
<div class="modal-footer" ng-show='! loading.show'>
<div class='text-success small'>{{state.success}}</div>
<div class='text-danger small'>{{state.error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="onFinish()">{{state.modified?"Cancel":"Close"}}</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="onSave();" ng-show="state.modified" >Save</button>
</div>
<div class="modal-footer" ng-show='loading.show'>
<div class='loader'></div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,201 +0,0 @@
<div>
<h5>Plex Settings</h5>
<h6>Plex Servers
<button class="pull-right btn btn-sm btn-success" style="margin-bottom:10px;" ng-disabled="isProcessing" ng-click="addPlexServer()">
Sign In/Add Servers
</button>
</h6>
<div ng-if="isProcessing">
<br>
<h6>
<span class="pull-right text-info">{{ isProcessing ? 'You have 2 minutes to sign into your Plex Account.' : ''}}</span>
</h6>
<br>
</div>
<table class="table">
<tr>
<th>Name</th>
<th>uri</th>
<th>UI Route</th>
<th>Backend Route</th>
<th></th>
</tr>
<tr ng-if="servers.length === 0">
<td colspan="7">
<p class="text-center text-danger">Add a Plex Server</p>
</td>
</tr>
<tr ng-if="serversPending">
<td><div class="loader"></div> <span class='text-info'>{{ addingServer }}</span></td>
</tr>
<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>ok</div>
<div class='text-danger' ng-if="x.uiStatus == -1"><i class='fa fa-exclamation-triangle'></i>error</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>ok</div>
<div class='text-danger' ng-if="x.backendStatus == -1"><i class='fa fa-exclamation-triangle'></i>error</div>
</td>
<td>
<button class="btn btn-sm btn-outline-secondary" ng-click="editPlexServer(x)">
<span class="fa fa-edit"></span>
</button>
</td>
<tr ng-if="serverError.length &gt; 0">
<td colspan="5">
<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">If a Plex server configuration has problems with the UI route, the channel editor won&apos;t be able to access its content.</p>
</td>
</tr>
<tr ng-if="isAnyBackendBad()">
<td colspan="5">
<p class="text-center text-danger small">If a Plex server configuration has problems with the backend route, dizqueTV won&apos;t be able to play its content.</p>
</td>
</tr>
</table>
<hr>
<h6>Plex Transcoder Settings
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
Update
</button>
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
Reset Options
</button>
</h6>
<hr>
<div class="row" >
<div class="col-sm-3">
<div class="form-group">
<input id="debugLogging" type="checkbox" ng-model="settings.debugLogging"></input>
<label for="debugLogging">Debug logging</label>
</div>
<div class="form-group">
<label>Paths</label>
<select ng-model="settings.streamPath"
ng-options="o.id as o.description for o in pathOptions" ></select>
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<input id="updatePlayStatus" type="checkbox" ng-model="settings.updatePlayStatus" ria-describedby="updatePlayStatusHelp"></input>
<label for="updatePlayStatus">Send play status to Plex</label>
<small id="updatePlayStatusHelp" class="form-text text-muted">Note: This affects the "on deck" for your plex account.</small>
</div>
</div>
</div>
<div class="row" ng-hide="hideIfNotPlexPath()">
<div class="col-sm-12">
<p class="text-center text-info small">If stream changes video codec, audio codec, or audio channels upon episode change, you will experience playback issues unless ffmpeg transcoding and normalization are also enabled.</p>
</div>
</div>
<div class="row" ng-hide="hideIfNotPlexPath()">
<div class="col-sm-6">
<h6 style="font-weight: bold">Video Options</h6>
<div class="form-group">
<label>Supported Video Formats</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.videoCodecs" ria-describedby="videoCodecsHelp"></input>
</div>
<div class="form-group">
<label>Max Playable Resolution</label>
<select ng-model="maxPlayableResString"
ng-options="o.id as o.description for o in resolutionOptions" ></select>
</div>
<div class="form-group">
<label>Max Transcode Resolution</label>
<select ng-model="maxTranscodeResString"
ng-options="o.id as o.description for o in resolutionOptions "></select>
</div>
</div>
<div class="col-sm-6">
<h6 style="font-weight: bold">Audio Options</h6>
<div class="form-group">
<label>Supported Audio Formats</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.audioCodecs" ria-describedby="audioCodecsHelp" ></input>
<small id="audioCodecsHelp" class="form-text text-muted">Comma separated list. Some possible values are 'ac3,aac,mp3'.</small>
</div>
<div class="form-group">
<label>Maximum Audio Channels</label>
<select ng-model="settings.maxAudioChannels"
ng-options="o.id as o.description for o in maxAudioChannelsOptions" ria-describedby="maxAudioChannelsHelp"></select>
<small id="maxAudioChannelsHelp" class="form-text text-muted">Note: 7.1 audio and on some clients, 6.1, is known to cause playback issues.</small>
</div>
<div class="form-group">
<label>Audio Boost</label>
<select ng-model="settings.audioBoost"
ng-options="o.id as o.description for o in audioBoostOptions" ria-describedby="audioBoostHelp"></select>
<small id="audioBoostHelp" class="form-text text-muted">Note: Only applies when downmixing to stereo.</small>
</div>
</div>
</div>
<div class="row" ng-hide="hideIfNotPlexPath()">
<div class="col-sm-6">
<h6 style="font-weight: bold">Miscellaneous Options</h6>
<div class="form-group">
<label>Max Direct Stream Bitrate (Kbps)</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.directStreamBitrate" ></input>
</div>
<div class="form-group">
<label>Max Transcode Bitrate (Kbps)</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeBitrate" aria-described-by="transcodebrhelp" ></input>
<small id="transcodebrhelp" class='text-muted form-text'>Plex will decide to transcode or direct play based on these settings and if Plex transcodes, it will try to match the transcode bitrate.</small>
</div>
<div class="form-group">
<label>Direct Stream Media Buffer Size</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.mediaBufferSize" ></input>
</div>
<div class="form-group">
<label>Transcode Media Buffer Size</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeMediaBufferSize" ></input>
</div>
<div class="form-group">
<label>Stream Protocol</label>
<select ng-model="settings.streamProtocol"
ng-options="o.id as o.description for o in streamProtocols" ></select>
</div>
<div class="form-group">
<input id="forceDirectPlay" type="checkbox" ng-model="settings.forceDirectPlay" ></input>
<label for="forceDirectPlay">Force Direct Play</label>
</div>
</div>
<div class="col-sm-6">
<h6 style="font-weight: bold">Subtitle Options</h6>
<div class="form-group">
<label>Subtitle Size</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.subtitleSize" ></input>
</div>
<div class="form-group">
<input class="form-check-input" id="enableSubtitles" type="checkbox" ng-model="settings.enableSubtitles" ng-disabled="shouldDisableSubtitles()" ></input>
<label class="form-check-label" for="enableSubtitles">Enable Subtitles (Requires Transcoding)</label>
</div>
</div>
</div>
<div class="row" ng-hide="hideIfNotDirectPath()">
<div class="col-sm-6">
<h6 style="font-weight: bold">Path Replacements</h6>
<div class="form-group">
<label>Original Plex path to replace:</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplace" ></input>
</div>
<div class="form-group">
<label>Replace Plex path with:</label>
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplaceWith" ></input>
</div>
</div>
</div>
<plex-server-edit state="_serverEditorState" on-finish="serverEditFinished"></plex-server-edit>

View File

@@ -1,107 +0,0 @@
<div ng-show="program">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Program Config (EPG)</h5>
</div>
</div>
<div class="modal-body container">
<select ng-model="program.type" class="pull-right">
<option>movie</option>
<option>episode</option>
<option>track</option>
</select>
<div ng-if="program.type === 'track'">
<label>Track Title
<span class="text-danger pull-right">{{error.title}}</span>
</label>
<input class="form-control form-control-sm" type="text" ng-model="program.title"></input>
<label>Subtitle</label>
<input class="form-control form-control-sm" type="text" ng-model="program.subtitle"></input>
<label>Summary</label>
<textarea class="form-control form-control-sm" ng-model="program.summary"></textarea>
<label>Rating</label>
<input class="form-control form-control-sm" type="text" ng-model="program.rating"></input>
<label>Icon</label>
<input class="form-control form-control-sm" type="text" ng-model="program.icon"></input>
<h6>Icon Preview</h6>
<div class="text-center">
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
</div>
</div>
<div ng-if="program.type === 'movie'">
<label>Movie Title
<span class="text-danger pull-right">{{error.title}}</span>
</label>
<input class="form-control form-control-sm" type="text" ng-model="program.title"></input>
<label>Subtitle</label>
<input class="form-control form-control-sm" type="text" ng-model="program.subtitle"></input>
<label>Summary</label>
<textarea class="form-control form-control-sm" ng-model="program.summary"></textarea>
<label>Rating</label>
<input class="form-control form-control-sm" type="text" ng-model="program.rating"></input>
<label>Icon</label>
<input class="form-control form-control-sm" type="text" ng-model="program.icon"></input>
<h6>Icon Preview</h6>
<div class="text-center">
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
</div>
</div>
<div ng-if="program.type === 'episode'">
<label>Show Title
<span class="text-danger pull-right">{{error.showTitle}}</span>
</label>
<input class="form-control form-control-sm" type="text" ng-model="program.showTitle"></input>
<label>Episode Title
<span class="text-danger pull-right">{{error.title}}</span>
</label>
<input class="form-control form-control-sm" type="text" ng-model="program.title"></input>
<label>Season
<span class="text-danger pull-right">{{error.season}}</span>
</label>
<input class="form-control form-control-sm" type="number" ng-model="program.season"></input>
<label>Episode
<span class="text-danger pull-right">{{error.episode}}</span>
</label>
<input class="form-control form-control-sm" type="number" ng-model="program.episode"></input>
<label>Summary</label>
<textarea class="form-control form-control-sm" ng-model="program.summary"></textarea>
<label>Rating</label>
<input class="form-control form-control-sm" type="text" ng-model="program.rating"></input>
<label>Icon</label>
<input class="form-control form-control-sm" type="text" ng-model="program.icon"></input>
<h6>Icon Preview</h6>
<div class="row">
<div class="col-sm-6">
<div class="text-center">
<img class="img" ng-src="{{program.icon}}" style="max-width: 200px;"></img>
</div>
</div>
<div class="col-sm-6 row" ng-if="program.showIcon">
<div class="col-sm-6 text-center">
<label>Show</label>
<img class="img" ng-src="{{program.showIcon}}" style="max-width: 75px; cursor: pointer;" ng-click="program.icon = program.showIcon"></img>
</div>
<div class="col-sm-6 text-center">
<label>Season</label>
<img class="img" ng-src="{{program.seasonIcon}}" style="max-width: 75px; cursor: pointer;" ng-click="program.icon = program.seasonIcon"></img>
</div>
<div class="col-sm-12 text-center">
<label>Episode</label>
<img class="img" ng-src="{{program.episodeIcon}}" style="max-width: 150px; cursor: pointer;" ng-click="program.icon = program.episodeIcon"></img>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="program = null">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(program);">Done</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,185 +0,0 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Random Slots</h5>
</div>
</div>
<div class="modal-body" ng-show='loading' >
<p><span class='loader'></span> Generating lineup, please wait...</p>
</div>
<div class="modal-body" ng-show='! loading' >
<div class="form-row" ng-repeat = "slot in schedule.slots" track-by = "$index">
<div class='form-group col-md-2' >
<label ng-if="$index==0" for="showTime{{$index}}">Duration</label>
<select
id="showDuration{{$index}}" class="custom-select form-control"
ng-model="slot.duration" ng-options="o.id as o.description for o in durationOptions"
ng-change="refreshSlots()"
>
</select>
<small class='form-text text-danger'>{{slot.timeError}}</small>
</div>
<div class='form-group col-md-5' >
<label ng-if="$index==0" for="showId{{$index}}">Program</label>
<select
id="showId{{$index}}" class="custom-select form-control"
ng-model="slot.showId" ng-options="o.id as o.description for o in showOptions"
ng-change="refreshSlots()"
>
</select>
</div>
<div class='form-group col-md-2'>
<label ng-if="$index==0" for="showCooldown{{$index}}" >Cooldown</label>
<select
id="showCooldown{{$index}}" class="custom-select form-control"
ng-model="slot.cooldown" ng-options="o.id as o.description for o in cooldownOptions"
ng-change="refreshSlots()"
>
</select>
</div>
<div class='form-group col-md-2'>
<label ng-if="$index==0" for="showOrder{{$index}}" ng-show="canShowSlot(slot)" >Order</label>
<select
id="showOrder{{$index}}" class="custom-select form-control"
ng-model="slot.order" ng-options="o.id as o.description for o in orderOptions"
ng-change="refreshSlots()"
ng-show="canShowSlot(slot)"
ng-disabled="slot.showId == 'movie.'"
>
</select>
</div>
<div class='form-group col-md-1'>
<label ng-if="$index==0" for="delete{{$index}}">-</label>
<button id='delete{{$index}}' class='btn btn-link form-control' ng-click='deleteSlot($index)' >
<i class='text-danger fa fa-trash-alt'></i>
</button>
</div>
<div class='form-group col-md-2'>
</div>
<div class='form-group col-md-7' ng-if="schedule.randomDistribution == 'weighted'" >
<label ng-if="$index==0" for="weightRange{{$index}}">Weight</label>
<input class='form-control-range custom-range' id="weightRange{{$index}}" type='range' ng-model='slot.weight' min=1 max=600
data-toggle="tooltip" data-placement="bottom" title="Slots with more weight will be picked more frequently."
ng-change="refreshSlots()"
>
</input>
</div>
<div class='form-group col-md-2' ng-if="schedule.randomDistribution == 'weighted'" >
<label ng-if="$index==0" for="weightp{{$index}}">%</label>
<input class='form-control flex-filler-percent' id="weightp{{$index}}" type='text' ng-model='slot.weightPercentage'
data-toggle="tooltip" data-placement="bottom" title="This is the overall probability this slot might be picked, assuming all lists are available." readonly
>
</input>
</div>
</div>
<div class="form-row">
<div class='form-group col-md-2'>
<label ng-if="schedule.slots.length==0" for="fakeTime">Duration</label>
<button
type="button"
class="btn btn-outline-secondary form-control"
ng-click="addSlot()"
>
Add Slot
</button>
</div>
</div>
<hr>
<div class='form-group'>
<label for="pad">Pad times</label>
<select
id="pad" class="custom-select form-control"
ng-model="schedule.pad" ng-options="o.id as o.description for o in padOptions"
aria-describedby="padHelp"
>
</select>
<small id='padHelp' class='form-text text-muted'>
Ensures programs have a nice-looking start time, it will add Flex time to fill the gaps.
</small>
</div>
<div class='form-group' ng-show='schedule.pad != 1'>
<label for="padStyle">What to pad?</label>
<select
id="padStyle" class="custom-select form-control"
ng-model="schedule.padStyle" ng-options="o.id as o.description for o in padStyleOptions"
aria-describedby="padStyleHelp"
>
</select>
<small id='padStyleHelp' class='form-text text-muted'>
When using the pad times option, you might prefer to only ensure the start times of the slot and not the individual episodes.
</small>
</div>
<div class='form-group'>
<label for="flexPreference">What to do with flex?</label>
<select
id="flexPreference" class="custom-select form-control"
ng-model="schedule.flexPreference" ng-options="o.id as o.description for o in flexOptions"
aria-describedby="flexPreferenceHelp"
>
</select>
<small id='flexPreferenceHelp' class='form-text text-muted'>
Usually slots need to add flex time to ensure that the next slot starts at the correct time. When there are multiple videos in the slot, you might prefer to distribute the flex time between the videos or to place most of the flex time at the end of the slot.
</small>
</div>
<div class='form-group'>
<label for="randomDistribution">Random Distribution</label>
<select
id="randomDistribution" class="custom-select form-control"
ng-model="schedule.randomDistribution" ng-options="o.id as o.description for o in distributionOptions"
aria-describedby="randomDistributiondHelp"
ng-change="randomDistributionChanged()"
>
</select>
<small id='randomDistributionHelp' class='form-text text-muted'>
Uniform means that all slots have an equal chancel to be picked. Weighted makes the configuration of the slots more complicated but allows to tweak the weight for each slot so you can make some slots more likely to be picked than others.
</small>
</div>
<div class='form-group'>
<label for="lateness">Maximum days to precalculate</label>
<input
id="maxDays" class="form-control"
type='number'
ng-model="schedule.maxDays"
min = 1
max = 3652
aria-describedby="maxDaysHelp"
required
>
</input>
<small id="maxDaysHelp" class='form-text text-muted'>
Maximum number of days to precalculate the schedule. Note that the length of the schedule is also bounded by the maximum number of programs allowed in a channel.
</small>
</div>
</div>
<div class="modal-footer" ng-show='!loading'>
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,38 +0,0 @@
<div ng-show="programInfos.length > 0">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Remove TV Show(s)</h5>
</div>
</div>
<div class="modal-body container">
<div class="list-group list-group-root">
<div class="list-group-item flex-container program-row" ng-repeat="program in programInfos" ng-click="toggleShowDeletion(program.id)">
<div class='col-sm-7 col-md-9'>
<span ng-show='deleted.indexOf(program.id) === -1'>{{program.displayName}}</span>
<span class="text-muted" ng-show='deleted.indexOf(program.id) > -1'><strike>{{program.displayName}}</strike></span>
</div>
<div class="flex-pull-right"></div>
<div class='col-sm-1 col-md-1'>
<button class="btn btn-sm btn-link">
<i ng-show="deleted.indexOf(program.id) === -1" class="text-danger fa fa-trash-alt"></i>
<i ng-show="deleted.indexOf(program.id) > -1" class="text-success fa fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="programInfos = null" ng-show="deleted.length > 0">Cancel</button>
<button type="button" class="btn btn-sm btn-link" ng-click="programInfos = null" ng-show="deleted.length === 0">Close</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(programInfos);" ng-show="deleted.length > 0" >Apply</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,121 +0,0 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">{{title}}</h5>
</div>
<div style='padding-left: 1rem; padding-right: 1rem' >
<div class="form-group">
<label for="name">Show Name:</label>
<input type="text" class="form-control" id="name" placeholder="Show Name" ng-model="name" ></input>
</div>
<h6 style="margin-top: 10px;">Clips</h6>
<div class="flex-container">
<div class="programming-counter small" ng-show="content.length > 0">
<span class="small"><b>Total:</b> {{content.length}}</span>
</div>
<div class='flex-pull-right' ></div>
<div>
<button class="btn btn-sm btn-secondary btn-programming-tools"
ng-click="showTools = !showTools"
ng-show="content.length !== 0">
<span
class="fa {{ showTools ? 'fa-chevron-down' : 'fa-chevron-right'}}"></span>&nbsp;&nbsp;Tools
</button>
</div>
<div>
<button class="btn btn-sm btn-primary" ng-click="showPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
</div>
</div>
<div ng-show="showTools" class='tools-pane' >
<div class="row">
<!-- TODO: Probably sort shows and sort dates are needed here -->
<div class="input-group col-xl-3 col-lg-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortShows()">
<i class='fa fa-sort-alpha-down'></i> Sort TV Shows
</button>
</div>
<div class="input-group col-xl-3 col-lg-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()">
<i class='fa fa-sort-alpha-down'></i> Sort Release Dates
</button>
</div>
<div class="input-group col-xl-6 col-lg-12" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="shuffleShows()">
<i class='fa fa fa-random'></i> Random Shuffle
</button>
</div>
<div class="input-group col-xl-3 col-lg-6" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="showRemoveDuplicates()">
<i class='fa fa-trash-alt'></i> Remove Duplicates
</button>
</div>
<div class="input-group col-xl-3 col-lg-6" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeSpecials()">
<i class='fa fa-trash-alt'></i> Remove Specials
</button>
</div>
<div class="input-group col-xl-6 col-lg-12" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="showRemoveAllShow()">
<i class='fa fa-trash-alt'></i> Remove All
</button>
</div>
</div>
</div>
<div ng-show="content.length === 0">
<p class="text-center text-info">Click the <span class="fa fa-plus"></span> to import show content from your Plex server(s).</p>
</div>
</div>
</div>
<div vs-repeat class="modal-body container list-group list-group-root show-list"
dnd-list="content" ng-if="showList()"
vs-repeat-reinitialized="vsReinitialized(event, startIndex, endIndex)"
ng-init="setUpWatcher()"
dnd-drop="dropFunction(index , item)"
dnd-list=""
>
<div class="list-group-item flex-container show-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)"
>
<div class="program-start" >
X{{ (x.$index + 1).toString().padStart(2, '0') }}
</div>
<div ng-style="programSquareStyle(x, false)" ></div>
<div class="title" >
{{ getProgramDisplayTitle(x) }}
</div>
<div class="flex-pull-right">
<button class="btn btn-sm btn-link" ng-click="contentSplice(x.$index,1)">
<i class="text-danger fa fa-trash-alt" ></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Done</button>
</div>
</div>
</div>
</div>
<plex-library limit=1000000000 height="300" visible="showPlexLibrary" on-finish="importPrograms"></plex-library>
</div>

View File

@@ -1,160 +0,0 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Time Slots</h5>
</div>
</div>
<div class="modal-body" ng-show='loading' >
<p><span class='loader'></span> Generating lineup, please wait...</p>
</div>
<div class="modal-body" ng-show='! loading' >
<div class="form-row" ng-repeat = "slot in schedule.slots" track-by = "$index">
<div class='form-group' ng-class='timeColumnClass()' >
<label ng-if="$index==0" for="showTime{{$index}}">Time</label>
<button
type="button"
class="btn btn-outline-secondary form-control"
ng-click="editTime($index)"
>
{{ displayTime(slot.time) }}
</button>
<small class='form-text text-danger'>{{slot.timeError}}</small>
</div>
<div class='form-group' ng-class='programColumnClass()' >
<label ng-if="$index==0" for="showId{{$index}}">Program</label>
<select
id="showId{{$index}}" class="custom-select form-control"
ng-model="slot.showId" ng-options="o.id as o.description for o in showOptions"
ng-change="refreshSlots()"
>
</select>
</div>
<div class='form-group col-md-2'>
<label ng-if="$index==0" for="showOrder{{$index}}" ng-show="canShowSlot(slot)" >Order</label>
<select
id="showOrder{{$index}}" class="custom-select form-control"
ng-model="slot.order" ng-options="o.id as o.description for o in orderOptions"
ng-change="refreshSlots()"
ng-show="canShowSlot(slot)"
ng-disabled="slot.showId == 'movie.'"
>
</select>
</div>
<div class='form-group col-md-1'>
<label ng-if="$index==0" for="delete{{$index}}">-</label>
<button id='delete{{$index}}' class='btn btn-link form-control' ng-click='deleteSlot($index)' >
<i class='text-danger fa fa-trash-alt'></i>
</button>
</div>
</div>
<div class="form-row">
<div class='form-group col-md-2' ng-class='timeColumnClass()'>
<label ng-if="schedule.slots.length==0" for="fakeTime">Time</label>
<button
type="button"
class="btn btn-outline-secondary form-control"
ng-click="addSlot()"
>
Add Slot
</button>
</div>
</div>
<hr>
<div class='form-group'>
<label for="period">Period</label>
<select
id="period" class="custom-select form-control"
ng-model="schedule.period" ng-options="o.id as o.description for o in periodOptions"
ng-change="periodChanged()"
aria-describedby="periodHelp"
>
</select>
<small id='periodHelp' class='form-text text-muted'>
By default, time slots are time of the day-based, you can change it to time of the day + day of the week. That means scheduling 7x the number of time slots. If you change from daily to weekly, the current schedule will be repeated 7 times. If you change from weekly to daily, many of the slots will be deleted.
</small>
</div>
<div class='form-group'>
<label for="lateness">Maximum lateness</label>
<select
id="lateness" class="custom-select form-control"
ng-model="schedule.lateness" ng-options="o.id as o.description for o in latenessOptions"
aria-describedby="latenessHelp"
>
</select>
<small id='latenessHelp' class='form-text text-muted'>
Allows programs to play a bit late if the previous program took longer than usual. If a program is too late, Flex is scheduled instead.
</small>
</div>
<div class='form-group'>
<label for="pad">Pad times</label>
<select
id="pad" class="custom-select form-control"
ng-model="schedule.pad" ng-options="o.id as o.description for o in padOptions"
aria-describedby="padHelp"
>
</select>
<small id='padHelp' class='form-text text-muted'>
Ensures programs have a nice-looking start time, it will add Flex time to fill the gaps.
</small>
</div>
<div class='form-group'>
<label for="pad">What to do with flex?</label>
<select
id="flexPreference" class="custom-select form-control"
ng-model="schedule.flexPreference" ng-options="o.id as o.description for o in flexOptions"
aria-describedby="flexPreferenceHelp"
>
</select>
<small id='flexPreferenceHelp' class='form-text text-muted'>
Usually slots need to add flex time to ensure that the next slot starts at the correct time. When there are multiple videos in the slot, you might prefer to distribute the flex time between the videos or to place most of the flex time at the end of the slot.
</small>
</div>
<div class='form-group'>
<label for="lateness">Maximum days to precalculate</label>
<input
id="maxDays" class="form-control"
type='number'
ng-model="schedule.maxDays"
min = 1
max = 3652
aria-describedby="maxDaysHelp"
required
>
</input>
<small id="maxDaysHelp" class='form-text text-muted'>
Maximum number of days to precalculate the schedule. Note that the length of the schedule is also bounded by the maximum number of programs allowed in a channel.
</small>
</div>
</div>
<div class="modal-footer" ng-show='!loading'>
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
</div>
</div>
</div>
</div>
<time-slots-time-editor slot="_editedTime" on-done="finishedTimeEdit"></time-slots-time-editor>
<time-slots-time-editor slot="_addedTime" on-done="finishedAddingTime"></time-slots-time-editor>
</div>

View File

@@ -1,70 +0,0 @@
<div ng-show="slot">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">{{slot.title}}</h5>
</div>
</div>
<div class="modal-body container">
<div class="form-row" ng-show='slot.isWeekly' >
<div class='form-group col-sm-auto'>
<label for="hour">Day</label>
<select
id="w" class="custom-select form-control"
ng-model="slot.w" ng-options="o.id as o.description for o in weekDayOptions"
>
</select>
</div>
</div>
<div class="form-row">
<div class='form-group col-sm-auto'>
<label for="hour">Hour</label>
<select
id="hour" class="custom-select form-control"
ng-model="slot.h" ng-options="o.id as o.description for o in hourOptions"
>
</select>
</div>
<div class='form-group col-sm-auto'>
<label for="hour">Minutes</label>
<select
id="hour" class="custom-select form-control"
ng-model="slot.m" ng-options="o.id as o.description for o in minuteOptions"
>
</select>
</div>
<div class='form-group col-sm-auto'>
<label for="hour">Seconds</label>
<select
id="hour" class="custom-select form-control"
ng-model="slot.s" ng-options="o.id as o.description for o in minuteOptions"
>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="slot = null">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(slot);">Done</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,23 +0,0 @@
<div style='position: fixed; top: 30px; right: 30px; width:400px; z-index: 1000000;'>
<div
ng-repeat="toast in toasts track by $index"
class="dizque-toast"
ng-class="toast.clazz"
ng-click="destroy($index)"
>
<div
class="flex-container"
>
<div>
<strong>{{ toast.title }}</strong>
</div>
<div class='flex-pull-right'>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div>{{ toast.text }}</div>
</div>
</div>

View File

@@ -1,36 +0,0 @@
<div>
<h5>XMLTV Settings
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
Update
</button>
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
Reset Options
</button>
</h5>
<h6>Output Path</h6>
<input type="text" class="form-control form-control-sm" ng-model="settings.outputPath" aria-describedby="pathhelp" readonly ></input>
<small id="pathhelp" class="form-text text-muted">You can edit this location in file xmltv-settings.json.</small>
<br></br>
<div class="row">
<div class="col-sm-6">
<label>EPG Hours</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.programmingHours" aria-describedby="cachehelp"></input>
<small id="cachehelp" class="form-text text-muted">How many hours of programming to include in the xmltv file.</small>
</div>
<div class="col-sm-6">
<label>Refresh Timer (hours)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.refreshHours" aria-describedby="timerhelp"></input>
<small id="timerhelp" class="form-text text-muted">How often should the xmltv file be updated.</small>
</div>
</div>
<br ></br>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="imageCache" aria-describedby="imageCacheHelp" ng-model='settings.enableImageCache'>
<label class="form-check-label" for="stealth">Image Cache</label>
<div class='text-muted' id="imageCacheHelp">If enabled the pictures used for Movie and TV Show posters will be cached in dizqueTV's .dizqueTV folder and will be delivered by dizqueTV's server instead of requiring calls to Plex. Note that using fixed xmltv location in Plex (as opposed to url) will not work correctly in this case.</div>
</div>
</div>

View File

@@ -1,40 +0,0 @@
<div class='container'>
<channel-config ng-if="showChannelConfig" channel="selectedChannel" channels="channels" on-done="onChannelConfigDone"></channel-config>
<h5>
Channels
<button class="pull-right btn btn-sm btn-primary" ng-click="selectChannel(-1)">
<span class="fa fa-plus"></span>
</button>
</h5>
<table class="table">
<tr>
<th width="40">Number</th>
<th width="120" class='text-center'>Icon</th>
<th>Name</th>
<th></th>
</tr>
<tr ng-if="channels.length === 0">
<td colspan="3">
<p class="text-center text-danger">No channels found. Click the <span class="fa fa-plus"></span> to create a channel.</p>
</td>
</tr>
<tr ng-repeat="x in channels" ng-click="selectChannel($index)" style="cursor: pointer; height: 3em" ng-class="{'stealth-channel':(x.stealth===true), 'channel-row' : true }" >
<td style='height: 3em'>
<div class="loader" ng-if="x.pending"></div>
<span ng-show="!x.pending">{{x.number}}</span>
</td>
<td style="padding: 0" class="text-center">
<img ng-if="x.icon !== ''" ng-src="{{x.icon.path}}" alt="{{x.name}}" style="max-height: 40px;"></img>
<div ng-if="x.icon === ''" style="padding-top: 14px;"><small>{{x.name}}</small></div>
</td>
<td>{{x.name}} <span ng-if='x.stealth===true' class='text-muted'>(Stealth)</span></td>
<td class="text-right">
<button ng-show="!x.pending" class="btn btn-sm btn-link" ng-click="removeChannel($index, x); $event.stopPropagation()">
<span class="text-danger fa fa-trash-alt"></span>
</button>
</td>
</tr>
</table>
</div>

View File

@@ -1,37 +0,0 @@
<div class='container'>
<show-config linker="registerShowConfig" on-done="onShowConfigDone"></show-config>
<delete-show linker="registerDeleteShow" on-exit="onShowDelete"></delete-show>
<h5>
Custom Shows
<button class="pull-right btn btn-sm btn-primary" ng-click="selectShow(-1)">
<span class="fa fa-plus"></span>
</button>
</h5>
<table class="table">
<tr>
<th>Name</th>
<th width="40">Clips</th>
<th style='width:2em'></th>
</tr>
<tr ng-if="shows.length === 0">
<td colspan="3">
<p class="text-center text-danger">No Custom Shows set. Click the <span class="fa fa-plus"></span> to add custom shows.</p>
</td>
</tr>
<tr class='show-row' ng-repeat="x in shows" ng-click="selectShow($index)" style="cursor: pointer; height: 3em" >
<td style='height: 3em'>
<div class="loader" ng-if="x.pending"></div>
<span ng-show="!x.pending">{{x.name}}</span>
</td>
<td>{{x.count}}</td>
<td>
<button class='btn btn-link' title='Delete...' ng-click='deleteShow($index)' >
<i class='fas fa-trash-alt text-danger'></i>
</button>
</td>
</tr>
</table>
</div>

View File

@@ -1,37 +0,0 @@
<div class='container'>
<filler-config linker="registerFillerConfig" on-done="onFillerConfigDone"></filler-config>
<delete-filler linker="registerDeleteFiller" on-exit="onFillerDelete"></delete-filler>
<h5>
Filler Lists
<button class="pull-right btn btn-sm btn-primary" ng-click="selectFiller(-1)">
<span class="fa fa-plus"></span>
</button>
</h5>
<table class="table">
<tr>
<th>Name</th>
<th width="40">Clips</th>
<th style='width:2em'></th>
</tr>
<tr ng-if="fillers.length === 0">
<td colspan="3">
<p class="text-center text-danger">No filler sources set. Click the <span class="fa fa-plus"></span> to add filler lists.</p>
</td>
</tr>
<tr class='filler-row' ng-repeat="x in fillers" ng-click="selectFiller($index)" style="cursor: pointer; height: 3em" >
<td style='height: 3em'>
<div class="loader" ng-if="x.pending"></div>
<span ng-show="!x.pending">{{x.name}}</span>
</td>
<td>{{x.count}}</td>
<td>
<button class='btn btn-link' title='Delete...' ng-click='deleteFiller($index)' >
<i class='fas fa-trash-alt text-danger'></i>
</button>
</td>
</tr>
</table>
</div>

View File

@@ -1,88 +0,0 @@
<div class='container-fluid'>
<h5>
{{title}}
</h5>
<div style='padding:0; position:relative'>
<table class="table tvguide" style="{'column-width': colspanPercent + '%' }">
<tr>
<th colspan="{{channelColspan}}" class='guidenav even' >
<button class="btn btn-sm btn-primary" ng-click="zoomIn()" ng-disabled='!zoomInEnabled()'>
<i class="fa fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-primary" ng-click="zoomOut()" ng-disabled='!zoomOutEnabled()'>
<i class="fa fa-search-minus"></i>
</button>
<button class="btn btn-sm btn-primary" ng-click="back()" ng-disabled='!backEnabled()'>
<i class="fa fa-arrow-left"></i>
</button>
<button class="btn btn-sm btn-primary" ng-click="next()" ng-disabled='!nextEnabled()'>
<i class="fa fa-arrow-right"></i>
</button>
</th>
<th class="hour" ng-Class="{'even' : ($index % 2==1), 'odd' : ($index % 2==0) }" ng-repeat="time in times track by $index" colspan="{{time.duration}}">
{{time.label}}
</th>
</tr>
<tr ng-mouseover="channels[channelNumber].mouse=true" ng-mouseleave="channels[channelNumber].mouse=false" ng-repeat="channelNumber in channelNumbers track by $index" ng-Class="{'even' : ($index % 2==0), 'odd' : ($index % 2==1) }" >
<td title='{{channels[channelNumber].altTitle}}' class='even channel-number' colspan="{{channelNumberColspan}}" >
<div>
<a role="button" href='/media-player/{{channelNumber}}.m3u' title="Attempt to play channel: '{{channels[channelNumber].altTitle}}' in local media player" class='btn btn-sm btn-outline-primary play-channel' ng-show='channels[channelNumber].mouse'>
<span class='fa fa-play'></span>
</a>
<span ng-hide='channels[channelNumber].mouse' >
{{channels[channelNumber].number}}
</span>
</div>
</td>
<td title='{{channels[channelNumber].altTitle}}' class='even channel-icon' colspan="{{channelIconColspan}}" >
<img src='{{channels[channelNumber].icon.path}}' alt='{{channels[channelNumber].name}}' ng-click='channels[channelNumber].mouse=true' ></img>
</td>
<td class='odd program' colspan="{{totalSpan}}" ng-if="channels[channelNumber].loading">
<div class='loader'></div>
</td>
<td ng-repeat="program in channels[channelNumber].programs track by $index" colspan="{{program.duration}}"
title="{{program.altTitle}}"
ng-Class="{'program' : true, 'program-with-end' : program.end, 'program-with-start' : program.start, 'even' : ($index % 2==1), 'odd' : ($index % 2==0) }"
ng-if="! channels[channelNumber].loading"
>
<div class='show-title'>
{{program.showTitle}}
</div>
<div class='sub-title'>
{{program.subTitle}} <span class='episodeTitle'>{{program.episodeTitle}}</span>
</div>
</td>
</tr>
<tr>
<th colspan="{{channelColspan}}" class='guidenav even' >
<button class="btn btn-sm btn-primary" ng-click="zoomIn()" ng-disabled='!zoomInEnabled()'>
<span class="fa fa-search-plus"></span>
</button>
<button class="btn btn-sm btn-primary" ng-click="zoomOut()" ng-disabled='!zoomOutEnabled()'>
<span class="fa fa-search-minus"></span>
</button>
<button class="btn btn-sm btn-primary" ng-click="back()" ng-disabled='!backEnabled()'>
<span class="fa fa-arrow-left"></span>
</button>
<button class="btn btn-sm btn-primary" ng-click="next()" ng-disabled='!nextEnabled()'>
<span class="fa fa-arrow-right"></span>
</button>
</th>
<th class="hour" ng-Class="{'even' : ($index % 2==1), 'odd' : ($index % 2==0) }" ng-repeat="time in times track by $index" colspan="{{time.duration}}">
{{time.label}}
</th>
</tr>
</table>
<div class="tv-guide-now" ng-style="{'left': nowPosition + '%'}" ng-show='showNow' ></div>
</div>
</div>

View File

@@ -1,40 +0,0 @@
<div class='container'>
<div class='row gy-15'>
<div class='col'>
<h5>
Library
</h5>
<p>Components that will allow you to organize your media library to help with the creation of channels that do things the way you want.</p>
</div>
</div>
<div class='row'>
<div class='col-md-auto'>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><a href="#!/filler" class="card-link">Filler...</a></h5>
<p class="card-text">Filler lists are collections of videos that you may want to play during <i>&apos;flex&apos;</i> time segments. Flex is time within a channel that does not have a program scheduled (Usually used for padding).</p>
</div>
</div>
</div>
<div class='col-md-auto'>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><a href="#!/custom-shows" class="card-link">Custom Shows....</a></h5>
<p class="card-text">Custom Shows are sequences of videos that represent a episodes of a virtual TV show. When you add these shows to a channel, the schedule tools will treat the videos as if they belonged to a single TV show.</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,85 +0,0 @@
<div class='container'>
<h5>Player</h5>
<p class='text-small text-info'>Play your channels in a local media player. This is mostly meant for testing purposes and to show what endpoints are available. </p>
<div class="form-group row">
<label for="channel" class="col-form-label col-sm-2">Channel:</label>
<div class="col">
<div ng-show='loading'>
<div class='loader'></div>
</div>
<input ng-show='! loading && channelOptions.length == 1' readonly class='form-control-plaintext'
id="endpoint"
value = "No channels found."
>
</input>
<select
id="channel"
ng-show='! loading && channelOptions.length != 1'
class="custom-select"
ng-model="channel"
ng-options="o.id as o.description for o in channelOptions"
>
</select>
</div>
</div>
<div class="form-group row">
<label for="endpoint" class="col-form-label col-sm-2">Endpoint:</label>
<div class="col">
<select
id="endpoint"
class="custom-select"
ng-model="selectedEndpoint"
ng-options="o.id as o.description for o in endpointOptions"
aria-describedby="endpointHelp"
>
</select>
<small id="endpointHelp" class="form-text text-muted">
<span ng-show="selectedEndpoint == 'video' ">
The /video endpoint is the one used by IPTV player or Plex to play the channel&apos;s content. It creates a single mpegts stream for the channel out of all of the videos scheduled for it. For this reason, it needs the videos to be formatted to the same codec and resolution (normalized). Use this endpoint to debug issues with Plex/IPTV players or when the other endpoints don&apos;t work correctly in your player.
</span>
<span ng-show="selectedEndpoint == 'm3u8' ">
The /m3u8 endpoint (misnomer) sends the channel as a playlist of videos, which allows <i>some</i> players to play the channel in sequence without the need for a single stream. Since there is no need for a single stream, it requires less normalization work .
</span>
<span ng-show="selectedEndpoint == 'radio' ">
The /radio endpoint plays only the audio of the channel, effectively turning it into a radio station. If you only need the audio, this endpoint is much more efficient as it will not need to extract or transcode video at all.
</span>
</small>
</div>
</div>
<div class="form-group row">
<label for="endpoint" class="col-form-label col-sm-2">URL to play:</label>
<div class="col">
<input readonly class='form-control-plaintext'
id="endpoint"
style="font-family: monospace"
value = "{{ endpoint() }}"
>
</input>
</div>
</div>
<div class='form-group row' ng-show="! buttonDisabled()" >
<div class='col-sm-2'>
<a
role="button"
ng-href='{{ endpointButtonHref() }}'
style="width:99%"
title="Attempt to play in local media player" class='btn btn-primary player-button'
>
<span class='fa fa-play'> Play</span>
</a>
</div>
</div>
</div>

View File

@@ -1,29 +0,0 @@
<div class='container'>
<ul class="nav nav-tabs">
<li class="nav-item">
<span class="nav-link btn btn-link {{ selected === 'xmltv' ? 'active' : ''}}" ng-click="selected = 'xmltv'">
XMLTV
</span>
</li>
<li class="nav-item">
<span class="nav-link btn btn-link {{ selected === 'ffmpeg' ? 'active' : ''}}" ng-click="selected = 'ffmpeg'">
FFMPEG
</span>
</li>
<li class="nav-item">
<span class="nav-link btn btn-link {{ selected === 'plex' ? 'active' : ''}}" ng-click="selected = 'plex'">
Plex
</span>
</li>
<li class="nav-item">
<span class="nav-link btn btn-link {{ selected === 'hdhr' ? 'active' : ''}}" ng-click="selected = 'hdhr'">
HDHR
</span>
</li>
</ul>
<br ></br>
<plex-settings ng-if="selected == 'plex'"></plex-settings>
<ffmpeg-settings ng-if="selected == 'ffmpeg'"></ffmpeg-settings>
<xmltv-settings ng-if="selected == 'xmltv'"></xmltv-settings>
<hdhr-settings ng-if="selected == 'hdhr'"></hdhr-settings>
</div>

View File

@@ -1,31 +0,0 @@
<div class='container'>
<h5>
Version Info
</h5>
<table class="table">
<tr>
<th width="120">Component</th>
<th width="120">Version</th>
</tr>
<tr>
<td>dizqueTV-backend</td>
<td><div class='loader' ng-if="version.length &lt;= 0"></div>{{version}}</td>
</tr>
<tr>
<td>dizqueTV-ui</td>
<td id='uiversion'><div class='loader'></div></td>
</tr>
<tr>
<td>FFMPEG</td>
<td><div class='loader' ng-if="version.length &lt;= 0"></div>{{ffmpegVersion}}</td>
</tr>
<tr>
<td>nodejs</td>
<td><div class='loader' ng-if="version.length &lt;= 0"></div>{{nodejs}}</td>
</tr>
<!-- coming soon: plex version, whatever can be used to help debug things-->
</table>
</div>

View File

@@ -1,262 +0,0 @@
//This is an exact copy of the file with the same now in the nodejs
//one of these days, we'll figure out how to share the code.
module.exports = function (getShowData) {
/*** Input: list of programs
* output: sorted list of programs */
function sortShows(programs) {
let shows = {}
let movies = [] //not exactly accurate name
let newProgs = []
let progs = programs
for (let i = 0, l = progs.length; i < l; i++) {
let showData = getShowData( progs[i] );
if ( showData.showId === 'movie.' || ! showData.hasShow ) {
movies.push(progs[i]);
} else {
if (typeof shows[showData.showId] === 'undefined') {
shows[showData.showId] = [];
}
shows[showData.showId].push(progs[i]);
}
}
let keys = Object.keys(shows)
for (let i = 0, l = keys.length; i < l; i++) {
shows[keys[i]].sort((a, b) => {
let aData = getShowData(a);
let bData = getShowData(b);
return aData.order - bData.order;
})
newProgs = newProgs.concat(shows[keys[i]])
}
return newProgs.concat(movies);
}
function shuffle(array, lo, hi ) {
if (typeof(lo) === 'undefined') {
lo = 0;
hi = array.length;
}
let currentIndex = hi, temporaryValue, randomIndex
while (lo !== currentIndex) {
randomIndex = lo + Math.floor(Math.random() * (currentIndex -lo) );
currentIndex -= 1
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}
let removeDuplicates = (progs) => {
let tmpProgs = {}
for (let i = 0, l = progs.length; i < l; i++) {
if ( progs[i].type ==='redirect' ) {
tmpProgs['_redirect ' + progs[i].channel + ' _ '+ progs[i].duration ] = progs[i];
} else {
let data = getShowData(progs[i]);
if (data.hasShow) {
let key = data.showId + "|" + data.order;
tmpProgs[key] = progs[i];
}
}
}
let newProgs = [];
let keys = Object.keys(tmpProgs);
for (let i = 0, l = keys.length; i < l; i++) {
newProgs.push(tmpProgs[keys[i]])
}
return newProgs;
}
let removeSpecials = (progs) => {
let tmpProgs = []
for (let i = 0, l = progs.length; i < l; i++) {
if (
(typeof(progs[i].customShowId) !== 'undefined')
||
(progs[i].season !== 0)
) {
tmpProgs.push(progs[i]);
}
}
return tmpProgs;
}
let getProgramDisplayTitle = (x) => {
let s = x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title
if (typeof(x.customShowId) !== 'undefined') {
s = x.customShowName + " X" + (x.customOrder+1).toString().padStart(2,'0') + " (" + s + ")";
}
return s;
}
let sortByDate = (programs) => {
programs.sort( (a,b) => {
let aHas = ( typeof(a.date) !== 'undefined' );
let bHas = ( typeof(b.date) !== 'undefined' );
if (!aHas && !bHas) {
return 0;
} else if (! aHas) {
return 1;
} else if (! bHas) {
return -1;
}
if (a.date < b.date ) {
return -1;
} else if (a.date > b.date) {
return 1;
} else {
let aHasSeason = ( typeof(a.season) !== 'undefined' );
let bHasSeason = ( typeof(b.season) !== 'undefined' );
if (! aHasSeason && ! bHasSeason) {
return 0;
} else if (! aHasSeason) {
return 1;
} else if (! bHasSeason) {
return -1;
}
if (a.season < b.season) {
return -1;
} else if (a.season > b.season) {
return 1;
} else if (a.episode < b.episode) {
return -1;
} else if (a.episode > b.episode) {
return 1;
} else {
return 0;
}
}
});
return programs;
}
let programSquareStyle = (program) => {
let background ="";
if ( (program.isOffline) && (program.type !== 'redirect') ) {
background = "rgb(255, 255, 255)";
} else {
let r = 0, g = 0, b = 0, r2=0, g2=0,b2=0;
let angle = 45;
let w = 3;
if (program.type === 'redirect') {
angle = 0;
w = 4 + (program.channel % 10);
let c = (program.channel * 100019);
//r = 255, g = 0, b = 0;
//r2 = 0, g2 = 0, b2 = 255;
r = ( (c & 3) * 77 );
g = ( ( (c >> 1) & 3) * 77 );
b = ( ( (c >> 2) & 3) * 77 );
r2 = ( ( (c >> 5) & 3) * 37 );
g2 = ( ( (c >> 3) & 3) * 37 );
b2 = ( ( (c >> 4) & 3) * 37 );
} else if ( typeof(program.customShowId) !== 'undefined') {
let h = Math.abs( getHashCode(program.customShowId, false));
let h2 = Math.abs( getHashCode(program.customShowId, true));
r = h % 256;
g = (h / 256) % 256;
b = (h / (256*256) ) % 256;
r2 = (h2 / (256*256) ) % 256;
g2 = (h2 / (256*256) ) % 256;
b2 = (h2 / (256*256) ) % 256;
angle = (360 - 90 + h % 180) % 360;
if ( angle >= 350 || angle < 10 ) {
angle += 53;
}
} else if (program.type === 'episode') {
let h = Math.abs( getHashCode(program.showTitle, false));
let h2 = Math.abs( getHashCode(program.showTitle, true));
r = h % 256;
g = (h / 256) % 256;
b = (h / (256*256) ) % 256;
r2 = (h2 / (256*256) ) % 256;
g2 = (h2 / (256*256) ) % 256;
b2 = (h2 / (256*256) ) % 256;
angle = (360 - 90 + h % 180) % 360;
if ( angle >= 350 || angle < 10 ) {
angle += 53;
}
} else if (program.type === 'track') {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 315;
w = 2;
} else {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 45;
w = 6;
}
let rgb1 = "rgb("+ r + "," + g + "," + b +")";
let rgb2 = "rgb("+ r2 + "," + g2 + "," + b2 +")"
angle += 90;
background = "repeating-linear-gradient( " + angle + "deg, " + rgb1 + ", " + rgb1 + " " + w + "px, " + rgb2 + " " + w + "px, " + rgb2 + " " + (w*2) + "px)";
}
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);
a = Math.min( w, Math.max(0.3, a) );
b = w - a + 0.01;
return {
'width': `${a}%`,
'height': '1.3em',
'margin-right': `${b}%`,
'background': background,
'border': '1px solid black',
'margin-top': "0.01em",
'margin-bottom': '1px',
};
}
let getHashCode = (s, rev) => {
var hash = 0;
if (s.length == 0) return hash;
let inc = 1, st = 0, e = s.length;
if (rev) {
inc = -1, st = e - 1, e = -1;
}
for (var i = st; i != e; i+= inc) {
hash = s.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
let interpolate = ( () => {
let h = 60*60*1000;
let ix = [0, 1*h, 2*h, 4*h, 8*h, 24*h];
let iy = [0, 1.0, 1.25, 1.5, 1.75, 2.0];
let n = ix.length;
return (x) => {
for (let i = 0; i < n-1; i++) {
if( (ix[i] <= x) && ( (x < ix[i+1]) || i==n-2 ) ) {
return iy[i] + (iy[i+1] - iy[i]) * ( (x - ix[i]) / (ix[i+1] - ix[i]) );
}
}
}
} )();
return {
sortShows: sortShows,
shuffle: shuffle,
removeDuplicates: removeDuplicates,
removeSpecials: removeSpecials,
sortByDate: sortByDate,
getProgramDisplayTitle: getProgramDisplayTitle,
programSquareStyle: programSquareStyle,
}
}

View File

@@ -1,435 +0,0 @@
module.exports = function ($http, $q) {
return {
getVersion: () => {
return $http.get('/api/version').then((d) => {
return d.data;
});
},
getPlexServers: () => {
return $http.get('/api/plex-servers').then((d) => {
return d.data;
});
},
addPlexServer: (plexServer) => {
return $http({
method: 'PUT',
url: '/api/plex-servers',
data: plexServer,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
updatePlexServer: (plexServer) => {
return $http({
method: 'POST',
url: '/api/plex-servers',
data: plexServer,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
checkExistingPlexServer: async (serverName) => {
let d = await $http({
method: 'POST',
url: '/api/plex-servers/status',
data: { name: serverName },
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
return d.data;
},
checkNewPlexServer: async (server) => {
let d = await $http({
method: 'POST',
url: '/api/plex-servers/foreignstatus',
data: server,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
return d.data;
},
removePlexServer: async (serverName) => {
let d = await $http({
method: 'DELETE',
url: '/api/plex-servers',
data: { name: serverName },
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
return d.data;
},
getPlexSettings: () => {
return $http.get('/api/plex-settings').then((d) => {
return d.data;
});
},
updatePlexSettings: (config) => {
return $http({
method: 'PUT',
url: '/api/plex-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
resetPlexSettings: (config) => {
return $http({
method: 'POST',
url: '/api/plex-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
getFfmpegSettings: () => {
return $http.get('/api/ffmpeg-settings').then((d) => {
return d.data;
});
},
updateFfmpegSettings: (config) => {
return $http({
method: 'PUT',
url: '/api/ffmpeg-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
resetFfmpegSettings: (config) => {
return $http({
method: 'POST',
url: '/api/ffmpeg-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
getXmltvSettings: () => {
return $http.get('/api/xmltv-settings').then((d) => {
return d.data;
});
},
updateXmltvSettings: (config) => {
return $http({
method: 'PUT',
url: '/api/xmltv-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
resetXmltvSettings: (config) => {
return $http({
method: 'POST',
url: '/api/xmltv-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
getHdhrSettings: () => {
return $http.get('/api/hdhr-settings').then((d) => {
return d.data;
});
},
updateHdhrSettings: (config) => {
return $http({
method: 'PUT',
url: '/api/hdhr-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
resetHdhrSettings: (config) => {
return $http({
method: 'POST',
url: '/api/hdhr-settings',
data: angular.toJson(config),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
getChannels: () => {
return $http.get('/api/channels').then((d) => {
return d.data;
});
},
getChannel: (number) => {
return $http.get(`/api/channel/${number}`).then((d) => {
return d.data;
});
},
getChannelDescription: (number) => {
return $http.get(`/api/channel/description/${number}`).then((d) => {
return d.data;
});
},
getChannelProgramless: (number) => {
return $http.get(`/api/channel/programless/${number}`).then((d) => {
return d.data;
});
},
getChannelPrograms: (number) => {
return $http.get(`/api/channel/programs/${number}`).then((d) => {
return d.data;
});
},
getChannelNumbers: () => {
return $http.get('/api/channelNumbers').then((d) => {
return d.data;
});
},
addChannel: (channel) => {
return $http({
method: 'POST',
url: '/api/channel',
data: angular.toJson(channel),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
uploadImage: (file) => {
return $http({
method: 'POST',
url: '/api/upload/image',
data: file,
headers: { 'Content-Type': undefined },
}).then((d) => {
return d.data;
});
},
addChannelWatermark: (file) => {
return $http({
method: 'POST',
url: '/api/channel/watermark',
data: file,
headers: { 'Content-Type': undefined },
}).then((d) => {
return d.data;
});
},
updateChannel: (channel) => {
return $http({
method: 'PUT',
url: '/api/channel',
data: angular.toJson(channel),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
removeChannel: (channel) => {
return $http({
method: 'DELETE',
url: '/api/channel',
data: angular.toJson(channel),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((d) => {
return d.data;
});
},
/*======================================================================
* Filler stuff
*/
getAllFillersInfo: async () => {
let f = await $http.get('/api/fillers');
return f.data;
},
getFiller: async (id) => {
let f = await $http.get(`/api/filler/${id}`);
return f.data;
},
updateFiller: async (id, filler) => {
return (
await $http({
method: 'POST',
url: `/api/filler/${id}`,
data: angular.toJson(filler),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
})
).data;
},
createFiller: async (filler) => {
return (
await $http({
method: 'PUT',
url: `/api/filler`,
data: angular.toJson(filler),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
})
).data;
},
deleteFiller: async (id) => {
return (
await $http({
method: 'DELETE',
url: `/api/filler/${id}`,
data: {},
headers: { 'Content-Type': 'application/json; charset=utf-8' },
})
).data;
},
getChannelsUsingFiller: async (fillerId) => {
return (await $http.get(`/api/filler/${fillerId}/channels`)).data;
},
/*======================================================================
* Custom Show stuff
*/
getAllShowsInfo: async () => {
let f = await $http.get('/api/shows');
return f.data;
},
getShow: async (id) => {
let f = await $http.get(`/api/show/${id}`);
return f.data;
},
updateShow: async (id, show) => {
return (
await $http({
method: 'POST',
url: `/api/show/${id}`,
data: angular.toJson(show),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
})
).data;
},
createShow: async (show) => {
return (
await $http({
method: 'PUT',
url: `/api/show`,
data: angular.toJson(show),
headers: { 'Content-Type': 'application/json; charset=utf-8' },
})
).data;
},
deleteShow: async (id) => {
return (
await $http({
method: 'DELETE',
url: `/api/show/${id}`,
data: {},
headers: { 'Content-Type': 'application/json; charset=utf-8' },
})
).data;
},
/*======================================================================
* TV Guide endpoints
*/
getGuideStatus: async () => {
let d = await $http({
method: 'GET',
url: '/api/guide/status',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
return d.data;
},
getChannelLineup: async (channelNumber, dateFrom, dateTo) => {
let a = dateFrom.toISOString();
let b = dateTo.toISOString();
let d = await $http({
method: 'GET',
url: `/api/guide/channels/${channelNumber}?dateFrom=${a}&dateTo=${b}`,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
return d.data;
},
/*======================================================================
* Channel Tool Services
*/
calculateTimeSlots: async (programs, schedule) => {
console.log(programs, schedule);
let d = await $http({
method: 'POST',
url: '/api/channel-tools/time-slots',
data: {
programs: programs,
schedule: schedule,
},
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
return d.data;
},
calculateRandomSlots: async (programs, schedule) => {
let d = await $http({
method: 'POST',
url: '/api/channel-tools/random-slots',
data: {
programs: programs,
schedule: schedule,
},
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
return d.data;
},
/*======================================================================
* Settings
*/
getAllSettings: async () => {
var deferred = $q.defer();
$http({
method: 'GET',
url: '/api/settings/cache',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((response) => {
if (response.status === 200) {
deferred.resolve(response.data);
} else {
deferred.reject();
}
});
return deferred.promise;
},
putSetting: async (key, value) => {
console.warn(key, value);
var deferred = $q.defer();
$http({
method: 'PUT',
url: `/api/settings/cache/${key}`,
data: {
value,
},
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}).then((response) => {
if (response.status === 200) {
deferred.resolve(response.data);
} else {
deferred.reject();
}
});
return deferred.promise;
},
};
};

View File

@@ -1,65 +0,0 @@
//This is an exact copy of the file with the same now in the nodejs
//one of these days, we'll figure out how to share the code.
module.exports = function () {
let movieTitleOrder = {};
let movieTitleOrderNumber = 0;
return (program) => {
if ( typeof(program.customShowId) !== 'undefined' ) {
return {
hasShow : true,
showId : "custom." + program.customShowId,
showDisplayName : program.customShowName,
order : program.customOrder,
}
} else if (program.isOffline && program.type === 'redirect') {
return {
hasShow : true,
showId : "redirect." + program.channel,
order : program.duration,
showDisplayName : `Redirect to channel ${program.channel}`,
channel: program.channel,
}
} else if (program.isOffline) {
return {
hasShow : false
}
} else if (program.type === 'movie') {
let key = program.serverKey + "|" + program.key;
if (typeof(movieTitleOrder[key]) === 'undefined') {
movieTitleOrder[key] = movieTitleOrderNumber++;
}
return {
hasShow : true,
showId : "movie.",
showDisplayName : "Movies",
order : movieTitleOrder[key],
}
} else if ( (program.type === 'episode') || (program.type === 'track') ) {
let s = 0;
let e = 0;
if ( typeof(program.season) !== 'undefined') {
s = program.season;
}
if ( typeof(program.episode) !== 'undefined') {
e = program.episode;
}
let prefix = "tv.";
if (program.type === 'track') {
prefix = "audio.";
}
return {
hasShow: true,
showId : prefix + program.showTitle,
showDisplayName : program.showTitle,
order : s * 1000000 + e,
}
} else {
return {
hasShow : false,
}
}
}
}

View File

@@ -1,427 +0,0 @@
module.exports = function ($http, $window, $interval, dizquetv) {
let exported = {
login: async () => {
const headers = {
Accept: 'application/json',
'X-Plex-Product': 'dizqueTV',
'X-Plex-Version': 'Plex OAuth',
'X-Plex-Client-Identifier': 'rg14zekk3pa5zp4safjwaa8z',
'X-Plex-Model': 'Plex OAuth',
};
return new Promise((resolve, reject) => {
$http({
method: 'POST',
url: 'https://plex.tv/api/v2/pins?strong=true',
headers: headers,
}).then(
(res) => {
const plexWindowSizes = {
width: 800,
height: 700,
};
const plexWindowPosition = {
width: window.innerWidth / 2 + plexWindowSizes.width,
height: window.innerHeight / 2 - plexWindowSizes.height,
};
const authModal = $window.open(
`https://app.plex.tv/auth/#!?clientID=rg14zekk3pa5zp4safjwaa8z&context[device][version]=Plex OAuth&context[device][model]=Plex OAuth&code=${res.data.code}&context[device][product]=Plex Web`,
'_blank',
`height=${plexWindowSizes.height}, width=${plexWindowSizes.width}, top=${plexWindowPosition.height}, left=${plexWindowPosition.width}`,
);
let limit = 120000; // 2 minute time out limit
let poll = 2000; // check every 2 seconds for token
let interval = $interval(() => {
$http({
method: 'GET',
url: `https://plex.tv/api/v2/pins/${res.data.id}`,
headers: headers,
}).then(
async (r2) => {
limit -= poll;
if (limit <= 0) {
$interval.cancel(interval);
if (authModal) {
authModal.close();
}
reject(
'Timed Out. Failed to sign in a timely manner (2 mins)',
);
}
if (r2.data.authToken !== null) {
$interval.cancel(interval);
if (authModal) {
authModal.close();
}
headers['X-Plex-Token'] = r2.data.authToken;
$http({
method: 'GET',
url: 'https://plex.tv/api/v2/resources?includeHttps=1',
headers: headers,
})
.then((r3) => {
let res_servers = [];
const servers = r3.data;
servers.forEach((server) => {
// not pms, skip
if (server.provides != `server`) return;
res_servers.push(server);
});
res.servers = res_servers;
resolve(res);
})
.catch((err) => {
reject(err);
});
}
},
(err) => {
$interval.cancel(interval);
if (authModal) {
authModal.close();
}
reject(err);
},
);
}, poll);
},
(err) => {
reject(err);
},
);
});
},
check: async (server) => {
try {
await $http({
method: 'GET',
url: '/api/plex',
params: {
path: '/',
name: server.name,
},
});
return 1;
} catch (err) {
console.error(err);
return -1;
}
},
getLibrary: async (server) => {
const { data: res } = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: '/library/sections',
name: server.name,
},
});
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 { data: genresRes } = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: `/library/sections/${res.Directory[i].key}/genre`,
name: server.name,
},
});
for (
let q = 0,
k =
typeof genresRes.Directory !== 'undefined'
? genresRes.Directory.length
: 0;
q < k;
q++
) {
if (genresRes.Directory[q].type === 'genre') {
genres.push({
title: 'Genre: ' + genresRes.Directory[q].title,
key: genresRes.Directory[q].fastKey,
type: 'genre',
});
}
}
}
sections.push({
title: res.Directory[i].title,
key: `/library/sections/${res.Directory[i].key}/all`,
icon: `${server.uri}${res.Directory[i].composite}?X-Plex-Token=${server.accessToken}`,
type: res.Directory[i].type,
genres: genres,
});
}
return sections;
},
getPlaylists: async (server) => {
const { data: res } = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: '/playlists',
name: server.name,
},
});
var playlists = [];
for (
let i = 0,
l = typeof res.Metadata !== 'undefined' ? res.Metadata.length : 0;
i < l;
i++
)
if (
res.Metadata[i].playlistType === 'video' ||
res.Metadata[i].playlistType === 'audio'
) {
playlists.push({
title: res.Metadata[i].title,
key: res.Metadata[i].key,
icon: `${server.uri}${res.Metadata[i].composite}?X-Plex-Token=${server.accessToken}`,
duration: res.Metadata[i].duration,
});
}
return playlists;
},
getStreams: async (server, key) => {
const { data: res } = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: key,
name: server.name,
},
});
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') {
streams[
i
].key = `${server.uri}${streams[i].key}?X-Plex-Token=${server.accessToken}`;
}
}
return streams;
},
getNested: async (server, lib, includeCollections, errors) => {
const key = lib.key;
const { data: res } = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: key,
name: server.name,
},
});
const size =
typeof res.Metadata !== 'undefined' ? res.Metadata.length : 0;
var nested = [];
if (typeof lib.genres !== 'undefined') {
nested = Array.from(lib.genres);
}
var seenFiles = {};
let albumKeys = {};
let albums = {};
for (let i = 0; i < size; i++) {
let meta = res.Metadata[i];
if (meta.type === 'track') {
albumKeys[meta.parentKey] = false;
}
}
albumKeys = Object.keys(albumKeys);
await Promise.all(
albumKeys.map(async (albumKey) => {
try {
const { data: album } = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: albumKey,
name: server.name,
},
});
if (typeof album !== 'undefined' && album.size == 1) {
album = album.Metadata[0];
}
albums[albumKey] = album;
} catch (err) {
console.error(err);
}
}),
);
for (let i = 0; i < size; i++) {
try {
// Skip any videos (movie or episode) without a duration set...
if (
typeof res.Metadata[i].duration === 'undefined' &&
(res.Metadata[i].type === 'episode' ||
res.Metadata[i].type === 'movie')
)
continue;
if (
res.Metadata[i].duration <= 0 &&
(res.Metadata[i].type === 'episode' ||
res.Metadata[i].type === 'movie')
)
continue;
let year = res.Metadata[i].year;
let date = res.Metadata[i].originallyAvailableAt;
let album = undefined;
if (res.Metadata[i].type === 'track') {
//complete album year and date
album = albums[res.Metadata[i].parentKey];
if (typeof album !== 'undefined') {
year = album.year;
date = album.originallyAvailableAt;
}
}
if (typeof date === 'undefined' && typeof year !== 'undefined') {
date = `${year}-01-01`;
}
var program = {
title: res.Metadata[i].title,
key: res.Metadata[i].key,
ratingKey: res.Metadata[i].ratingKey,
server: server,
icon: `${server.uri}${res.Metadata[i].thumb}?X-Plex-Token=${server.accessToken}`,
type: res.Metadata[i].type,
duration: res.Metadata[i].duration,
durationStr: msToTime(res.Metadata[i].duration),
subtitle: res.Metadata[i].subtitle,
summary: res.Metadata[i].summary,
rating: res.Metadata[i].contentRating,
date: date,
year: year,
};
if (
program.type === 'episode' ||
program.type === 'movie' ||
program.type === 'track'
) {
program.plexFile = `${res.Metadata[i].Media[0].Part[0].key}`;
program.file = `${res.Metadata[i].Media[0].Part[0].file}`;
}
if (program.type === 'episode') {
//Make sure that video files that contain multiple episodes are only listed once:
var anyNewFile = false;
for (var j = 0; j < res.Metadata[i].Media.length; j++) {
for (var k = 0; k < res.Metadata[i].Media[j].Part.length; k++) {
var fileName = res.Metadata[i].Media[j].Part[k].file;
if (seenFiles[fileName] !== true) {
seenFiles[fileName] = true;
anyNewFile = true;
}
}
}
if (!anyNewFile) {
continue;
}
program.showTitle = res.Metadata[i].grandparentTitle;
program.episode = res.Metadata[i].index;
program.season = res.Metadata[i].parentIndex;
program.icon = `${server.uri}${res.Metadata[i].grandparentThumb}?X-Plex-Token=${server.accessToken}`;
program.episodeIcon = `${server.uri}${res.Metadata[i].thumb}?X-Plex-Token=${server.accessToken}`;
program.seasonIcon = `${server.uri}${res.Metadata[i].parentThumb}?X-Plex-Token=${server.accessToken}`;
program.showIcon = `${server.uri}${res.Metadata[i].grandparentThumb}?X-Plex-Token=${server.accessToken}`;
} else if (program.type === 'track') {
if (typeof album !== 'undefined') {
program.showTitle = album.title;
} else {
program.showTitle = res.Metadata[i].title;
}
program.episode = res.Metadata[i].index;
program.season = res.Metadata[i].parentIndex;
} else if (program.type === 'movie') {
program.showTitle = res.Metadata[i].title;
program.episode = 1;
program.season = 1;
}
nested.push(program);
} catch (err) {
let msg =
'Error when attempting to read nested data for ' +
key +
' ' +
res.Metadata[i].title;
errors.push(msg);
console.error(msg, err);
}
}
if (includeCollections === true && res.viewGroup !== 'artist') {
let k = res.librarySectionID;
k = `/library/sections/${k}/collections`;
let { data: collections } = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: k,
name: server.name,
},
});
if (typeof collections.Metadata === 'undefined') {
collections.Metadata = [];
}
let directories = collections.Metadata;
let nestedCollections = [];
for (let i = 0; i < directories.length; i++) {
let title;
if (res.viewGroup === 'show') {
title = directories[i].title + ' Collection';
} else {
title = directories[i].title;
}
nestedCollections.push({
key: directories[i].key,
title: title,
type: 'collection',
collectionType: res.viewGroup,
});
}
nested = nestedCollections.concat(nested);
}
return nested;
},
};
return exported;
};
function msToTime(duration) {
var milliseconds = parseInt((duration % 1000) / 100),
seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
hours = hours < 10 ? '0' + hours : hours;
minutes = minutes < 10 ? '0' + minutes : minutes;
seconds = seconds < 10 ? '0' + seconds : seconds;
return hours + ':' + minutes + ':' + seconds + '.' + milliseconds;
}

View File

@@ -1,18 +0,0 @@
module.exports = function () {
return {
get: () => {
return [
{id:"420x420",description:"420x420 (1:1)"},
{id:"480x270",description:"480x270 (HD1080/16 16:9)"},
{id:"576x320",description:"576x320 (18:10)"},
{id:"640x360",description:"640x360 (nHD 16:9)"},
{id:"720x480",description:"720x480 (WVGA 3:2)"},
{id:"800x600",description:"800x600 (SVGA 4:3)"},
{id:"1024x768",description:"1024x768 (WXGA 4:3)"},
{id:"1280x720",description:"1280x720 (HD 16:9)"},
{id:"1920x1080",description:"1920x1080 (FHD 16:9)"},
{id:"3840x2160",description:"3840x2160 (4K 16:9)"},
];
}
}
}

383
pnpm-lock.yaml generated
View File

@@ -333,7 +333,7 @@ importers:
specifier: 5.2.2
version: 5.2.2
web2:
web:
dependencies:
'@emotion/react':
specifier: ^11.11.1
@@ -446,10 +446,10 @@ importers:
version: 9.0.6
'@typescript-eslint/eslint-plugin':
specifier: ^6.0.0
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.2.2)
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.3.3)
'@typescript-eslint/parser':
specifier: ^6.0.0
version: 6.0.0(eslint@8.45.0)(typescript@5.2.2)
version: 6.0.0(eslint@8.45.0)(typescript@5.3.3)
'@vitejs/plugin-react-swc':
specifier: ^3.3.2
version: 3.3.2(vite@4.4.5)
@@ -464,16 +464,16 @@ importers:
version: 0.4.3(eslint@8.45.0)
nodemon:
specifier: ^3.0.3
version: 3.0.3
version: 3.1.0
openapi-zod-client:
specifier: ^1.14.0
version: 1.14.0(react@18.2.0)
ts-essentials:
specifier: ^9.4.1
version: 9.4.1(typescript@5.2.2)
version: 9.4.1(typescript@5.3.3)
typescript:
specifier: ^5.2.2
version: 5.2.2
version: 5.3.3
vite:
specifier: ^4.4.5
version: 4.4.5
@@ -525,20 +525,12 @@ packages:
openapi-types: 12.1.3
dev: true
/@babel/code-frame@7.22.13:
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.22.20
chalk: 2.4.2
/@babel/code-frame@7.23.5:
resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.23.4
chalk: 2.4.2
dev: true
/@babel/compat-data@7.23.5:
resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
@@ -608,21 +600,21 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.22.15
'@babel/types': 7.23.3
'@babel/types': 7.23.6
dev: true
/@babel/helper-hoist-variables@7.22.5:
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.3
'@babel/types': 7.23.6
dev: true
/@babel/helper-module-imports@7.22.15:
resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.3
'@babel/types': 7.23.6
/@babel/helper-module-transforms@7.23.3(@babel/core@7.23.6):
resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
@@ -642,24 +634,19 @@ packages:
resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.3
'@babel/types': 7.23.6
dev: true
/@babel/helper-split-export-declaration@7.22.6:
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.3
'@babel/types': 7.23.6
dev: true
/@babel/helper-string-parser@7.22.5:
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
engines: {node: '>=6.9.0'}
/@babel/helper-string-parser@7.23.4:
resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier@7.22.20:
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
@@ -681,14 +668,6 @@ packages:
- supports-color
dev: true
/@babel/highlight@7.22.20:
resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.22.20
chalk: 2.4.2
js-tokens: 4.0.0
/@babel/highlight@7.23.4:
resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==}
engines: {node: '>=6.9.0'}
@@ -696,7 +675,6 @@ packages:
'@babel/helper-validator-identifier': 7.22.20
chalk: 2.4.2
js-tokens: 4.0.0
dev: true
/@babel/parser@7.18.4:
resolution: {integrity: sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==}
@@ -706,14 +684,6 @@ packages:
'@babel/types': 7.23.6
dev: true
/@babel/parser@7.23.3:
resolution: {integrity: sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.23.3
dev: true
/@babel/parser@7.23.6:
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
engines: {node: '>=6.0.0'}
@@ -722,13 +692,6 @@ packages:
'@babel/types': 7.23.6
dev: true
/@babel/runtime@7.23.2:
resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
dev: false
/@babel/runtime@7.23.9:
resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==}
engines: {node: '>=6.9.0'}
@@ -740,9 +703,9 @@ packages:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.22.13
'@babel/parser': 7.23.3
'@babel/types': 7.23.3
'@babel/code-frame': 7.23.5
'@babel/parser': 7.23.6
'@babel/types': 7.23.6
dev: true
/@babel/traverse@7.23.6:
@@ -772,14 +735,6 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@babel/types@7.23.3:
resolution: {integrity: sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.22.5
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
/@babel/types@7.23.6:
resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==}
engines: {node: '>=6.9.0'}
@@ -787,7 +742,6 @@ packages:
'@babel/helper-string-parser': 7.23.4
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
dev: true
/@calebboyd/semaphore@1.3.1:
resolution: {integrity: sha512-17z9me12RgAEcMhIgR7f+BiXKbzwF9p1VraI69OxrUUSWGuSMOyOTEHQNVtMKuVrkEDVD0/Av5uiGZPBMYZljw==}
@@ -817,7 +771,7 @@ packages:
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
dependencies:
'@babel/helper-module-imports': 7.22.15
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
'@emotion/hash': 0.9.1
'@emotion/memoize': 0.8.1
'@emotion/serialize': 1.1.2
@@ -862,7 +816,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
'@emotion/babel-plugin': 11.11.0
'@emotion/cache': 11.11.0
'@emotion/serialize': 1.1.2
@@ -881,7 +835,7 @@ packages:
'@emotion/memoize': 0.8.1
'@emotion/unitless': 0.8.1
'@emotion/utils': 1.2.1
csstype: 3.1.2
csstype: 3.1.3
dev: false
/@emotion/sheet@1.2.2:
@@ -898,7 +852,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
'@emotion/babel-plugin': 11.11.0
'@emotion/is-prop-valid': 1.2.1
'@emotion/react': 11.11.1(@types/react@18.2.15)(react@18.2.0)
@@ -1727,13 +1681,13 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
'@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0)
'@mui/types': 7.2.8(@types/react@18.2.15)
'@mui/utils': 5.14.17(@types/react@18.2.15)(react@18.2.0)
'@mui/types': 7.2.13(@types/react@18.2.15)
'@mui/utils': 5.15.9(@types/react@18.2.15)(react@18.2.0)
'@popperjs/core': 2.11.8
'@types/react': 18.2.15
clsx: 2.0.0
clsx: 2.1.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -1754,7 +1708,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
'@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.15
react: 18.2.0
@@ -1777,18 +1731,18 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
'@emotion/react': 11.11.1(@types/react@18.2.15)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.15)(react@18.2.0)
'@mui/base': 5.0.0-beta.23(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@mui/core-downloads-tracker': 5.14.17
'@mui/system': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.15)(react@18.2.0)
'@mui/types': 7.2.8(@types/react@18.2.15)
'@mui/utils': 5.14.17(@types/react@18.2.15)(react@18.2.0)
'@mui/system': 5.15.9(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.15)(react@18.2.0)
'@mui/types': 7.2.13(@types/react@18.2.15)
'@mui/utils': 5.15.9(@types/react@18.2.15)(react@18.2.0)
'@types/react': 18.2.15
'@types/react-transition-group': 4.4.9
clsx: 2.0.0
csstype: 3.1.2
clsx: 2.1.0
csstype: 3.1.3
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -1796,23 +1750,6 @@ packages:
react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
dev: false
/@mui/private-theming@5.14.17(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-u4zxsCm9xmQrlhVPug+Ccrtsjv7o2+rehvrgHoh0siSguvVgVQq5O3Hh10+tp/KWQo2JR4/nCEwquSXgITS1+g==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@mui/utils': 5.14.17(@types/react@18.2.15)(react@18.2.0)
'@types/react': 18.2.15
prop-types: 15.8.1
react: 18.2.0
dev: false
/@mui/private-theming@5.15.9(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-/aMJlDOxOTAXyp4F2rIukW1O0anodAMCkv1DfBh/z9vaKHY3bd5fFf42wmP+0GRmwMinC5aWPpNfHXOED1fEtg==}
engines: {node: '>=12.0.0'}
@@ -1830,28 +1767,6 @@ packages:
react: 18.2.0
dev: false
/@mui/styled-engine@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0):
resolution: {integrity: sha512-AqpVjBEA7wnBvKPW168bNlqB6EN7HxTjLOY7oi275AzD/b1C7V0wqELy6NWoJb2yya5sRf7ENf4iNi3+T5cOgw==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@emotion/cache': 11.11.0
'@emotion/react': 11.11.1(@types/react@18.2.15)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.15)(react@18.2.0)
csstype: 3.1.2
prop-types: 15.8.1
react: 18.2.0
dev: false
/@mui/styled-engine@5.15.9(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0):
resolution: {integrity: sha512-NRKtYkL5PZDH7dEmaLEIiipd3mxNnQSO+Yo8rFNBNptY8wzQnQ+VjayTq39qH7Sast5cwHKYFusUrQyD+SS4Og==}
engines: {node: '>=12.0.0'}
@@ -1874,36 +1789,6 @@ packages:
react: 18.2.0
dev: false
/@mui/system@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-Ccz3XlbCqka6DnbHfpL3o3TfOeWQPR+ewvNAgm8gnS9M0yVMmzzmY6z0w/C1eebb+7ZP7IoLUj9vojg/GBaTPg==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@emotion/react': 11.11.1(@types/react@18.2.15)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.15)(react@18.2.0)
'@mui/private-theming': 5.14.17(@types/react@18.2.15)(react@18.2.0)
'@mui/styled-engine': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0)
'@mui/types': 7.2.8(@types/react@18.2.15)
'@mui/utils': 5.14.17(@types/react@18.2.15)(react@18.2.0)
'@types/react': 18.2.15
clsx: 2.0.0
csstype: 3.1.2
prop-types: 15.8.1
react: 18.2.0
dev: false
/@mui/system@5.15.9(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-SxkaaZ8jsnIJ77bBXttfG//LUf6nTfOcaOuIgItqfHv60ZCQy/Hu7moaob35kBb+guxVJnoSZ+7vQJrA/E7pKg==}
engines: {node: '>=12.0.0'}
@@ -1945,35 +1830,6 @@ packages:
'@types/react': 18.2.15
dev: false
/@mui/types@7.2.8(@types/react@18.2.15):
resolution: {integrity: sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.15
dev: false
/@mui/utils@5.14.17(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@types/prop-types': 15.7.10
'@types/react': 18.2.15
prop-types: 15.8.1
react: 18.2.0
react-is: 18.2.0
dev: false
/@mui/utils@5.15.9(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-yDYfr61bCYUz1QtwvpqYy/3687Z8/nS4zv7lv/ih/6ZFGMl1iolEvxRmR84v2lOYxlds+kq1IVYbXxDKh8Z9sg==}
engines: {node: '>=12.0.0'}
@@ -2029,15 +1885,15 @@ packages:
moment-jalaali:
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
'@emotion/react': 11.11.1(@types/react@18.2.15)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.15)(react@18.2.0)
'@mui/base': 5.0.0-beta.23(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@mui/system': 5.15.9(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.15)(react@18.2.0)
'@mui/utils': 5.14.17(@types/react@18.2.15)(react@18.2.0)
'@mui/utils': 5.15.9(@types/react@18.2.15)(react@18.2.0)
'@types/react-transition-group': 4.4.9
clsx: 2.0.0
clsx: 2.1.0
dayjs: 1.11.10
prop-types: 15.8.1
react: 18.2.0
@@ -2561,12 +2417,8 @@ packages:
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
dev: false
/@types/prop-types@15.7.10:
resolution: {integrity: sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==}
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
dev: false
/@types/qs@6.9.10:
resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==}
@@ -2597,9 +2449,9 @@ packages:
/@types/react@18.2.15:
resolution: {integrity: sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==}
dependencies:
'@types/prop-types': 15.7.10
'@types/prop-types': 15.7.11
'@types/scheduler': 0.16.6
csstype: 3.1.2
csstype: 3.1.3
/@types/responselike@1.0.3:
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
@@ -2686,6 +2538,37 @@ packages:
- supports-color
dev: true
/@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
'@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 6.0.0
'@typescript-eslint/type-utils': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.0.0
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.45.0
grapheme-splitter: 1.0.4
graphemer: 1.4.0
ignore: 5.2.4
natural-compare: 1.4.0
natural-compare-lite: 1.4.0
semver: 7.5.4
ts-api-utils: 1.0.3(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/parser@6.0.0(eslint@8.45.0)(typescript@5.2.2):
resolution: {integrity: sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -2707,6 +2590,27 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser@6.0.0(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 6.0.0
'@typescript-eslint/types': 6.0.0
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.0.0
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.45.0
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager@6.0.0:
resolution: {integrity: sha512-o4q0KHlgCZTqjuaZ25nw5W57NeykZT9LiMEG4do/ovwvOcPnDO1BI5BQdCsUkjxFyrCL0cSzLjvIMfR9uo7cWg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -2735,6 +2639,26 @@ packages:
- supports-color
dev: true
/@typescript-eslint/type-utils@6.0.0(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-ah6LJvLgkoZ/pyJ9GAdFkzeuMZ8goV6BH7eC9FPmojrnX9yNCIsfjB+zYcnex28YO3RFvBkV6rMV6WpIqkPvoQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.3.3)
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.45.0
ts-api-utils: 1.0.3(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/types@6.0.0:
resolution: {integrity: sha512-Zk9KDggyZM6tj0AJWYYKgF0yQyrcnievdhG0g5FqyU3Y2DRxJn4yWY21sJC0QKBckbsdKKjYDV2yVrrEvuTgxg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -2761,6 +2685,27 @@ packages:
- supports-color
dev: true
/@typescript-eslint/typescript-estree@6.0.0(typescript@5.3.3):
resolution: {integrity: sha512-2zq4O7P6YCQADfmJ5OTDQTP3ktajnXIRrYAtHM9ofto/CJZV3QfJ89GEaM2BNGeSr1KgmBuLhEkz5FBkS2RQhQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 6.0.0
'@typescript-eslint/visitor-keys': 6.0.0
debug: 4.3.4(supports-color@5.5.0)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
ts-api-utils: 1.0.3(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils@6.0.0(eslint@8.45.0)(typescript@5.2.2):
resolution: {integrity: sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -2781,6 +2726,26 @@ packages:
- typescript
dev: true
/@typescript-eslint/utils@6.0.0(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0)
'@types/json-schema': 7.0.14
'@types/semver': 7.5.4
'@typescript-eslint/scope-manager': 6.0.0
'@typescript-eslint/types': 6.0.0
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.3.3)
eslint: 8.45.0
eslint-scope: 5.1.1
semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/@typescript-eslint/visitor-keys@6.0.0:
resolution: {integrity: sha512-cvJ63l8c0yXdeT5POHpL0Q1cZoRcmRKFCtSjNGJxPkcP571EfZMcNbzWAc7oK3D1dRzm/V5EwtkANTZxqvuuUA==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -3157,7 +3122,7 @@ packages:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
cosmiconfig: 7.1.0
resolve: 1.22.8
dev: false
@@ -3699,11 +3664,6 @@ packages:
engines: {node: '>=0.8'}
dev: false
/clsx@2.0.0:
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
engines: {node: '>=6'}
dev: false
/clsx@2.1.0:
resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
engines: {node: '>=6'}
@@ -3947,12 +3907,8 @@ packages:
randomfill: 1.0.4
dev: true
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dev: false
/currently-unhandled@0.4.1:
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
@@ -4255,8 +4211,8 @@ packages:
/dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
'@babel/runtime': 7.23.2
csstype: 3.1.2
'@babel/runtime': 7.23.9
csstype: 3.1.3
dev: false
/domain-browser@1.2.0:
@@ -6119,7 +6075,7 @@ packages:
/match-sorter@6.3.1:
resolution: {integrity: sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==}
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
remove-accents: 0.4.2
dev: false
@@ -6901,7 +6857,7 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
dependencies:
'@babel/code-frame': 7.22.13
'@babel/code-frame': 7.23.5
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
@@ -7156,15 +7112,6 @@ packages:
yaml: 2.3.4
dev: true
/postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.7
picocolors: 1.0.0
source-map-js: 1.0.2
dev: true
/postcss@8.4.33:
resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
engines: {node: ^10 || ^12 || >=14}
@@ -7464,7 +7411,7 @@ packages:
react: '>=16.6.0'
react-dom: '>=16.6.0'
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
@@ -7479,7 +7426,7 @@ packages:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
memoize-one: 5.2.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -7583,7 +7530,7 @@ packages:
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
'@babel/runtime': 7.23.2
'@babel/runtime': 7.23.9
dev: false
/reflect-metadata@0.1.13:
@@ -7593,10 +7540,6 @@ packages:
/reflect-metadata@0.2.1:
resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==}
/regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: false
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: false
@@ -8473,15 +8416,13 @@ packages:
typescript: 5.2.2
dev: true
/ts-essentials@9.4.1(typescript@5.2.2):
resolution: {integrity: sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==}
/ts-api-utils@1.0.3(typescript@5.3.3):
resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
engines: {node: '>=16.13.0'}
peerDependencies:
typescript: '>=4.1.0'
peerDependenciesMeta:
typescript:
optional: true
typescript: '>=4.2.0'
dependencies:
typescript: 5.2.2
typescript: 5.3.3
dev: true
/ts-essentials@9.4.1(typescript@5.3.3):
@@ -8894,7 +8835,7 @@ packages:
optional: true
dependencies:
esbuild: 0.18.20
postcss: 8.4.31
postcss: 8.4.33
rollup: 3.29.4
optionalDependencies:
fsevents: 2.3.3
@@ -9030,7 +8971,7 @@ packages:
resolution: {integrity: sha512-exmM13v2lg8juBbfS2tao/alV68jyryPXS+jf29NBNGLzE2hRgmzvQFQGX5CxNfH4Ag9qRqd6gGpXTH2JxqKHg==}
engines: {node: '>=14'}
dependencies:
'@babel/parser': 7.23.3
'@babel/parser': 7.23.6
eval-estree-expression: 1.1.0
dev: true

View File

@@ -1,5 +1,5 @@
packages:
- server
- web2
- web
- types
- shared

View File

@@ -1,21 +0,0 @@
### Explanation of the changes, problem that they are intended to fix.
...
### All Submissions:
* [ ] I have read the code of conduct.
* [ ] I am submitting to the correct base branch
<!--
* Bug fixes must go to `dev/1.4.x`.
* New features must go to `dev/1.5.x`.
-->
### Changes that modify the db structure
* [ ] Backwards compatibility: Users running the new code using an existing .disquetv folder will not lose their channels / settings.
* [ ] I've implemented the necessary db migration steps if any.
### New features
* [ ] 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.

View File

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Some files were not shown because too many files have changed in this diff Show More