Skip to content

Instantly share code, notes, and snippets.

@albertcard
Last active November 19, 2025 00:54
Show Gist options
  • Select an option

  • Save albertcard/3b8b0002afd367b82b2f41058487ae66 to your computer and use it in GitHub Desktop.

Select an option

Save albertcard/3b8b0002afd367b82b2f41058487ae66 to your computer and use it in GitHub Desktop.
  1. In Dispatcharr, I get my channels based on the uverse guide for my locale from here: https://www.tvtv.us/tx/bulverde/78163/luUSA-TX65939-X

  2. I add them in dispatcharr and number the channels based on the Uverse channel guide

  3. Then I have an nginx server where I plan to store the XML guides for dispatcharr to pull from

  4. I use this script to generate my XMLs (doesn't grab more than the first 1000 uverse channels (rate-limiting issues). Change the '$lineUpID=<ZIP>' to meet your tvtv URL for your station locale

<?php
//
// tvtv2xmltv Guide Data
// https://gist.github.com/idolpx/c82747bb740c303f56ad8a1e8f17d575
// Author: Jaime Idolpx ([email protected])
//
// - This script will extract guide data from "tvtv.us" and produce an "XmlTV" data file
// - Set the options for the guide data you want to extract below
// - Host this on a php enabled web server
// - Configure your TV Guide software to use it as a data source (Jellyfin in my case)
//
// https://www.tvtv.us/
// http://wiki.xmltv.org/index.php/XMLTVFormat
// https://www.xmltvlistings.com/help/api/xmltv
//


### THIS IS THE ONE I USE: https://gist.github.com/gboudreau/bf79115177ca0c2a3802951712f4b29d
### THIS IS THE GUIDE LINK: https://www.tvtv.us/tx/bulverde/78163/luUSA-TX65939-X

$timezone = "America/Chicago";  // Set to your local timezone
$lineUpID = "USA-TX65939-X";      // Set this to ID of the Line Up data you want to extract
$days     = 4;                   // Number of days worth of guide data to collect (8 days max)

//////////////////////////////////////////////////////////////////////////////////////////////////

// Setup filename for download
$fileDate = date ( "Ymd" );
header("Content-disposition: attachment; filename=xmltv.".$fileDate.".xml");
header("Content-type: text/xml");

// Build XMLTV data
$url = "http". ( !empty ( $_SERVER['HTTPS'] ) ? "s" : "" )."://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
$now = strtotime ( "now" );
$startTime = date ( 'Y-m-d\T00:00:00.000\Z', $now );

echo("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\r\n");
//echo("<!DOCTYPE tv SYSTEM \"xmltv.dtd\">\r\n");  // Jellyfin doesn't like this line now (20250418)
echo("<tv date=\"".$startTime."\" source-info-url=\"".$url."\" source-info-name=\"tvtv2xmltv\">\r\n");

// GET lineup data
$lineup_url = "https://www.tvtv.us/api/v1/lineup/".$lineUpID."/channels";
$json = file_get_contents( $lineup_url );
$lineup_data = json_decode( $json, true );

$all_channels = [];
foreach ( $lineup_data as &$channel )
{
    // Filter: Only proceed if the channelNumber (used as the XML 'id') is exactly "1000".
    if ( $channel["channelNumber"] <= "1000" ) {
        // Build channel query string
        $all_channels[] = $channel["stationId"];

        // Channel data
        echo("<channel id=\"".$channel["channelNumber"]."\">");
        echo("<display-name>".$channel["channelNumber"]."</display-name>");
        echo("<display-name>".$channel["stationCallSign"]."</display-name>");
        echo("<icon src=\"https://www.tvtv.us".$channel["logo"]."\" />");
        echo("</channel>\r\n");
        @ob_flush(); flush();
    } ### ADDED TO CLOSE SOLVE TOO MANY CHANNELS
}

// Get max 8 days of guide data starting today
if ( $days > 8 ) $days = 8;
for ( $day = 0; $day < $days; $day++)
{
    // GET guide data
    $now = strtotime ( "now + ".$day." day" );
    $end = strtotime ("now + ".($day + 1)." day" );
    $startTime = date ( 'Y-m-d\T04:00:00.000\Z', $now ); //"2023-05-23T04:00:00.000Z";
    $endTime = date ( 'Y-m-d\T03:59:00.000\Z', $end ); //"2023-05-24T03:59:00.000Z";

    // Load listing data in batches of 20 channels max; more than that will trigger a Cloudflare block
    $listing_data = [];
    for ($i = 0; $i <= count($all_channels); $i += 20) {
        $channels = array_slice($all_channels, $i, 20);
        $listing_url = "https://www.tvtv.us/api/v1/lineup/".$lineUpID."/grid/".$startTime."/".$endTime."/".implode(',', $channels);
        $json = file_get_contents( $listing_url );
        $listing_data = array_merge($listing_data, json_decode( $json, true ));
    }

    $index = 0;

    foreach ( $lineup_data as &$channel )
    {
      ### ADDED LINE TO SOLVE TOO MANY CHANNELS
      if ( $channel["channelNumber"] <= "1000" ) {

	    // Program Data
        foreach ( $listing_data[$index] as &$program )
        {
            $programId = htmlspecialchars ( $program['programId'], ENT_XML1, 'UTF-8' );
            $title = htmlspecialchars ( $program['title'], ENT_XML1, 'UTF-8' );
            $subtitle = @htmlspecialchars ( $program['subtitle'], ENT_XML1, 'UTF-8' );
            $flags = implode ( ", ", $program['flags'] );
            $type = htmlspecialchars ( $program['type'], ENT_XML1, 'UTF-8' );
            $startTime = htmlspecialchars ( $program['startTime'], ENT_XML1, 'UTF-8' );
            $start = htmlspecialchars ( $program['start'], ENT_XML1, 'UTF-8' );
            $duration = htmlspecialchars ( $program['duration'], ENT_XML1, 'UTF-8' );
            $runTime = htmlspecialchars ( $program['runTime'], ENT_XML1, 'UTF-8' );

            $tStart = new DateTime($startTime);
            $tStart->setTimeZone(new DateTimeZone($timezone));
            $startTime = $tStart->format("YmdHis O");
            $tStart->add(new DateInterval('PT'.$program['runTime'].'M'));
            $endTime = $tStart->format("YmdHis O");

            echo("<programme start=\"".$startTime."\" stop=\"".$endTime."\" duration=\"".$duration."\" channel=\"".$channel["channelNumber"]."\">");
            echo("<title lang=\"en\">".$title."</title>");
            echo("<sub-title lang=\"en\">".$subtitle."</sub-title>");

            if ( $type == "M" )
                echo("<category lang=\"en\">movie</category>");

            if ( $type == "N" )
                echo("<category lang=\"en\">news</category>");

            if ( $type == "S" )
                echo("<category lang=\"en\">sports</category>");

            if ( strstr($flags, "EI") )
                echo("<category lang=\"en\">kids</category>");

            if ( strstr($flags, "HD") )
            {
                echo("<video>");
                echo("<quality>HDTV</quality>");
                echo("</video>");
            }

            if ( strstr($flags, "Stereo") )
            {
                echo("<audio>");
                echo("<stereo>stereo</stereo>");
                echo("</audio>");
            }

            if ( strstr($flags, "New") )
            {
                echo("<new />");
            }

            echo("</programme>\r\n");
            @ob_flush(); flush();
        }
      } ### ADDED TO CLOSE SOLVE TOO MANY CHANNELS
        $index++;
    }
}

echo("</tv>");
  1. I automate the generation with a bash script / cronjob
$ cat /var/www/html/guides/otpchannels.sh
#!/bin/bash
### hit rate-limits on the broader uverse channel guide so watch for that
( cd /var/www/html/guides ; for ZIP in 78163; do php uverse-${ZIP}.php | tee uverse-${ZIP}.xml; done )
$ crontab -l
# run at 2:05am every 2 days which generates guides for multiple stations
5  2 1-31/2 * * bash /var/www/html/guides/otpchannels.sh
  1. If done right, should be able to navigate to the XML in the browser http://<nginxserver>/<webroot>/uverse-<zip>.xml

  2. Add EPG to Dispatcharr

  3. I dont auto-sync channels. I manually assign the channels their EPG number based on uverse (in dispatcharr). It's time consuming but worth it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment