Merge pull request #112 from vexorian/20200911_dev

20200911 dev
This commit is contained in:
vexorian 2020-09-11 23:05:22 -04:00 committed by GitHub
commit d4cb8c0429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 216 additions and 135 deletions

141
README.md
View File

@ -9,147 +9,38 @@ dizqueTV is a fork of the project previously-known as [pseudotv-plex](https://gi
Configure your channels, programs, commercials and settings using the dizqueTV web UI.
Access your channels by adding the spoofed dizqueTV HDHomerun tuner to Plex, or utilize the M3U Url with any 3rd party app.
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
- TV Channels are made available as an IPTV stream. There's a lot of IPTV clients and software that supports it.
- 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.
- Centralized server instance so that you need only configure your channels once.
- Customize your TV channels' programming including the specific air times.
- Supports multiple channels all available through a XMLTV tv guide.
- Docker image and prepackage binaries for Windows, Linux and Mac
- Web UI for channel configuration and app settings
- 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 ("commercials", 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
- Sign into your Plex servers using any sign in method (Username, Email, Google, Facebook, etc.)
- Ability to auto update Plex DVR guide data and channel mappings
- Auto update the xmltv.xml file at a set interval (in hours). You can also set the amount EPG cache (in hours).
- Continuous playback support
- Media track selection (video, audio, subtitle). (subtitles disabled by default)
- Includes a WEB TV Guide where you can even play channels in your desktop by using your local media player.
- Subtitle support.
- Ability to overlay channel icon over stream
- Auto deinterlace any Plex media not marked `"scanType": "progressive"`
- Can be configured to completely force Direct play.
- Can normalize video formats to prevent stream breaking.
- Can be configured to completely force Direct play, if you are ready for the caveats.
## Limitations
- Plex Pass is required to unlock Plex Live TV/DVR feature
- Only one EPG source can be used with Plex server. This may cause an issue if you are adding the dizquetv tuner to a Plex server with Live TV/DVR already enabled/configured.
- 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'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's contents.
* There are projects like xteve that allow you to unify multiple EPG sources into a single list which Plex can use.
## Releases
- dizqueTV does not currently watch your Plex server for media updates/changes. You must manually remove and readd your programs for any changes to take effect. Same goes for Plex server changes (changing IP, port, etc).. all media will fail..
- Many IPTV players (including Plex) will break after switching episodes if video / audio format is too different between. dizqueTV can be configured to use ffmpeg transcoding to prevent htis, but that costs resources. This is an intrinsic issue with the IPTV approach.
- Plex's IPTV player will be always recording the stream's playback for the purposes of allowing you to pause or rewind the stream. This is not necessarily an issue with other IPTV players.
- https://github.com/vexorian/dizquetv/releases
## Wiki
## Useful Tips/Info
- For setup instructions, check [the wiki](https://github.com/vexorian/dizquetv/wiki)
- dizqueTV can use both Plex and ffmpeg transcoding. Plex transcoding is advantageous in that there's access for many more features and formats than would be available.
- Audio track and subtitle choice depends on Plex configuraiton for that video/episode and user.
- Subtitles are transcoded by Plex before being delivered to dizqueTV.
- Can be configured to force a direct stream both from Plex and dizqueTV's side.
- Playing many different kinds of formats and resolutions in the same stream does ntpossible without transcoding them. So unless you are certain that all formats used in the same channel will be identical, your life will be easier if you let ffmpeg be used for normalization.
- If normalization is too heavy, try utilizing your hardware's transcoding features by picking the correct encoder in FFMPEG settings. *Note that some encoders may not be capable of handling every transcoding scenario, libx264 and mpeg2video seem to be the most stable.*
- Intel Quick Sync: `h264_qsv`, `mpeg2_qsv`
- NVIDIA GPU: `h264_nvenc`
- MPEG2 `mpeg2video`
- H264 `libx264` (default)
- MacOS `h264_videotoolbox`
- **Enable the option to log ffmpeg's stderr output directly to the dizquetv app console, for detecting issues**
- Host your own images for channel icons, program icons, etc.. Simply add your image to `.dizquetv/images` and reference them via `http://dizquetv-ip:8000/images/myImage.png`
- Use the Block Shuffle feature to play a specified number of TV episodes before advancing to the next available TV show in the channel. You can also specify to randomize the TV Show order. Any movies added to the channel will be pushed to the end of the program lineup, this is also applicable the "Sort TV Shows" option.
- Plex is smart enough not to open another stream if it currently is being viewed by another user. This allows only one transcode session for mulitple viewers if they are watching the same channel.
- Use the tools menu in the channel editor to access a lot of features to process your channel's programming, such as shuffling.
- Flex time is a useful feature that allows you to configure breaks between TV shows that play random content. This is useful if you want to simulate "commercials". A frequent use case is to use this filler to pad the starting times of TV shows (so that all TV shows start at :00 or :30 times, for example.
- Even if your Plex server is running on the same machine as the dizqueTV app, use your network address (not a loopback) when configuring your Plex Server(s) in the web UI.
## Installation
* *If you were a pseudotv user, please rename your old `.pseudotv` to `.dizquetv` before running. dizque tv will attempt to migrate your settings and channels to the new features.*
Unless your are using Docker/Unraid, you must download and install **ffmpeg** to your system and set the correct path in the dizqueTV Web UI.
By default, dizquetv will create the directory `.dizquetv` wherever dizquetv is launched from. Your `xmltv.xml` file and config databases are stored here.
#### Binary Release
[Download](https://github.com/vexorian/dizquetv/releases) and run the dizqueTV executable (argument defaults below)
```
./dizquetv-win-x64.exe --port 8000 --database ./dizquetv
```
#### Docker
The Docker repository can be viewed [here](https://hub.docker.com/r/vexorian/dizquetv).
Use Docker to fetch dizqueTV, then run the container.. (replace `C:\.dizquetv` with your desired config directory location)
```
docker pull vexorian/dizquetv:latest
docker run --name dizquetv -p 8000:8000 -v C:\.dizquetv:/home/node/app/.dizquetv vexorian/dizquetv:latest
```
If you were a pseudotv user, make sure to stop the pseudotv container and use the same folder you used for configuration in pseudotv as configuration for dizquetv.
#### Unraid
Template Repository: [https://github.com/vexorian/dizquetv/tree/main](https://github.com/vexorian/dizquetv/tree/main)
#### Building Docker image from source
Build docker image from source and run the container. (replace `C:\.dizquetv` with your desired config directory location)
```
git clone https://github.com/vexorian/dizquetv
cd dizquetv
git checkout version
#replace version with the version you want
docker build -t dizquetv .
docker run --name dizquetv -p 8000:8000 -v C:\.dizquetv:/home/node/app/.dizquetv dizquetv
```
#### Unraid Install
Add
```
https://github.com/vexorian/dizquetv/tree/main
```
to your "Template repositories" in the Docker tab.
Click the "Add Container" button
Select either the dizquetv template or the dizquetv-nvidia template if you want nvidia hardware accelerated transcoding.
Make sure you have the Unraid Nvidia plugin installed and change your video encoder to h264_nvenc in the dizquetv ffmpeg settings.
#### From Source
Install NodeJS and FFMPEG
```
git clone https://github.com/vexorian/dizquetv
cd dizquetv
npm install
npm run build
npm run start
```
## Plex Setup
Add the dizqueTV spoofed HDHomerun tuner to Plex via Plex Settings.
If the tuner isn't automatically listed, manually enter the network address of dizquetv. Example:
```
127.0.0.1:8000
```
When prompted for a Postal/Zip code, click the `"Have an XMLTV guide on your server? Click here to use that instead."` link.
Enter the location of the `.dizquetv/xmltv.xml` file. Example (Windows):
```
C:\.dizquetv\xmltv.xml
```
**Do not use the Web UI XMLTV URL when feeding Plex the xmltv.xml file. Plex fails to update it's EPG from a URL for some reason (at least on Windows). Use the local file path to `.dizquetv/xmltv.xml`**
## App Preview
<img src="./docs/channels.png" width="500">

View File

@ -165,6 +165,14 @@ app.get('/version.js', (req, res) => {
app.use('/images', express.static(path.join(process.env.DATABASE, 'images')))
app.use(express.static(path.join(__dirname, 'web/public')))
app.use('/images', express.static(path.join(process.env.DATABASE, 'images')))
app.use('/favicon-16.png', express.static(
path.join(__dirname, 'resources/favicon-16.png')
) );
app.use('/favicon-32.png', express.static(
path.join(__dirname, 'resources/favicon-32.png')
) );
app.use(api.router(db, channelDB, xmltvInterval, guideService ))
app.use(video.router( channelDB, db))
app.use(hdhr.router)

BIN
resources/favicon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

BIN
resources/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

111
src/svg/favicon.svg Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="200"
height="200"
viewBox="0 0 52.9168 52.916668"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="favicon.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.5768297"
inkscape:cx="89.499712"
inkscape:cy="78.946496"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1880"
inkscape:window-height="1056"
inkscape:window-x="1920"
inkscape:window-y="24"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-244.08278)">
<g
id="g851"
transform="translate(0.529168)">
<g
transform="matrix(1.9936912,0,0,1.9936912,-142.37056,-231.27189)"
style="fill:#1f1f1f;fill-opacity:1;stroke-width:0.50158221"
id="g4581">
<rect
style="opacity:1;fill:#1f1f1f;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4524"
width="41.471352"
height="27.75024"
x="65.156158"
y="239.28041"
transform="rotate(-0.94645665)" />
</g>
<rect
transform="rotate(0.52601414)"
y="253.3098"
x="4.4118633"
height="35.886436"
width="15.94953"
id="rect4518"
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
transform="rotate(1.4727575)"
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4520"
width="15.949528"
height="35.886436"
x="24.544073"
y="252.93878" />
<rect
transform="rotate(-3.2986122)"
y="255.30464"
x="17.124516"
height="35.88644"
width="15.949525"
id="rect4522"
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<circle
r="4.9842277"
cy="257.7131"
cx="63.762325"
id="path4568"
style="opacity:1;fill:#6a6a6a;fill-opacity:0.86792453;stroke:none;stroke-width:1.46500003;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<circle
style="opacity:1;fill:#6a6a6a;fill-opacity:0.86792453;stroke:none;stroke-width:1.46500003;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle4570"
cx="66.117706"
cy="277.41794"
r="4.9842277" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -14,7 +14,6 @@ module.exports = function ($timeout, $location, dizquetv) {
scope.showHelp = false;
scope._frequencyModified = false;
scope._frequencyMessage = "";
scope.millisecondsOffset = 0;
scope.minProgramIndex = 0;
scope.episodeMemory = {
saved : false,
@ -89,8 +88,7 @@ module.exports = function ($timeout, $location, dizquetv) {
if (typeof(scope.channel.disableFillerOverlay) === 'undefined') {
scope.channel.disableFillerOverlay = true;
}
scope.millisecondsOffset = (t - offset) % 1000;
scope.channel.startTime = new Date(t - offset - scope.millisecondsOffset);
scope.channel.startTime = new Date(t - offset);
// move runningProgram to index 0
scope.channel.programs = scope.channel.programs.slice(runningProgram, this.length)
.concat(scope.channel.programs.slice(0, runningProgram) );
@ -696,7 +694,6 @@ module.exports = function ($timeout, $location, dizquetv) {
let progs = [];
let t = scope.channel.startTime.getTime();
t = t - t % mod;
scope.millisecondsOffset = 0;
scope.channel.startTime = new Date(t);
function addPad(force) {
let m = t % mod;
@ -878,10 +875,14 @@ module.exports = function ($timeout, $location, dizquetv) {
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
function shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex)
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]
@ -938,6 +939,29 @@ module.exports = function ($timeout, $location, dizquetv) {
});
return progs;
}
scope.replicate = (t) => {
let arr = [];
for (let j = 0; j < t; j++) {
for (let i = 0; i < scope.channel.programs.length; i++) {
arr.push( JSON.parse( angular.toJson(scope.channel.programs[i]) ) );
arr[i].$index = i;
}
}
scope.channel.programs = arr;
updateChannelDuration();
}
scope.shuffleReplicate =(t) => {
shuffle( scope.channel.programs );
let n = scope.channel.programs.length;
let a = Math.floor(n / 2);
scope.replicate(t);
for (let i = 0; i < t; i++) {
shuffle( scope.channel.programs, n*i, n*i + a);
shuffle( scope.channel.programs, n*i + a, n*i + n);
}
updateChannelDuration();
}
function cyclicShuffle(array) {
let shows = {};
let next = {};
@ -1025,6 +1049,7 @@ module.exports = function ($timeout, $location, dizquetv) {
channelNumbers.push(scope.channels[i].number)
// validate
var now = new Date()
scope.error.any = true;
if (typeof channel.number === "undefined" || channel.number === null || channel.number === "")
scope.error.number = "Select a channel number"
else if (channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1 && scope.isNewChannel) // we need the parseInt for indexOf to work properly
@ -1044,13 +1069,13 @@ module.exports = function ($timeout, $location, dizquetv) {
else if (channel.programs.length === 0)
scope.error.programs = "No programs have been selected. Select at least one program."
else {
channel.startTime.setMilliseconds( scope.millisecondsOffset);
scope.error.any = false;
for (let i = 0; i < scope.channel.programs.length; i++) {
delete scope.channel.programs[i].$index;
}
scope.onDone(JSON.parse(angular.toJson(channel)))
}
$timeout(() => { scope.error = {} }, 3500)
$timeout(() => { scope.error = {} }, 60000)
}
}
@ -1102,6 +1127,13 @@ module.exports = function ($timeout, $location, dizquetv) {
scope._selectedProgram = JSON.parse(angular.toJson(program));
}
}
scope.maxReplicas = () => {
if (scope.channel.programs.length == 0) {
return 1;
} else {
return Math.floor( 50000 / scope.channel.programs.length );
}
}
scope.removeItem = (x) => {
scope.channel.programs.splice(x, 1)
updateChannelDuration()

View File

@ -3,6 +3,8 @@
<head>
<title>dizqueTV</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/favicon-16.png" sizes="16x16"></link>
<link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32"></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">
@ -18,6 +20,11 @@
<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/U64P9MR" title='Discord' >
<span class="fab fa-discord"></span>

View File

@ -163,6 +163,13 @@
<h6>Reruns</h6>
<p>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. </p>
<h6>Replicate</h6>
<p>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, there&apos;s a limit of 50000 programs to the size of the resulting channel when using this tool.</p>
<h6>Replicate &amp; Shuffle</h6>
<p>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>
<h6>Save|Recover Episode Positions</h6>
<p>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>
@ -336,6 +343,30 @@
</div>
<div class="row">
<div class="col-md-6" style="padding: 5px;">
<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)">
<i class='fas fa-recycle'></i> Replicate
</button>
</div>
</div>
<div class="col-md-6" style="padding: 5px;">
<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)">
<i class='fas fa-dice'></i> Replicate &amp; Shuffle
</button>
</div>
</div>
</div>
<div class="row">
<div class="input-group col-md-6" style="padding: 5px;">
@ -443,6 +474,7 @@
</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>
<div class="text-right">
<button class="btn btn-sm btn-link" ng-click="_onDone()">
Cancel