Skip to content

Commit

Permalink
Checkpoint before redoing Lineup
Browse files Browse the repository at this point in the history
  • Loading branch information
robbiet480 committed Aug 15, 2018
1 parent d7f66fa commit 952cbc7
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 117 deletions.
72 changes: 47 additions & 25 deletions lineup.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"strings"
"time"

"github.com/spf13/viper"
"github.com/tombowditch/telly/m3u"
"github.com/tombowditch/telly/providers"
"github.com/tombowditch/telly/xmltv"
)

Expand Down Expand Up @@ -74,7 +76,7 @@ func (p *Playlist) Filter() error {
return unmarshalErr
}

if opts.Regex.MatchString(track.Raw) == opts.RegexInclusive {
if GetStringAsRegex("filter.regexstr").MatchString(track.Raw) == viper.GetBool("filter.regexinclusive") {
p.Tracks = append(p.Tracks, track)
}
}
Expand Down Expand Up @@ -105,6 +107,8 @@ type HDHomeRunChannel struct {

// Lineup is a collection of tracks
type Lineup struct {
Providers []providers.Provider

Playlists []Playlist
PlaylistsCount int
TracksCount int
Expand All @@ -128,32 +132,46 @@ type Lineup struct {
}

// NewLineup returns a new Lineup for the given config struct.
func NewLineup(opts config) *Lineup {
func NewLineup() *Lineup {
tv := xmltv.TV{
GeneratorInfoName: namespaceWithVersion,
GeneratorInfoURL: "https://github.com/tombowditch/telly",
}

lineup := &Lineup{
xmlTVChannelNumbers: opts.XMLTVChannelNumbers,
xmlTVChannelNumbers: viper.GetBool("iptv.xmltv-channels"),
chanNumToURLMap: make(map[string]string),
xmlTv: tv,
xmlTvChannelMap: make(map[string]xmlTVChannel),
StartingChannelNumber: opts.StartingChannel,
channelNumber: opts.StartingChannel,
StartingChannelNumber: viper.GetInt("iptv.starting-channel"),
channelNumber: viper.GetInt("iptv.starting-channel"),
Refreshing: false,
LastRefreshed: time.Now(),
}

var cfgs []providers.Configuration

if unmarshalErr := viper.UnmarshalKey("source", &cfgs); unmarshalErr != nil {
log.WithError(unmarshalErr).Panicln("Unable to unmarshal source configuration to slice of providers.Configuration, check your configuration!")
}

for _, cfg := range cfgs {
log.Infoln("Adding provider", cfg.Name)
provider, providerErr := cfg.GetProvider()
if providerErr != nil {
panic(providerErr)
}
if addErr := lineup.AddProvider(provider); addErr != nil {
log.WithError(addErr).Panicln("error adding new provider to lineup")
}
}

return lineup
}

// AddPlaylist adds a new playlist to the Lineup.
func (l *Lineup) AddPlaylist(plist string) error {
// Attempt to split the string by semi colon for complex config passing with m3uPath,xmlPath,name
splitStr := strings.Split(plist, ";")
path := splitStr[0]
reader, info, readErr := l.getM3U(path)
// AddProvider adds a new Provider to the Lineup.
func (l *Lineup) AddProvider(provider providers.Provider) error {
reader, info, readErr := l.getM3U(provider.PlaylistURL())
if readErr != nil {
log.WithError(readErr).Errorln("error getting m3u")
return readErr
Expand All @@ -165,8 +183,8 @@ func (l *Lineup) AddPlaylist(plist string) error {
return err
}

if len(splitStr) > 1 {
epg, epgReadErr := l.getXMLTV(splitStr[1])
if provider.EPGURL() != "" {
epg, epgReadErr := l.getXMLTV(provider.EPGURL())
if epgReadErr != nil {
log.WithError(epgReadErr).Errorln("error getting XMLTV")
return epgReadErr
Expand All @@ -182,7 +200,7 @@ func (l *Lineup) AddPlaylist(plist string) error {
}
}

playlist, playlistErr := l.NewPlaylist(rawPlaylist, info, (len(splitStr) > 1))
playlist, playlistErr := l.NewPlaylist(provider, rawPlaylist, info)
if playlistErr != nil {
return playlistErr
}
Expand All @@ -196,7 +214,8 @@ func (l *Lineup) AddPlaylist(plist string) error {
}

// NewPlaylist will return a new and filtered Playlist for the given m3u.Playlist and M3UFile.
func (l *Lineup) NewPlaylist(rawPlaylist *m3u.Playlist, info *M3UFile, hasEPG bool) (Playlist, error) {
func (l *Lineup) NewPlaylist(provider providers.Provider, rawPlaylist *m3u.Playlist, info *M3UFile) (Playlist, error) {
hasEPG := provider.EPGURL() != ""
playlist := Playlist{rawPlaylist, info, nil, nil, len(rawPlaylist.Tracks), 0, hasEPG}

if filterErr := playlist.Filter(); filterErr != nil {
Expand All @@ -205,7 +224,7 @@ func (l *Lineup) NewPlaylist(rawPlaylist *m3u.Playlist, info *M3UFile, hasEPG bo
}

for idx, track := range playlist.Tracks {
tt, channelNumber, hd, ttErr := l.processTrack(hasEPG, track)
tt, channelNumber, hd, ttErr := l.processTrack(provider, track)
if ttErr != nil {
return playlist, ttErr
}
Expand All @@ -224,7 +243,7 @@ func (l *Lineup) NewPlaylist(rawPlaylist *m3u.Playlist, info *M3UFile, hasEPG bo
hdhr := HDHomeRunChannel{
GuideNumber: channelNumber,
GuideName: guideName,
URL: fmt.Sprintf("http://%s/auto/v%d", opts.BaseAddress.String(), channelNumber),
URL: fmt.Sprintf("http://%s/auto/v%d", viper.GetString("web.base-address"), channelNumber),
HD: convertibleBoolean(hd),
DRM: convertibleBoolean(false),
}
Expand Down Expand Up @@ -253,9 +272,11 @@ func (l *Lineup) NewPlaylist(rawPlaylist *m3u.Playlist, info *M3UFile, hasEPG bo
return playlist, nil
}

func (l Lineup) processTrack(hasEPG bool, track Track) (*Track, int, bool, error) {
func (l Lineup) processTrack(provider providers.Provider, track Track) (*Track, int, bool, error) {

hd := hdRegex.MatchString(strings.ToLower(track.Track.Raw))
channelNumber := l.channelNumber

if xmlChan, ok := l.xmlTvChannelMap[track.TvgID]; ok {
log.Debugln("found an entry in xmlTvChannelMap for", track.Name)
if l.xmlTVChannelNumbers && xmlChan.Number != 0 {
Expand Down Expand Up @@ -308,11 +329,12 @@ func (l Lineup) Refresh() error {
l.FilteredTracksCount = 0
l.StartingChannelNumber = 0

for _, playlist := range existingPlaylists {
if addErr := l.AddPlaylist(playlist.M3UFile.Path); addErr != nil {
return addErr
}
}
// FIXME: Re-implement AddProvider to use a provider.
// for _, playlist := range existingPlaylists {
// if addErr := l.AddProvider(playlist.M3UFile.Path); addErr != nil {
// return addErr
// }
// }

log.Infoln("Done refreshing the lineup!")

Expand Down Expand Up @@ -428,7 +450,7 @@ func (l *Lineup) processXMLTV(tv *xmltv.TV) (map[string]xmlTVChannel, error) {
}
sort.StringSlice(displayNames).Sort()
for i := 0; i < 10; i++ {
iterateDisplayNames(displayNames, xTVChan)
extractDisplayNames(displayNames, xTVChan)
}
channelMap[xTVChan.ID] = *xTVChan
// Duplicate this to first display-name just in case the M3U and XMLTV differ significantly.
Expand All @@ -440,7 +462,7 @@ func (l *Lineup) processXMLTV(tv *xmltv.TV) (map[string]xmlTVChannel, error) {
return channelMap, nil
}

func iterateDisplayNames(displayNames []string, xTVChan *xmlTVChannel) {
func extractDisplayNames(displayNames []string, xTVChan *xmlTVChannel) {
for _, displayName := range displayNames {
if channelNumberRegex(displayName) {
if chanNum, chanNumErr := strconv.Atoi(displayName); chanNumErr == nil {
Expand Down
115 changes: 73 additions & 42 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package main

import (
"encoding/json"
fflag "flag"
"fmt"
"net"
"os"
"regexp"
"strings"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/version"
"github.com/sirupsen/logrus"
kingpin "gopkg.in/alecthomas/kingpin.v2"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
)

var (
Expand Down Expand Up @@ -53,76 +57,103 @@ var (
func main() {

// Discovery flags
kingpin.Flag("discovery.device-id", "8 digits used to uniquely identify the device. $(TELLY_DISCOVERY_DEVICE_ID)").Envar("TELLY_DISCOVERY_DEVICE_ID").Default("12345678").IntVar(&opts.DeviceID)
kingpin.Flag("discovery.device-friendly-name", "Name exposed via discovery. Useful if you are running two instances of telly and want to differentiate between them $(TELLY_DISCOVERY_DEVICE_FRIENDLY_NAME)").Envar("TELLY_DISCOVERY_DEVICE_FRIENDLY_NAME").Default("telly").StringVar(&opts.FriendlyName)
kingpin.Flag("discovery.device-auth", "Only change this if you know what you're doing $(TELLY_DISCOVERY_DEVICE_AUTH)").Envar("TELLY_DISCOVERY_DEVICE_AUTH").Default("telly123").Hidden().StringVar(&opts.DeviceAuth)
kingpin.Flag("discovery.device-manufacturer", "Manufacturer exposed via discovery. $(TELLY_DISCOVERY_DEVICE_MANUFACTURER)").Envar("TELLY_DISCOVERY_DEVICE_MANUFACTURER").Default("Silicondust").StringVar(&opts.Manufacturer)
kingpin.Flag("discovery.device-model-number", "Model number exposed via discovery. $(TELLY_DISCOVERY_DEVICE_MODEL_NUMBER)").Envar("TELLY_DISCOVERY_DEVICE_MODEL_NUMBER").Default("HDTC-2US").StringVar(&opts.ModelNumber)
kingpin.Flag("discovery.device-firmware-name", "Firmware name exposed via discovery. $(TELLY_DISCOVERY_DEVICE_FIRMWARE_NAME)").Envar("TELLY_DISCOVERY_DEVICE_FIRMWARE_NAME").Default("hdhomeruntc_atsc").StringVar(&opts.FirmwareName)
kingpin.Flag("discovery.device-firmware-version", "Firmware version exposed via discovery. $(TELLY_DISCOVERY_DEVICE_FIRMWARE_VERSION)").Envar("TELLY_DISCOVERY_DEVICE_FIRMWARE_VERSION").Default("20150826").StringVar(&opts.FirmwareVersion)
kingpin.Flag("discovery.ssdp", "Turn on SSDP announcement of telly to the local network $(TELLY_DISCOVERY_SSDP)").Envar("TELLY_DISCOVERY_SSDP").Default("true").BoolVar(&opts.SSDP)
flag.Int("discovery.device-id", 12345678, "8 digits used to uniquely identify the device. $(TELLY_DISCOVERY_DEVICE_ID)")
flag.String("discovery.device-friendly-name", "telly", "Name exposed via discovery. Useful if you are running two instances of telly and want to differentiate between them $(TELLY_DISCOVERY_DEVICE_FRIENDLY_NAME)")
flag.String("discovery.device-auth", "telly123", "Only change this if you know what you're doing $(TELLY_DISCOVERY_DEVICE_AUTH)")
flag.String("discovery.device-manufacturer", "Silicondust", "Manufacturer exposed via discovery. $(TELLY_DISCOVERY_DEVICE_MANUFACTURER)")
flag.String("discovery.device-model-number", "HDTC-2US", "Model number exposed via discovery. $(TELLY_DISCOVERY_DEVICE_MODEL_NUMBER)")
flag.String("discovery.device-firmware-name", "hdhomeruntc_atsc", "Firmware name exposed via discovery. $(TELLY_DISCOVERY_DEVICE_FIRMWARE_NAME)")
flag.String("discovery.device-firmware-version", "20150826", "Firmware version exposed via discovery. $(TELLY_DISCOVERY_DEVICE_FIRMWARE_VERSION)")
flag.Bool("discovery.ssdp", true, "Turn on SSDP announcement of telly to the local network $(TELLY_DISCOVERY_SSDP)")

// Regex/filtering flags
kingpin.Flag("filter.regex-inclusive", "Whether the provided regex is inclusive (whitelisting) or exclusive (blacklisting). If true (--filter.regex-inclusive), only channels matching the provided regex pattern will be exposed. If false (--no-filter.regex-inclusive), only channels NOT matching the provided pattern will be exposed. $(TELLY_FILTER_REGEX_INCLUSIVE)").Envar("TELLY_FILTER_REGEX_INCLUSIVE").Default("false").BoolVar(&opts.RegexInclusive)
kingpin.Flag("filter.regex", "Use regex to filter for channels that you want. A basic example would be .*UK.*. $(TELLY_FILTER_REGEX)").Envar("TELLY_FILTER_REGEX").Default(".*").RegexpVar(&opts.Regex)
flag.Bool("filter.regex-inclusive", false, "Whether the provided regex is inclusive (whitelisting) or exclusive (blacklisting). If true (--filter.regex-inclusive), only channels matching the provided regex pattern will be exposed. If false (--no-filter.regex-inclusive), only channels NOT matching the provided pattern will be exposed. $(TELLY_FILTER_REGEX_INCLUSIVE)")
flag.String("filter.regex", ".*", "Use regex to filter for channels that you want. A basic example would be .*UK.*. $(TELLY_FILTER_REGEX)")

// Web flags
kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry $(TELLY_WEB_LISTEN_ADDRESS)").Envar("TELLY_WEB_LISTEN_ADDRESS").Default("localhost:6077").TCPVar(&opts.ListenAddress)
kingpin.Flag("web.base-address", "The address to expose via discovery. Useful with reverse proxy $(TELLY_WEB_BASE_ADDRESS)").Envar("TELLY_WEB_BASE_ADDRESS").Default("localhost:6077").TCPVar(&opts.BaseAddress)
flag.String("web.listen-address", "localhost:6077", "Address to listen on for web interface and telemetry $(TELLY_WEB_LISTEN_ADDRESS)")
flag.String("web.base-address", "localhost:6077", "The address to expose via discovery. Useful with reverse proxy $(TELLY_WEB_BASE_ADDRESS)")

// Log flags
kingpin.Flag("log.level", "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal] $(TELLY_LOG_LEVEL)").Envar("TELLY_LOG_LEVEL").Default(logrus.InfoLevel.String()).StringVar(&opts.LogLevel)
kingpin.Flag("log.requests", "Log HTTP requests $(TELLY_LOG_REQUESTS)").Envar("TELLY_LOG_REQUESTS").Default("false").BoolVar(&opts.LogRequests)
flag.String("log.level", logrus.InfoLevel.String(), "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal] $(TELLY_LOG_LEVEL)")
flag.Bool("log.requests", false, "Log HTTP requests $(TELLY_LOG_REQUESTS)")

// IPTV flags
kingpin.Flag("iptv.playlist", "Path to an M3U file and optionally, a XMLTV file. Combine both strings with a semi-colon (;) for this functionality. Paths can be on disk or a URL. This flag can be used multiple times. $(TELLY_IPTV_PLAYLIST)").Envar("TELLY_IPTV_PLAYLIST").Default("iptv.m3u").StringsVar(&opts.Playlists)
kingpin.Flag("iptv.streams", "Number of concurrent streams allowed $(TELLY_IPTV_STREAMS)").Envar("TELLY_IPTV_STREAMS").Default("1").IntVar(&opts.ConcurrentStreams)
kingpin.Flag("iptv.starting-channel", "The channel number to start exposing from. $(TELLY_IPTV_STARTING_CHANNEL)").Envar("TELLY_IPTV_STARTING_CHANNEL").Default("10000").IntVar(&opts.StartingChannel)
kingpin.Flag("iptv.xmltv-channels", "Use channel numbers discovered via XMLTV file, if provided. $(TELLY_IPTV_XMLTV_CHANNELS)").Envar("TELLY_IPTV_XMLTV_CHANNELS").Default("true").BoolVar(&opts.XMLTVChannelNumbers)

kingpin.Version(version.Print("telly"))
kingpin.HelpFlag.Short('h')
kingpin.Parse()
flag.String("iptv.playlist", "", "Path to an M3U file on disk or at a URL. $(TELLY_IPTV_PLAYLIST)")
flag.Int("iptv.streams", 1, "Number of concurrent streams allowed $(TELLY_IPTV_STREAMS)")
flag.Int("iptv.starting-channel", 10000, "The channel number to start exposing from. $(TELLY_IPTV_STARTING_CHANNEL)")
flag.Bool("iptv.xmltv-channels", true, "Use channel numbers discovered via XMLTV file, if provided. $(TELLY_IPTV_XMLTV_CHANNELS)")

flag.CommandLine.AddGoFlagSet(fflag.CommandLine)
flag.Parse()
viper.BindPFlags(flag.CommandLine)
viper.SetConfigName("telly.config") // name of config file (without extension)
viper.AddConfigPath("/etc/telly/") // path to look for the config file in
viper.AddConfigPath("$HOME/.telly") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
viper.SetEnvPrefix(namespace)
viper.AutomaticEnv()
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
log.WithError(err).Panicln("fatal error while reading config file:")
}
}

log.Infoln("Starting telly", version.Info())
log.Infoln("Build context", version.BuildContext())

prometheus.MustRegister(version.NewCollector("telly"), exposedChannels)

level, parseLevelErr := logrus.ParseLevel(opts.LogLevel)
level, parseLevelErr := logrus.ParseLevel(viper.GetString("log.level"))
if parseLevelErr != nil {
log.WithError(parseLevelErr).Panicln("error setting log level!")
}
log.SetLevel(level)

opts.FriendlyName = fmt.Sprintf("HDHomerun (%s)", opts.FriendlyName)
opts.DeviceUUID = fmt.Sprintf("%d-AE2A-4E54-BBC9-33AF7D5D6A92", opts.DeviceID)
if log.Level == logrus.DebugLevel {
js, _ := json.MarshalIndent(viper.AllSettings(), "", " ")
log.Debugf("Loaded configuration %s", js)
}

if opts.BaseAddress.IP.IsUnspecified() {
log.Panicln("base URL is set to 0.0.0.0, this will not work. please use the --web.base-address option and set it to the (local) ip address telly is running on.")
if viper.IsSet("filter.regexstr") {
if _, regexErr := regexp.Compile(viper.GetString("filter.regex")); regexErr != nil {
log.WithError(regexErr).Panicln("Error when compiling regex, is it valid?")
}
}

if opts.ListenAddress.IP.IsUnspecified() && opts.BaseAddress.IP.IsLoopback() {
log.Warnln("You are listening on all interfaces but your base URL is localhost (meaning Plex will try and load localhost to access your streams) - is this intended?")
var addrErr error
if _, addrErr = net.ResolveTCPAddr("tcp", viper.GetString("web.listenaddress")); addrErr != nil {
log.WithError(addrErr).Panic("Error when parsing Listen address, please check the address and try again.")
return
}
if _, addrErr = net.ResolveTCPAddr("tcp", viper.GetString("web.base-address")); addrErr != nil {
log.WithError(addrErr).Panic("Error when parsing Base addresses, please check the address and try again.")
return
}

if len(opts.Playlists) == 1 && opts.Playlists[0] == "iptv.m3u" {
log.Warnln("using default m3u option, 'iptv.m3u'. launch telly with the --iptv.playlist=yourfile.m3u option to change this!")
if GetTCPAddr("web.base-address").IP.IsUnspecified() {
log.Panicln("base URL is set to 0.0.0.0, this will not work. please use the --web.baseaddress option and set it to the (local) ip address telly is running on.")
}

opts.lineup = NewLineup(opts)
if GetTCPAddr("web.listenaddress").IP.IsUnspecified() && GetTCPAddr("web.base-address").IP.IsLoopback() {
log.Warnln("You are listening on all interfaces but your base URL is localhost (meaning Plex will try and load localhost to access your streams) - is this intended?")
}

for _, playlistPath := range opts.Playlists {
if addErr := opts.lineup.AddPlaylist(playlistPath); addErr != nil {
log.WithError(addErr).Panicln("error adding new playlist to lineup")
}
viper.Set("discovery.device-friendly-name", fmt.Sprintf("HDHomerun (%s)", viper.GetString("discovery.device-friendly-name")))
viper.Set("discovery.device-uuid", fmt.Sprintf("%d-AE2A-4E54-BBC9-33AF7D5D6A92", viper.GetInt("discovery.device-id")))

if flag.Lookup("iptv.playlist").Changed {
viper.Set("playlists.default.m3u", flag.Lookup("iptv.playlist").Value.String())
}

log.Infof("Loaded %d channels into the lineup", opts.lineup.FilteredTracksCount)
lineup := NewLineup()

log.Infof("Loaded %d channels into the lineup", lineup.FilteredTracksCount)

// if opts.lineup.FilteredTracksCount > 420 {
// log.Panicln("telly has loaded more than 420 channels into the lineup. Plex does not deal well with more than this amount and will more than likely hang when trying to fetch channels. You must use regular expressions to filter out channels. You can also start another Telly instance.")
// }
if lineup.FilteredTracksCount > 420 {
log.Panicf("telly has loaded more than 420 channels (%d) into the lineup. Plex does not deal well with more than this amount and will more than likely hang when trying to fetch channels. You must use regular expressions to filter out channels. You can also start another Telly instance.", lineup.FilteredTracksCount)
}

serve(opts)
serve(lineup)
}
4 changes: 4 additions & 0 deletions providers/eternal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package providers

// M3U:http://live.eternaltv.net:25461/get.php?username=xxxxxxx&password=xxxxxx&output=ts&type=m3u_plus
// XMLTV: http://live.eternaltv.net:25461/xmltv.php?username=xxxxx&password=xxxxx&type=m3u_plus&output=ts
4 changes: 4 additions & 0 deletions providers/hellraiser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package providers

// Playlist URL: http://liquidit.info:8080/get.php?username=xxxx&password=xxxxxxx&type=m3u_plus&output=ts
// XMLTV URL: http://liquidit.info:8080/xmltv.php?username=xxxxxx&password=xxxxxx
4 changes: 4 additions & 0 deletions providers/iptv-epg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package providers

// M3U: http://iptv-epg.com/<random string>.m3u
// XMLTV: http://iptv-epg.com/<random string>.xml
Loading

0 comments on commit 952cbc7

Please sign in to comment.