Esempio n. 1
0
        private static void ProcessMd5ScheduleEntry(string md5)
        {
            // ensure cached file exists
            if (!epgCache.JsonFiles.ContainsKey(md5))
            {
                return;
            }

            // read the cached file
            ScheduleResponse schedule;

            try
            {
                using (var reader = new StringReader(epgCache.GetAsset(md5)))
                {
                    var serializer = new JsonSerializer();
                    schedule = (ScheduleResponse)serializer.Deserialize(reader, typeof(ScheduleResponse));
                    if (schedule == null)
                    {
                        Logger.WriteError("Failed to read Md5Schedule file in cache directory.");
                        return;
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.WriteError("Error occurred when trying to read Md5Schedule file in cache directory. Message: " + ex.Message);
                return;
            }

            // determine which service entry applies to
            var mxfService = SdMxf.GetService(schedule.StationId);

            // process each program schedule entry
            foreach (var scheduleProgram in schedule.Programs)
            {
                // prepopulate some of the program
                var mxfProgram = SdMxf.GetProgram(scheduleProgram.ProgramId);
                if (mxfProgram.extras.Count == 0)
                {
                    mxfProgram.ProgramId   = scheduleProgram.ProgramId;
                    mxfProgram.UidOverride = $"{scheduleProgram.ProgramId.Substring(0, 10)}_{scheduleProgram.ProgramId.Substring(10)}";
                    mxfProgram.extras.Add("md5", scheduleProgram.Md5);
                    if (scheduleProgram.Multipart?.PartNumber > 0)
                    {
                        mxfProgram.extras.Add("multipart", $"{scheduleProgram.Multipart.PartNumber}/{scheduleProgram.Multipart.TotalParts}");
                    }
                    if (config.OadOverride && scheduleProgram.New)
                    {
                        mxfProgram.extras.Add("newAirDate", scheduleProgram.AirDateTime.ToLocalTime());
                    }
                }
                mxfProgram.IsSeasonFinale   |= Helper.StringContains(scheduleProgram.IsPremiereOrFinale, "Season Finale");
                mxfProgram.IsSeasonPremiere |= Helper.StringContains(scheduleProgram.IsPremiereOrFinale, "Season Premiere");
                mxfProgram.IsSeriesFinale   |= Helper.StringContains(scheduleProgram.IsPremiereOrFinale, "Series Finale");
                mxfProgram.IsSeriesPremiere |= Helper.StringContains(scheduleProgram.IsPremiereOrFinale, "Series Premiere");
                if (!mxfProgram.extras.ContainsKey("premiere"))
                {
                    mxfProgram.extras.Add("premiere", false);
                }
                if (scheduleProgram.Premiere)
                {
                    mxfProgram.extras["premiere"] = true;                           // used only for movie and miniseries premieres
                }
                // grab any tvratings from desired countries
                var scheduleTvRatings = new Dictionary <string, string>();
                if (scheduleProgram.Ratings != null)
                {
                    var ratings = config.RatingsOrigin.Split(',');
                    foreach (var rating in scheduleProgram.Ratings.Where(rating => string.IsNullOrEmpty(rating.Country) || Helper.TableContains(ratings, "ALL") || Helper.TableContains(ratings, rating.Country)))
                    {
                        scheduleTvRatings.Add(rating.Body, rating.Code);
                    }
                }

                // populate the schedule entry and create program entry as required
                mxfService.MxfScheduleEntries.ScheduleEntry.Add(new MxfScheduleEntry
                {
                    AudioFormat = EncodeAudioFormat(scheduleProgram.AudioProperties),
                    Duration    = scheduleProgram.Duration,
                    Is3D        = Helper.TableContains(scheduleProgram.VideoProperties, "3d"),
                    IsBlackout  = scheduleProgram.SubjectToBlackout,
                    IsClassroom = scheduleProgram.CableInTheClassroom,
                    IsCc        = Helper.TableContains(scheduleProgram.AudioProperties, "cc"),
                    IsDelay     = Helper.StringContains(scheduleProgram.LiveTapeDelay, "delay"),
                    IsDvs       = Helper.TableContains(scheduleProgram.AudioProperties, "dvs"),
                    IsEnhanced  = Helper.TableContains(scheduleProgram.VideoProperties, "enhanced"),
                    IsFinale    = Helper.StringContains(scheduleProgram.IsPremiereOrFinale, "finale"),
                    IsHdtv      = CheckHdOverride(schedule.StationId) || !CheckSdOverride(schedule.StationId) && Helper.TableContains(scheduleProgram.VideoProperties, "hdtv"),
                    //IsHdtvSimulCast = null,
                    IsInProgress = scheduleProgram.JoinedInProgress,
                    IsLetterbox  = Helper.TableContains(scheduleProgram.VideoProperties, "letterbox"),
                    IsLive       = Helper.StringContains(scheduleProgram.LiveTapeDelay, "live"),
                    //IsLiveSports = null,
                    IsPremiere  = scheduleProgram.Premiere || Helper.StringContains(scheduleProgram.IsPremiereOrFinale, "premiere"),
                    IsRepeat    = !scheduleProgram.New,
                    IsSap       = Helper.TableContains(scheduleProgram.AudioProperties, "sap"),
                    IsSubtitled = Helper.TableContains(scheduleProgram.AudioProperties, "subtitled"),
                    IsTape      = Helper.StringContains(scheduleProgram.LiveTapeDelay, "tape"),
                    Part        = scheduleProgram.Multipart?.PartNumber ?? 0,
                    Parts       = scheduleProgram.Multipart?.TotalParts ?? 0,
                    mxfProgram  = mxfProgram,
                    StartTime   = scheduleProgram.AirDateTime,
                    //TvRating is determined in the class itself to combine with the program content ratings
                    IsSigned = scheduleProgram.Signed
                });
                mxfService.MxfScheduleEntries.ScheduleEntry.Last().extras.Add("ratings", scheduleTvRatings);
            }
        }
Esempio n. 2
0
        private static bool GetMd5ScheduleEntries(string[] dates, int start)
        {
            // reject 0 requests
            if (SdMxf.With.Services.Count - start < 1)
            {
                return(true);
            }

            // build request for station schedules
            var requests = new ScheduleRequest[Math.Min(SdMxf.With.Services.Count - start, MaxQueries / dates.Length)];

            for (var i = 0; i < requests.Length; ++i)
            {
                requests[i] = new ScheduleRequest()
                {
                    StationId = SdMxf.With.Services[start + i].StationId,
                    Date      = dates
                };
            }

            // request schedule md5s from Schedules Direct
            var stationResponses = SdApi.GetScheduleMd5s(requests);

            if (stationResponses == null)
            {
                return(false);
            }

            // build request of daily schedules not downloaded yet
            var newRequests = new List <ScheduleRequest>();

            foreach (var request in requests)
            {
                var requestErrors = new Dictionary <int, string>();
                var mxfService    = SdMxf.GetService(request.StationId);
                if (stationResponses.TryGetValue(request.StationId, out var stationResponse))
                {
                    // if the station return is empty, go to next station
                    if (stationResponse.Count == 0)
                    {
                        var comment = $"Failed to parse the schedule Md5 return for stationId {mxfService.StationId} ({mxfService.CallSign}) on {dates[0]} and after.";
                        if (CheckSuppressWarnings(mxfService.CallSign))
                        {
                            Logger.WriteInformation(comment);
                        }
                        else
                        {
                            Logger.WriteWarning(comment);
                        }
                        processedObjects += dates.Length; ReportProgress();
                        continue;
                    }

                    // scan through all the dates returned for the station and request dates that are not cached
                    var newDateRequests = new List <string>();
                    var dupeMd5s        = new HashSet <string>();
                    foreach (var day in dates)
                    {
                        if (stationResponse.TryGetValue(day, out var dayResponse) && (dayResponse.Code == 0) && !string.IsNullOrEmpty(dayResponse.Md5))
                        {
                            var filepath = $"{Helper.Epg123CacheFolder}\\{SafeFilename(dayResponse.Md5)}";
                            var file     = new FileInfo(filepath);
                            if (file.Exists && (file.Length > 0) && !epgCache.JsonFiles.ContainsKey(dayResponse.Md5))
                            {
                                using (var reader = File.OpenText(filepath))
                                {
                                    epgCache.AddAsset(dayResponse.Md5, reader.ReadToEnd());
                                }
                            }

                            if (epgCache.JsonFiles.ContainsKey(dayResponse.Md5))
                            {
                                ++processedObjects; ReportProgress();
                                ++cachedSchedules;
                            }
                            else
                            {
                                newDateRequests.Add(day);
                            }

                            if (!ScheduleEntries.ContainsKey(dayResponse.Md5))
                            {
                                ScheduleEntries.Add(dayResponse.Md5, new[] { request.StationId, day });
                            }
                            else
                            {
                                var previous = ScheduleEntries[dayResponse.Md5][1];
                                var comment  = $"Duplicate schedule Md5 return for stationId {mxfService.StationId} ({mxfService.CallSign}) on {day} with {previous}.";
                                Logger.WriteWarning(comment);
                                dupeMd5s.Add(dayResponse.Md5);
                            }
                        }
                        else if ((dayResponse != null) && (dayResponse.Code != 0) && !requestErrors.ContainsKey(dayResponse.Code))
                        {
                            requestErrors.Add(dayResponse.Code, dayResponse.Message);
                        }
                    }

                    // clear out dupe entries
                    foreach (var dupe in dupeMd5s)
                    {
                        var previous = ScheduleEntries[dupe][1];
                        var comment  = $"Removing duplicate Md5 schedule entry for stationId {mxfService.StationId} ({mxfService.CallSign}) on {previous}.";
                        Logger.WriteWarning(comment);
                        ScheduleEntries.Remove(dupe);
                    }

                    // create the new request for the station
                    if (newDateRequests.Count > 0)
                    {
                        newRequests.Add(new ScheduleRequest()
                        {
                            StationId = request.StationId,
                            Date      = newDateRequests.ToArray()
                        });
                    }
                }
                else
                {
                    // requested station was not in response
                    Logger.WriteWarning($"Requested stationId {mxfService.StationId} ({mxfService.CallSign}) was not present in schedule Md5 response.");
                    processedObjects += dates.Length; ReportProgress();
                    continue;
                }

                if (requestErrors.Count <= 0)
                {
                    continue;
                }
                foreach (var keyValuePair in requestErrors)
                {
                    Logger.WriteError($"Requests for MD5 schedule entries of station {request.StationId} returned error code {keyValuePair.Key} , message: {keyValuePair.Value}");
                }
            }
            ReportProgress();

            // download the remaining daily schedules to the cache directory
            if (newRequests.Count > 0)
            {
                // request daily schedules from Schedules Direct
                var responses = SdApi.GetScheduleListings(newRequests.ToArray());
                if (responses == null)
                {
                    return(false);
                }

                // process the responses
                foreach (var response in responses)
                {
                    ++processedObjects; ReportProgress();
                    if (response?.Programs == null)
                    {
                        continue;
                    }
                    ++downloadedSchedules;

                    // serialize JSON directly to a file
                    if (ScheduleEntries.TryGetValue(response.Metadata.Md5, out var serviceDate))
                    {
                        using (var writer = new StringWriter())
                        {
                            try
                            {
                                var serializer = new JsonSerializer();
                                serializer.Serialize(writer, response);
                                epgCache.AddAsset(response.Metadata.Md5, writer.ToString());
                            }
                            catch
                            {
                                Logger.WriteInformation($"Failed to write station daily schedule file to cache file. station: {serviceDate[0]} ; date: {serviceDate[1]}");
                            }
                        }
                    }
                    else
                    {
                        try
                        {
                            var compare = ScheduleEntries
                                          .Where(arg => arg.Value[0].Equals(response.StationId))
                                          .Single(arg => arg.Value[1].Equals(response.Metadata.StartDate));
                            Logger.WriteWarning($"Md5 mismatch for station {compare.Value[0]} on {compare.Value[1]}. Expected: {compare.Key} - Downloaded {response.Metadata.Md5}");
                        }
                        catch
                        {
                            Logger.WriteWarning($"Md5 mismatch for station {response.StationId} on {response.Metadata.StartDate}. Downloaded {response.Metadata.Md5}");
                        }
                    }
                }
            }
            ReportProgress();
            return(true);
        }
Esempio n. 3
0
        private static bool BuildLineupServices()
        {
            // query what lineups client is subscribed to
            var clientLineups = SdApi.GetSubscribedLineups();

            if (clientLineups == null)
            {
                return(false);
            }

            // determine if there are custom lineups to consider
            if (File.Exists(Helper.Epg123CustomLineupsXmlPath))
            {
                CustomLineups customLineups;
                using (var stream = new StreamReader(Helper.Epg123CustomLineupsXmlPath, Encoding.Default))
                {
                    var        serializer = new XmlSerializer(typeof(CustomLineups));
                    TextReader reader     = new StringReader(stream.ReadToEnd());
                    customLineups = (CustomLineups)serializer.Deserialize(reader);
                    reader.Close();
                }

                foreach (var lineup in customLineups.CustomLineup.Where(lineup => config.IncludedLineup.Contains(lineup.Lineup)))
                {
                    customLineup = lineup;

                    clientLineups.Lineups.Add(new SubscribedLineup
                    {
                        Lineup    = lineup.Lineup,
                        Name      = lineup.Name,
                        Transport = "CUSTOM",
                        Location  = lineup.Location,
                        Uri       = "CUSTOM",
                        IsDeleted = false
                    });

                    customMap = new StationChannelMap
                    {
                        Map      = new List <LineupChannel>(),
                        Stations = new List <LineupStation>(),
                        Metadata = new LineupMetadata {
                            Lineup = lineup.Lineup
                        }
                    };
                }
            }

            // reset counters
            processedObjects = 0; totalObjects = clientLineups.Lineups.Count;
            ReportProgress();

            // process lineups
            Logger.WriteMessage($"Entering BuildLineupServices() for {clientLineups.Lineups.Count} lineups.");
            foreach (var clientLineup in clientLineups.Lineups)
            {
                var flagCustom = !string.IsNullOrEmpty(clientLineup.Uri) && clientLineup.Uri.Equals("CUSTOM");
                ++processedObjects; ReportProgress();

                // request the lineup's station maps
                StationChannelMap lineupMap = null;
                if (!flagCustom)
                {
                    lineupMap = SdApi.GetStationChannelMap(clientLineup.Lineup);
                    if (lineupMap == null)
                    {
                        continue;
                    }

                    foreach (var station in lineupMap.Stations.Where(station => !AllStations.ContainsKey(station.StationId)))
                    {
                        AllStations.Add(station.StationId, station);
                    }
                }

                if (!config.IncludedLineup.Contains(clientLineup.Lineup))
                {
                    Logger.WriteVerbose($"Subscribed lineup {clientLineup.Lineup} has been EXCLUDED from download and processing.");
                    continue;
                }
                if (clientLineup.IsDeleted)
                {
                    Logger.WriteWarning($"Subscribed lineup {clientLineup.Lineup} has been DELETED at the headend.");
                    continue;
                }
                if (flagCustom)
                {
                    foreach (var station in customLineup.Station.Where(station => station.StationId != null))
                    {
                        if (AllStations.TryGetValue(station.StationId, out var lineupStation))
                        {
                            customMap.Map.Add(new LineupChannel
                            {
                                StationId = station.StationId,
                                AtscMajor = station.Number,
                                AtscMinor = station.Subnumber,
                                MatchName = station.MatchName
                            });
                            CustomStations.Add(station.StationId);
                            customMap.Stations.Add(lineupStation);
                        }
                        else if (!string.IsNullOrEmpty(station.Alternate) && AllStations.TryGetValue(station.Alternate, out lineupStation))
                        {
                            customMap.Map.Add(new LineupChannel
                            {
                                StationId = station.Alternate,
                                AtscMajor = station.Number,
                                AtscMinor = station.Subnumber,
                                MatchName = station.MatchName
                            });
                            CustomStations.Add(station.Alternate);
                            customMap.Stations.Add(lineupStation);
                        }
                    }
                    lineupMap = customMap;
                    Logger.WriteVerbose($"Successfully retrieved the station mapping for lineup {clientLineup.Lineup}.");
                }
                if (lineupMap == null)
                {
                    return(false);
                }

                var lineupIndex = SdMxf.With.Lineups.Count;
                SdMxf.With.Lineups.Add(new MxfLineup
                {
                    Index    = lineupIndex + 1,
                    LineupId = clientLineup.Lineup,
                    Name     = $"EPG123 {clientLineup.Name} ({clientLineup.Location})"
                });

                // use hashset to make sure we don't duplicate channel entries for this station
                var channelNumbers = new HashSet <string>();

                // build the services and lineup
                foreach (var station in lineupMap.Stations)
                {
                    // check if station should be downloaded and processed
                    if (!flagCustom)
                    {
                        if (station == null || (ExcludedStations.Contains(station.StationId) && !CustomStations.Contains(station.StationId)))
                        {
                            continue;
                        }
                        if (!IncludedStations.Contains(station.StationId) && !config.AutoAddNew)
                        {
                            Logger.WriteWarning($"**** Lineup {clientLineup.Name} ({clientLineup.Location}) has added station {station.StationId} ({station.Callsign}). ****");
                            continue;
                        }
                    }

                    // build the service if necessary
                    var mxfService = SdMxf.GetService(station.StationId);
                    if (string.IsNullOrEmpty(mxfService.CallSign))
                    {
                        // instantiate stationLogo and override uid
                        StationImage stationLogo = null;
                        mxfService.UidOverride = $"EPG123_{station.StationId}";

                        // determine station name for ATSC stations
                        var atsc  = false;
                        var names = station.Name.Replace("-", "").Split(' ');
                        if (!string.IsNullOrEmpty(station.Affiliate) && names.Length == 2 && names[0] == station.Callsign && $"({names[0]})" == $"{names[1]}")
                        {
                            atsc = true;
                        }

                        // add callsign and station name
                        mxfService.CallSign = CheckCustomCallsign(station.StationId) ?? station.Callsign;
                        mxfService.Name     = CheckCustomServicename(station.StationId) ?? (atsc ? $"{station.Callsign} ({station.Affiliate})" : null) ?? station.Name;

                        // add affiliate if available
                        if (!string.IsNullOrEmpty(station.Affiliate))
                        {
                            mxfService.mxfAffiliate = SdMxf.GetAffiliate(station.Affiliate);
                        }

                        // add station logo if available and allowed
                        var logoPath    = $"{Helper.Epg123LogosFolder}\\{station.Callsign}.png";
                        var urlLogoPath = logoPath.Replace($"{Helper.Epg123LogosFolder}\\", $"http://{Environment.MachineName}:{Helper.TcpPort}/logos/");
                        var customPath  = $"{Helper.Epg123LogosFolder}\\{station.Callsign}_c.png";
                        if (config.IncludeSdLogos)
                        {
                            // make sure logos directory exists
                            if (!Directory.Exists(Helper.Epg123LogosFolder))
                            {
                                Directory.CreateDirectory(Helper.Epg123LogosFolder);
                            }

                            if (station.StationLogos != null)
                            {
                                stationLogo = station.StationLogos.FirstOrDefault(arg => arg.Category != null && arg.Category.Equals(config.PreferredLogoStyle, StringComparison.OrdinalIgnoreCase)) ??
                                              station.StationLogos.FirstOrDefault(arg => arg.Category != null && arg.Category.Equals(config.AlternateLogoStyle, StringComparison.OrdinalIgnoreCase));

                                if (stationLogo != null)
                                {
                                    switch (stationLogo.Category)
                                    {
                                    case "dark":
                                        logoPath = logoPath.Replace(".png", "_d.png");
                                        break;

                                    case "gray":
                                        logoPath = logoPath.Replace(".png", "_g.png");
                                        break;

                                    case "light":
                                        logoPath = logoPath.Replace(".png", "_l.png");
                                        break;

                                    case "white":
                                        logoPath = logoPath.Replace(".png", "_w.png");
                                        break;
                                    }
                                }
                            }
                            if (stationLogo == null && !config.PreferredLogoStyle.Equals("none", StringComparison.OrdinalIgnoreCase) && !config.AlternateLogoStyle.Equals("none", StringComparison.OrdinalIgnoreCase))
                            {
                                stationLogo = station.Logo;
                            }

                            // download the logo from SD if not present in the .\logos folder
                            if (stationLogo != null && !File.Exists(logoPath))
                            {
                                var url = stationLogo.Url;

                                // download, crop & resize logo image, save and add
                                if (!string.IsNullOrEmpty(url))
                                {
                                    StationLogosToDownload.Add(new KeyValuePair <MxfService, string[]>(mxfService, new[] { logoPath, url }));
                                }
                            }

                            // add the existing logo; custom logo overrides downloaded logos
                            if (File.Exists(customPath))
                            {
                                logoPath = customPath;
                            }
                            urlLogoPath = logoPath.Replace($"{Helper.Epg123LogosFolder}\\", $"http://{Environment.MachineName}:{Helper.TcpPort}/logos/");

                            if (File.Exists(logoPath))
                            {
                                mxfService.mxfGuideImage = SdMxf.GetGuideImage(Helper.Standalone ? $"file://{logoPath}" : urlLogoPath, GetStringEncodedImage(logoPath));
                            }
                        }

                        // handle xmltv logos
                        if (config.XmltvIncludeChannelLogos.Equals("url"))
                        {
                            if (stationLogo != null)
                            {
                                mxfService.extras.Add("logo", stationLogo);
                            }
                            else if (station.Logo?.Url != null)
                            {
                                mxfService.extras.Add("logo", station.Logo);
                            }
                        }
                        else if (config.XmltvIncludeChannelLogos.Equals("local") && config.IncludeSdLogos)
                        {
                            if (File.Exists(logoPath))
                            {
                                var image = Image.FromFile(logoPath);
                                mxfService.extras.Add("logo", new StationImage
                                {
                                    Url    = Helper.Standalone ? logoPath : urlLogoPath,
                                    Height = image.Height,
                                    Width  = image.Width
                                });
                            }
                            else if (stationLogo != null)
                            {
                                mxfService.extras.Add("logo", new StationImage
                                {
                                    Url = Helper.Standalone ? logoPath : urlLogoPath
                                });
                            }
                        }
                    }

                    // match station with mapping for lineup number and subnumbers
                    foreach (var map in lineupMap.Map)
                    {
                        var number    = -1;
                        var subnumber = 0;
                        if (!map.StationId.Equals(station.StationId))
                        {
                            continue;
                        }

                        // QAM
                        if (map.ChannelMajor != 0)
                        {
                            number    = map.ChannelMajor;
                            subnumber = map.ChannelMinor;
                        }

                        // ATSC (and CUSTOM) or NTSC
                        else if (map.AtscMajor != 0)
                        {
                            number    = map.AtscMajor;
                            subnumber = map.AtscMinor;
                        }
                        else if (map.UhfVhf != 0)
                        {
                            number = map.UhfVhf;
                        }

                        // Cable or Satellite
                        else if (!string.IsNullOrEmpty(map.Channel))
                        {
                            subnumber = 0;
                            if (Regex.Match(map.Channel, @"[A-Za-z]{1}[\d]{4}").Length > 0)
                            {
                                // 4dtv has channels starting with 2 character satellite identifier
                                number = int.Parse(map.Channel.Substring(2));
                            }
                            else if (!int.TryParse(Regex.Replace(map.Channel, "[^0-9.]", ""), out number))
                            {
                                // if channel number is not a whole number, must be a decimal number
                                var numbers = Regex.Replace(map.Channel, "[^0-9.]", "").Replace('_', '.').Replace('-', '.').Split('.');
                                if (numbers.Length == 2)
                                {
                                    number    = int.Parse(numbers[0]);
                                    subnumber = int.Parse(numbers[1]);
                                }
                            }
                        }

                        string matchName = null;
                        switch (clientLineup.Transport)
                        {
                        case "CUSTOM":
                            matchName = map.MatchName;
                            break;

                        case "DVB-S":
                            var m = Regex.Match(lineupMap.Metadata.Lineup, @"\d+\.\d+");
                            if (m.Success && map.FrequencyHz > 0 && map.NetworkId > 0 && map.TransportId > 0 && map.ServiceId > 0)
                            {
                                while (map.FrequencyHz > 13000)
                                {
                                    map.FrequencyHz /= 1000;
                                }
                                matchName = $"DVBS:{m.Value.Replace(".", "")}:{map.FrequencyHz}:{map.NetworkId}:{map.TransportId}:{map.ServiceId}";
                            }
                            number    = -1;
                            subnumber = 0;
                            break;

                        case "DVB-T":
                            if (map.NetworkId > 0 && map.TransportId > 0 && map.ServiceId > 0)
                            {
                                matchName = $"DVBT:{map.NetworkId}:{map.TransportId}:{map.ServiceId}";
                            }
                            break;

                        case "Antenna":
                            if (map.AtscMajor > 0 && map.AtscMinor > 0)
                            {
                                matchName = $"OC:{map.AtscMajor}:{map.AtscMinor}";
                            }
                            break;
                        }

                        var channelNumber = $"{number}{(subnumber > 0 ? $".{subnumber}" : "")}";
                        if (channelNumbers.Add($"{channelNumber}:{station.StationId}"))
                        {
                            SdMxf.With.Lineups[lineupIndex].channels.Add(new MxfChannel
                            {
                                mxfLineup  = SdMxf.With.Lineups[lineupIndex],
                                mxfService = mxfService,
                                Number     = number,
                                SubNumber  = subnumber,
                                MatchName  = matchName
                            });
                        }
                    }
                }
            }

            if (StationLogosToDownload.Count > 0)
            {
                StationLogosDownloadComplete = false;
                Logger.WriteInformation($"Kicking off background worker to download and process {StationLogosToDownload.Count} station logos.");
                BackgroundDownloader                            = new System.ComponentModel.BackgroundWorker();
                BackgroundDownloader.DoWork                    += BackgroundDownloader_DoWork;
                BackgroundDownloader.RunWorkerCompleted        += BackgroundDownloader_RunWorkerCompleted;
                BackgroundDownloader.WorkerSupportsCancellation = true;
                BackgroundDownloader.RunWorkerAsync();
            }

            if (SdMxf.With.Services.Count > 0)
            {
                // report specific stations that are no longer available
                var missing = (from station in IncludedStations where SdMxf.With.Services.FirstOrDefault(arg => arg.StationId.Equals(station)) == null select config.StationId.Single(arg => arg.StationId.Equals(station)).CallSign).ToList();
                if (missing.Count > 0)
                {
                    MissingStations = missing.Count;
                    Logger.WriteInformation($"Stations no longer available since last configuration save are: {string.Join(", ", missing)}");
                }

                Logger.WriteMessage("Exiting BuildLineupServices(). SUCCESS.");
                return(true);
            }

            Logger.WriteError($"There are 0 stations queued for download from {clientLineups.Lineups.Count} subscribed lineups. Exiting.");
            Logger.WriteError("Check that lineups are 'INCLUDED' and stations are selected in the EPG123 GUI.");
            return(false);
        }