using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; using ChanSort.Api; namespace ChanSort.Loader.Loewe; /* * The XML used in by this loader seems like a transformed version of the SQLite3 database format used by the Hisense/Loewe loader * * * The 2017 Hisense / Loewe data model for channel lists is a bit different than all other supported models and need some workarounds to be supported. * It is based on a flat "Services" table which doesn't hold program numbers and a FavoritesList/FavoritesItem table to assign numbers * to physical tuner lists and user favorite lists alike. * * Physical channel lists (e.g. for $av, Astra, Hot Bird) have their own ChannelList in the channelList dictionary and use * ChannelInfo.NewProgramNr to hold the program number. This doesn't allow the user to add services from other lists. * * The user favorite lists (FAV1-FAV4) use the separate favList ChannelList filled with all services from all physical lists. * ChannelInfo.FavIndex[0-3] holds the information for the program numbers in FAV1-4. The value -1 is used to indicate "not included". * * The $all list is hidden from the user and automatically updated to match the contents of all other lists (except $av and FAV1-4). * * The $av list is hidden from the user and not updated at all. * * This loader poses the following restrictions on the database: * - a service must not appear in more than one physical channel list ($all and FAV1-4 are not part of this restriction) * - a service can't appear more than once in any list * */ class Serializer : SerializerBase { private XmlDocument doc; private readonly StringBuilder fileInfo = new(); private readonly ChannelList mixedFavTv; private readonly ChannelList mixedFavRadio; /// /// Fields of the ChannelInfo that will be shown in the UI /// private static readonly List ColumnNames = new() { "OldPosition", "Position", "Source", "NewProgramNr", "Name", "ShortName", "Favorites", "Skip", "Lock", "Hidden", "Encrypted", "FreqInMhz", "OriginalNetworkId", "TransportStreamId", "ServiceId", //"ServiceType", "ServiceTypeName", "NetworkName", "Satellite" // "SymbolRate" }; /// /// mapping of FavoriteList.Pid => ChannelList. /// This dict does not include real user favorite lists (FAV1-FAV4). /// private readonly Dictionary channelLists = new(); /// /// mapping of FavoriteList.Pid for $all and FAV1-4 => index of the internal favorite list within userFavList (0-3) /// Pids that don't belong to the FAV1-4 are not included in this dictionary. /// private readonly Dictionary favListIdToFavIndex = new(); /// /// mapping of Service.Pid => ChannelInfo /// private readonly Dictionary channelsById = new(); /// /// FavoriteList.Pid of the $all list /// private int pidAll; /// /// FavoriteList.Pid of the $av list /// private int pidAv; #region ctor() public Serializer(string inputFile) : base(inputFile) { Features.ChannelNameEdit = ChannelNameEditMode.All; Features.DeleteMode = DeleteMode.NotSupported; Features.CanSkipChannels = true; Features.CanLockChannels = true; Features.CanHideChannels = true; Features.CanHaveGaps = true; Features.FavoritesMode = FavoritesMode.MixedSource; this.mixedFavTv = new ChannelList(0, "TV"); this.mixedFavTv.VisibleColumnFieldNames = ColumnNames; this.mixedFavTv.IsMixedSourceFavoritesList = true; this.mixedFavRadio = new ChannelList(0, "Radio"); this.mixedFavRadio.VisibleColumnFieldNames = ColumnNames; this.mixedFavRadio.IsMixedSourceFavoritesList = true; } #endregion #region Load() public override void Load() { this.doc = new XmlDocument(); doc.Load(this.FileName); var sl = doc["servicelist"]; if (sl == null) throw LoaderException.TryNext("expected root element "); var tuners = sl["tuners"]; if (tuners == null) throw LoaderException.TryNext("missing list"); var services = sl["services"]; if (services == null) throw LoaderException.TryNext("missing list"); var favorites = sl["favorites"]; if (favorites == null) throw LoaderException.TryNext("missing list"); LoadTuners(tuners); LoadServices(services); LoadFavorites(favorites); Features.MaxFavoriteLists = this.favListIdToFavIndex.Count; this.channelLists.Add(0, mixedFavTv); this.channelLists.Add(0x8000, mixedFavRadio); } #endregion #region LoadTuners() private void LoadTuners(XmlElement tuners) { int index = 0; foreach (var child in tuners.ChildNodes) { if (child is not XmlElement e) continue; if (e.LocalName == "tuner") LoadTunerBaseData(e, index++); else if (e.LocalName == "dvbt2-tuner") LoadDvbTunerData(e); } } private void LoadTunerBaseData(XmlElement e, int index) { var a = e.Attributes; var id = int.Parse(a["TunerId"].InnerText); var t = new Transponder(id); t.OriginalNetworkId = int.Parse(a["Oid"].InnerText); t.TransportStreamId = int.Parse(a["Tid"].InnerText); if (e.LocalName == "dvbt-tuner" || e.LocalName == "dvbt2-tuner") t.SignalSource |= SignalSource.Antenna; else if (e.LocalName == "dvbc-tuner" || e.LocalName == "dvbc2-tuner") t.SignalSource |= SignalSource.Cable; else if (e.LocalName == "dvbs-tuner" || e.LocalName == "dvbs2-tuner") t.SignalSource |= SignalSource.Sat; DataRoot.Transponder.Add(id, t); } private void LoadDvbTunerData(XmlElement e) { var a = e.Attributes; var id = int.Parse(a["TunerId"].InnerText); if (!DataRoot.Transponder.TryGetValue(id, out var t)) return; t.FrequencyInMhz = decimal.Parse(a["Frequency"].InnerText) / 1000; } #endregion #region LoadServices private void LoadServices(XmlElement services) { int index = 0; foreach (var child in services.ChildNodes) { if (child is not XmlElement e) continue; if (e.LocalName == "service") LoadServiceBaseData(e, index++); else if (e.LocalName == "analog-service") LoadServiceAnalogData(e); else if (e.LocalName == "dvb-service") LoadServiceDigitalData(e); } } private void LoadServiceBaseData(XmlElement e, int index) { var a = e.Attributes; var id = int.Parse(a["Pid"].InnerText); var c = new Channel(id); c.XmlElement = e; this.channelsById[id] = c; c.RecordOrder = index; c.Name = a["Name"].InnerText; c.Lock = a["ParentalLock"].InnerText == "1"; c.Skip = a["Selectable"].InnerText == "0"; c.ShortName = a["ShortName"].InnerText; c.Hidden = a["Visible"].InnerText == "0"; ChannelList list; switch (int.Parse(a["MediaType"].InnerText)) { case 1: c.SignalSource |= SignalSource.Tv; list = mixedFavTv; break; case 2: c.SignalSource |= SignalSource.Radio; list = mixedFavRadio; break; default: list = mixedFavTv; break; } list.AddChannel(c); } private void LoadServiceAnalogData(XmlElement e) { var a = e.Attributes; var id = int.Parse(a["ServiceId"].InnerText); if (!this.channelsById.TryGetValue(id, out var c)) return; c.FreqInMhz = decimal.Parse(a["Frequency"].InnerText) / 1000; c.SignalSource |= SignalSource.Analog; } private void LoadServiceDigitalData(XmlElement e) { var a = e.Attributes; var id = int.Parse(a["ServiceId"].InnerText); if (!this.channelsById.TryGetValue(id, out var c)) return; c.ServiceId = int.Parse(a["Sid"].InnerText); c.SignalSource |= SignalSource.Dvb; var transponderId = int.Parse(a["TunerId"].InnerText); var t = DataRoot.Transponder.TryGet(transponderId); if (t != null) { c.Transponder = t; c.OriginalNetworkId = t.OriginalNetworkId; c.TransportStreamId = t.TransportStreamId; c.FreqInMhz = t.FrequencyInMhz; c.SignalSource |= t.SignalSource; } } #endregion #region LoadFavorites() private void LoadFavorites(XmlElement favorites) { foreach (var node in favorites.ChildNodes) { if (node is not XmlElement e) continue; if (e.LocalName == "favorite-list") LoadFavoriteList(e); else if (e.LocalName == "favorite-item") LoadFavoriteItem(e); else if (e.LocalName == "lcn") LoadLcnItem(e); } this.DataRoot.AddChannelList(this.mixedFavTv); this.DataRoot.AddChannelList(this.mixedFavRadio); } private void LoadFavoriteList(XmlElement e) { var a = e.Attributes; var name = a["Name"].InnerText; var creator = a["Creator"].InnerText; var id = int.Parse(a["Pid"].InnerText); if (name == "$av") this.pidAv = id; else if (creator.StartsWith("User.") || name == "$all") { if (name == "$all") this.pidAll = id; var idx = this.favListIdToFavIndex.Count; this.favListIdToFavIndex[id] = idx + 1; this.mixedFavTv.SetFavListCaption(idx, name); this.mixedFavRadio.SetFavListCaption(idx, name); return; } var list = new ChannelList(0, name + " TV"); list.VisibleColumnFieldNames = ColumnNames; channelLists.Add(id, list); DataRoot.AddChannelList(list); if (name.StartsWith("$av")) { list.ShortCaption = "A/V"; list.ReadOnly = true; return; } list = new ChannelList(0, name + " Radio"); list.VisibleColumnFieldNames = ColumnNames; channelLists.Add(id | 0x8000, list); DataRoot.AddChannelList(list); } private void LoadFavoriteItem(XmlElement e) { var a = e.Attributes; var listId = int.Parse(a["FavoriteId"].InnerText); var serviceId = int.Parse(a["ServiceId"].InnerText); var channelNo = int.Parse(a["ChannelNum"].InnerText); if (!this.channelsById.TryGetValue(serviceId, out var c)) return; if (this.favListIdToFavIndex.TryGetValue(listId, out var listIdx)) c.SetOldPosition(listIdx, channelNo); else { c.OldProgramNr = channelNo; ((Channel)c).PhysicalListId = listId; } if (listIdx == 0) { if ((c.SignalSource & SignalSource.Radio) != 0) listId |= 0x8000; var list = channelLists.TryGet(listId); DataRoot.AddChannel(list, c); } } private void LoadLcnItem(XmlElement e) { var a = e.Attributes; var listId = int.Parse(a["FavoriteId"].InnerText); if (!int.TryParse(a["ServiceId"].InnerText, out var serviceId)) return; if (!int.TryParse(a["Lcn"].InnerText, out var lcn)) return; if (!this.channelsById.TryGetValue(serviceId, out var c)) return; c.ProgramNrPreset = lcn; if (this.favListIdToFavIndex.TryGetValue(listId, out var listIdx)) { var list = channelLists.TryGet(listId); list.ReadOnly = true; } else if (this.channelLists.TryGetValue(listId, out var list)) { list.ReadOnly = true; } } #endregion // Save #region Save() public override void Save() { var fav = this.doc["servicelist"]["favorites"]; var elements = fav.GetElementsByTagName("favorite-item"); var items = new List(); foreach(XmlNode node in elements) items.Add(node); foreach (var node in items) node.ParentNode.RemoveChild(node); elements = fav.GetElementsByTagName("lcn"); var lcn = new List(); foreach (XmlNode node in elements) lcn.Add(node); foreach (var node in lcn) node.ParentNode.RemoveChild(node); var idVal = 0; var lists = new List>(); foreach (var list in this.DataRoot.ChannelLists) { if (list.IsMixedSourceFavoritesList) { foreach (var entry in favListIdToFavIndex) lists.Add(Tuple.Create(entry.Key, list, entry.Value)); } else { lists.Add(Tuple.Create(channelLists.FirstOrDefault(e => e.Value == list).Key, list, 0)); } } //var listIds = this.channelLists.Keys.Union(this.favListIdToFavIndex.Keys).OrderBy(k => k).ToList(); foreach (var tuple in lists) { var listId = tuple.Item1 & 0x7FFF; var list = tuple.Item2; var favIndex = tuple.Item3; foreach (var chan in list.Channels) { if (chan is not Channel c || c.IsProxy) continue; var chno = c.GetPosition(favIndex); if (chno < 0) continue; var e = doc.CreateElement("favorite-item"); e.SetAttribute("Active", "0"); e.SetAttribute("Attribute", "0"); e.SetAttribute("ChannelNum", chno.ToString()); e.SetAttribute("FavoriteId", listId.ToString()); e.SetAttribute("Id", (++idVal).ToString()); e.SetAttribute("OriginalFavoriteId", listId.ToString()); e.SetAttribute("Selectable", "-1"); e.SetAttribute("ServiceId", c.RecordIndex.ToString()); e.SetAttribute("ServiceName", ""); e.SetAttribute("Visible", "-1"); fav.AppendChild(e); } } foreach (XmlNode item in lcn) fav.AppendChild(item); doc.Save(this.FileName); } #endregion }