diff --git a/source/ChanSort.Api/Controller/SerializerBase.cs b/source/ChanSort.Api/Controller/SerializerBase.cs index 1a4e8a6..37046fc 100644 --- a/source/ChanSort.Api/Controller/SerializerBase.cs +++ b/source/ChanSort.Api/Controller/SerializerBase.cs @@ -230,7 +230,7 @@ namespace ChanSort.Api } #endregion - #region ParseInt() + #region ParseLong() protected long ParseLong(string input) { if (string.IsNullOrWhiteSpace(input)) diff --git a/source/ChanSort.Loader.Philips/ChanLstBin.cs b/source/ChanSort.Loader.Philips/ChanLstBin.cs index 457a6f4..e3b142f 100644 --- a/source/ChanSort.Loader.Philips/ChanLstBin.cs +++ b/source/ChanSort.Loader.Philips/ChanLstBin.cs @@ -58,11 +58,17 @@ namespace ChanSort.Loader.Philips off += 4; if (modelNameLen >= 1) - this.ModelName = Encoding.ASCII.GetString(content, off, modelNameLen-1); + this.ModelName = Encoding.ASCII.GetString(content, off, modelNameLen).TrimEnd('\0'); off += modelNameLen; log?.Invoke($"Philips TV model: {this.ModelName}\nFile format version: {VersionMajor}.{VersionMinor}\n\n"); + if (VersionMajor >= 120) + { + LoadVersion120OrLater(path); + return; + } + var baseDir = Path.GetDirectoryName(path) ?? ""; var relPath = "/channellib/"; while (off < content.Length) @@ -86,19 +92,76 @@ namespace ChanSort.Loader.Philips } } - this.Validate(path); + this.Validate(path, false); } - private void Validate(string chanLstBinPath) + private class V120Info + { + public readonly int Offset; + public readonly string Filename; + public readonly int BytesBeforeChecksum; + public readonly string SubDir; + + public V120Info(int offset, string filename, int bytesBeforeChecksum, string subDir) + { + this.Offset = offset; + this.Filename = filename; + this.BytesBeforeChecksum = bytesBeforeChecksum; + this.SubDir = subDir; + } + } + + private void LoadVersion120OrLater(string path) + { + // starting with version 120, the chanLst.bin + // - no longer includes a 0x00 terminating character as part of the file name + // - has a random number of 0x00 bytes following the file name (0-3) (2 after DVBT, 3 after DVBC, 0 after DVBSall, 1 after Favorite) + // - only stores 1 byte for the length of "/s2channellib/" instead of 4 + // - only stores the lower 8 bits of the CRC16-Modbus + // This format required a tailor-made implementation with the exact byte layout + + if (content.Length != 118) + throw LoaderException.Fail($"chanLst.bin has an unsupported size"); + + var entries = new List + { + new V120Info(0x29, "DVBT.xml", 2, "/channellib/"), + new V120Info(0x38, "DVBC.xml", 3, "/channellib/"), + new V120Info(0x58, "DVBSall.xml", 0, "/s2channellib/"), + new V120Info(0x68, "Favorite.xml", 1, ""), + }; + + + foreach(var entry in entries ) + { + var off = entry.Offset; + var fileName = Encoding.ASCII.GetString(content, off, entry.Filename.Length); + if (fileName != entry.Filename) + throw LoaderException.Fail("Entry in chanLst.bin doesn't match expected data"); + + var newPath = entry.SubDir + fileName; + crcOffsetByRelPath[newPath] = off + entry.Filename.Length + entry.BytesBeforeChecksum; + } + + this.Validate(path, true); + } + + private void Validate(string chanLstBinPath, bool onlyLower8Bits) { var baseDir = Path.GetDirectoryName(chanLstBinPath); string errors = ""; foreach (var entry in crcOffsetByRelPath) { var crcOffset = entry.Value; - var expectedCrc = BitConverter.ToUInt16(this.content, crcOffset); - if (expectedCrc == 0) - continue; + int expectedCrc; + if (onlyLower8Bits) + expectedCrc = this.content[crcOffset]; + else + { + expectedCrc = BitConverter.ToUInt16(this.content, crcOffset); + if (expectedCrc == 0) + continue; + } var filePath = baseDir + entry.Key; if (!File.Exists(filePath)) @@ -115,6 +178,8 @@ namespace ChanSort.Loader.Philips // length = 0x0140000; var actualCrc = Crc16.Modbus(data, 0, length); + if (onlyLower8Bits) + actualCrc &= 0x00FF; if (actualCrc != expectedCrc) { var msg = $"chanLst.bin: stored CRC for {entry.Key} is {expectedCrc:X4} but calculated {actualCrc:X4}"; @@ -163,7 +228,8 @@ namespace ChanSort.Loader.Philips var crc = Crc16.Modbus(data, 0, length); var off = entry.Value; content[off] = (byte) crc; - content[off + 1] = (byte) (crc >> 8); + if (VersionMajor < 120) + content[off + 1] = (byte) (crc >> 8); } File.WriteAllBytes(chanLstBinPath, content); } diff --git a/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini b/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini index 04fa5cb..2e4a14f 100644 --- a/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini +++ b/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini @@ -427,7 +427,7 @@ incrementFavListVersion=true allowDelete=false ############################################################################ -# ChannelMap_110: same as 110 +# ChannelMap_115: same as 110 [Map115] padChannelName=true @@ -437,3 +437,19 @@ userReorderChannel=0 reorderRecordsByChannelNumber=true incrementFavListVersion=true allowDelete=false + +############################################################################ +# ChannelMap_120: mostly same as 115 +# - Umlauts are encoded with high byte 0xFF instead of 0x00 +# - Scrambled is always 0 without any real indication for encryption +# - UserHidden has value "3" instead of "0" for most channels, "1" still exists for some + +[Map120] +padChannelName=true +setFavoriteNumber=false +setReorderedFavNumber=false +userReorderChannel=0 +reorderRecordsByChannelNumber=true +incrementFavListVersion=false +allowDelete=false +userHiddenDefaultValue=3 diff --git a/source/ChanSort.Loader.Philips/Channel.cs b/source/ChanSort.Loader.Philips/Channel.cs index 10a7bef..796129b 100644 --- a/source/ChanSort.Loader.Philips/Channel.cs +++ b/source/ChanSort.Loader.Philips/Channel.cs @@ -10,7 +10,7 @@ namespace ChanSort.Loader.Philips this.RecordOrder = (int)index; } - internal Channel(SignalSource source, int order, int rowId, XmlNode setupNode) + internal Channel(SignalSource source, int order, long rowId, XmlNode setupNode) { this.SignalSource = source; this.RecordOrder = order; diff --git a/source/ChanSort.Loader.Philips/PhilipsPlugin.cs b/source/ChanSort.Loader.Philips/PhilipsPlugin.cs index 9665f99..169c751 100644 --- a/source/ChanSort.Loader.Philips/PhilipsPlugin.cs +++ b/source/ChanSort.Loader.Philips/PhilipsPlugin.cs @@ -73,9 +73,13 @@ namespace ChanSort.Loader.Philips * e.g. 65PUS8535/12, 55PUS7334/12 * * version 115.0 - * same as 110.0 + * same as 105.0 * - * Version 0.1 and 100-115 are XML based and loaded through the XmlSerializer. + * version 120.0 + * same as 105 plus additional ChannelList\MtkChannelList.xml + * + * + * Version 0.1 and 100-120 are XML based and loaded through the XmlSerializer. * Version 1.1 and 1.2 are loaded through the BinSerializer. * Version 0.0, 11.1 and 45.1 are not supported yet. */ @@ -131,7 +135,7 @@ namespace ChanSort.Loader.Philips } } - if (majorVersion == 0 || majorVersion >= 100 && majorVersion <= 115) + if (majorVersion == 0 || majorVersion >= 100 && majorVersion <= 120) return new XmlSerializer(inputFile); if (majorVersion == 1 || majorVersion == 2 || majorVersion == 30 || majorVersion == 45) // || majorVersion == 11 // format version 11 is similar to 1.x, but not (yet) supported return new BinarySerializer(inputFile); diff --git a/source/ChanSort.Loader.Philips/XmlSerializer.cs b/source/ChanSort.Loader.Philips/XmlSerializer.cs index 5728cc2..f0dd7b4 100644 --- a/source/ChanSort.Loader.Philips/XmlSerializer.cs +++ b/source/ChanSort.Loader.Philips/XmlSerializer.cs @@ -426,7 +426,7 @@ namespace ChanSort.Loader.Philips data.Add(attr.LocalName, attr.Value); } - if (!data.ContainsKey("UniqueID") || !int.TryParse(data["UniqueID"], out var uniqueId)) // UniqueId only exists in ChannelMap_105 and later + if (!data.ContainsKey("UniqueID") || !long.TryParse(data["UniqueID"], out var uniqueId)) // UniqueId only exists in ChannelMap_105 and later uniqueId = rowId; var chan = new Channel(curList.SignalSource & SignalSource.MaskBcast, rowId, uniqueId, setupNode); chan.OldProgramNr = -1; @@ -478,7 +478,7 @@ namespace ChanSort.Loader.Philips chan.RawName = data.TryGet("ChannelName"); chan.Name = DecodeName(chan.RawName); chan.Lock = data.TryGet("ChannelLock") == "1"; - chan.Hidden = data.TryGet("UserHidden") == "1"; + chan.Hidden = data.TryGet("UserHidden") == "1"; // can be "3" instead of "0", at least in format 120 var fav = ParseInt(data.TryGet("FavoriteNumber")); chan.SetOldPosition(1, fav == 0 ? -1 : fav); chan.OriginalNetworkId = ParseInt(data.TryGet("Onid")); @@ -487,13 +487,23 @@ namespace ChanSort.Loader.Philips chan.FreqInMhz = ParseInt(data.TryGet("Frequency")); ; if (chan.FreqInMhz > 2000 && (chan.SignalSource & SignalSource.Sat) == 0) chan.FreqInMhz /= 1000; - + + // version 120 stores actual DVB ServiceType values, earlier versions only 1=TV, 2=Radio var st = ParseInt(data.TryGet("ServiceType")); - chan.ServiceTypeName = st == 1 ? "TV" : "Radio"; - if (st == 1) - chan.SignalSource |= SignalSource.Tv; + if (chanLstBin != null && chanLstBin.VersionMajor >= 120) + { + chan.ServiceType = st; + chan.ServiceTypeName = LookupData.Instance.GetServiceTypeDescription(st); + chan.SignalSource |= LookupData.Instance.IsRadioTvOrData(st); + } else - chan.SignalSource |= SignalSource.Radio; + { + chan.ServiceTypeName = st == 1 ? "TV" : "Radio"; + if (st == 1) + chan.SignalSource |= SignalSource.Tv; + else + chan.SignalSource |= SignalSource.Radio; + } chan.Source = (chan.SignalSource & SignalSource.Sat) != 0 ? "DVB-S" : (chan.SignalSource & SignalSource.Cable) != 0 ? "DVB-C" : (chan.SignalSource & SignalSource.Antenna) != 0 ? "DVB-T" : ""; chan.SignalSource |= LookupData.Instance.IsRadioTvOrData(chan.ServiceType); @@ -528,19 +538,21 @@ namespace ChanSort.Loader.Philips { favChannels.Channels.Add(chan); for (int i=0; i= 120 ? 0 : +1; + foreach (XmlNode child in node.ChildNodes) { if (child.LocalName == "FavoriteChannel") { - var uniqueId = ParseInt(child["UniqueID"].InnerText); + var uniqueId = ParseLong(child["UniqueID"].InnerText); var favNumber = ParseInt(child["FavNumber"].InnerText); - var chan = this.favChannels.Channels.FirstOrDefault(ch => ch.RecordIndex == uniqueId); - chan?.SetOldPosition(index, favNumber + 1); + var chan = this.favChannels.Channels.FirstOrDefault(ch => ch.RecordIndex == uniqueId && ch.GetOldPosition(index) <= 0); + chan?.SetOldPosition(index, favNumber + startNrBias); } } } @@ -555,15 +567,21 @@ namespace ChanSort.Loader.Philips // according to https://github.com/PredatH0r/ChanSort/issues/347 Philips seems to not use UTF 16, but instead use locale dependent encoding and // writing "0xAA 0x00" to the file for an 8 bit code point. At least for the favorite list captions. Congratulations, well done! + // In version 120 umlauts in channel names are encoded as 1 byte CP-1252 (= low byte UTF16) code point + 0xFF as the second byte + var hexParts = input.Split(' '); var buffer = new MemoryStream(); + bool highByte = false; foreach (var part in hexParts) { if (part == "") continue; var val = (byte)ParseInt(part); + if (highByte && val == 0xff) // hack-around for version 120 + val = 0; buffer.WriteByte(val); + highByte = !highByte; } return Encoding.Unicode.GetString(buffer.GetBuffer(), 0, (int) buffer.Length).TrimEnd('\x0'); @@ -573,7 +591,11 @@ namespace ChanSort.Loader.Philips #region GetDataFilePaths() public override IEnumerable GetDataFilePaths() { - return this.fileDataList.Select(f => f.path); + var list = new List(); + if (this.chanLstBin != null) + list.Add(this.FileName); + list.AddRange(this.fileDataList.Select(f => f.path)); + return list; } #endregion @@ -596,7 +618,7 @@ namespace ChanSort.Loader.Philips // The official Philips Editor 6.61.22 does not reorder the XML nodes and does not change dtv_cmdb_*.bin when editing a ChannelMap_100 folder. But it is unclear if this editor is designed to handle the cmdb flavor. // A user with a ChannelMap_100 export including a dtv_cmdb_2.bin reported, that the TV shows the reordered list in the menu, but tunes the channels based on the original numbers. - // It's unclear if that happenes because the XML was reordered and out-of-sync with the .bin, or if the TV always uses the .bin for tuning and XML edits are moot. + // It's unclear if that happens because the XML was reordered and out-of-sync with the .bin, or if the TV always uses the .bin for tuning and XML edits are moot. // On top of that this TV messed up Umlauts during the import, despite ChanSort writing the exact same name data in hex-encoded UTF16. The result was as if the string was exported as UTF-8 bytes and then parsed with an 8-bit code page. var reorderNodes = this.iniMapSection?.GetBool("reorderRecordsByChannelNumber") ?? false; @@ -666,7 +688,7 @@ namespace ChanSort.Loader.Philips } setup["ChannelLock"].Value = ch.Lock ? "1" : "0"; - setup["UserHidden"].Value = ch.Hidden ? "1" : "0"; + setup["UserHidden"].Value = ch.Hidden ? "1" : iniMapSection.GetInt("userHiddenDefaultValue", 0).ToString(); // ChannelMap_100 supports a single fav list and stores the favorite number directly in the channel. // The official Philips editor allows to reorder favorites when switched to the "Favourite" list view @@ -726,18 +748,39 @@ namespace ChanSort.Loader.Philips attr.InnerText = (version + 1).ToString(); } + var startNrBias = (chanLstBin?.VersionMajor ?? 0) >= 120 ? 0 : -1; + var uniqueIdFormat = (chanLstBin?.VersionMajor ?? 0) >= 120 ? "d10" : "d"; // v120 writes 10 digits as uniqueID, including leading zeros + foreach (var ch in favChannels.Channels.OrderBy(ch => ch.GetPosition(index))) { var nr = ch.GetPosition(index); if (nr <= 0) continue; var uniqueIdNode = favFile.doc.CreateElement("UniqueID"); - uniqueIdNode.InnerText = ch.RecordIndex.ToString(); + uniqueIdNode.InnerText = ch.RecordIndex.ToString(uniqueIdFormat); var favNrNode = favFile.doc.CreateElement("FavNumber"); - favNrNode.InnerText = (nr-1).ToString(); + favNrNode.InnerText = (nr + startNrBias).ToString(); var channelNode = favFile.doc.CreateElement("FavoriteChannel"); channelNode.AppendChild(uniqueIdNode); channelNode.AppendChild(favNrNode); + + // Version 120 also stores a element in the XML + if ((chanLstBin?.VersionMajor ?? 0) >= 120) + { + var chanTypeNode = favFile.doc.CreateElement("ChannelType"); + string type = null; + + if ((ch.SignalSource & SignalSource.Sat) != 0) type = "TYPE_DVB_S"; // observed + else if ((ch.SignalSource & SignalSource.Cable) != 0) type = "TYPE_DVB_C"; // fairly sure + else if ((ch.SignalSource & SignalSource.Antenna) != 0) type = "TYPE_DVB_T"; // could also be T2 maybe + + if (type != null) + { + chanTypeNode.InnerText = type; + channelNode.AppendChild(chanTypeNode); + } + } + favListNode.AppendChild(channelNode); } }