diff --git a/src/TaglibSharp.Tests/TaggingFormats/Id3V2Test.cs b/src/TaglibSharp.Tests/TaggingFormats/Id3V2Test.cs index 8b2ea676c..77aea7f76 100644 --- a/src/TaglibSharp.Tests/TaggingFormats/Id3V2Test.cs +++ b/src/TaglibSharp.Tests/TaggingFormats/Id3V2Test.cs @@ -18,6 +18,8 @@ public class Id3V2Test static readonly string[] val_gnre = {"Rap", "Jazz", "Non-Genre", "Blues"}; + static readonly System.DateTime val_date = new System.DateTime (2022, 10, 20, 16, 45, 23, 0, 0); + [Test] public void TestTitle () { @@ -576,6 +578,126 @@ public void TestPictures () } } + [Test] + public void TestPocastFlag () + { + Tag tag = new Tag (); + + // This property isn't supported in version 2. + for (byte version = 3; version <= 4; version++) { + tag.Version = version; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Initial (IsEmpty): " + m); + Assert.IsFalse (t.PodcastFlag, "Initial (False): " + m); + }, 3); + + tag.PodcastFlag = true; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsFalse (t.IsEmpty, "Value Set (!IsEmpty): " + m); + Assert.IsTrue (t.PodcastFlag, "Value Set (!False): " + m); + }, 3); + + tag.PodcastFlag = false; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Value Cleared (IsEmpty): " + m); + Assert.IsFalse (t.PodcastFlag, "Value Cleared (False): " + m); + }, 3); + } + } + + [Test] + public void TestPodcastIdentifier () + { + var tag = new Tag (); + + // This property isn't supported in version 2. + for (byte version = 3; version <= 4; version++) { + tag.Version = version; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Initial (IsEmpty): " + m); + Assert.IsNull (t.PodcastIdentifier, "Initial (Null): " + m); + }, 3); + + tag.PodcastIdentifier = val_sing; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsFalse (t.IsEmpty, "Value Set (!IsEmpty): " + m); + Assert.AreEqual (val_sing, t.PodcastIdentifier, "Value Set (!Null): " + m); + }, 3); + + tag.PodcastIdentifier = string.Empty; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Value Cleared (IsEmpty): " + m); + Assert.IsNull (t.PodcastIdentifier, "Value Cleared (Null): " + m); + }, 3); + } + } + + [Test] + public void TestPodcastFeed () + { + var tag = new Tag (); + + // This property isn't supported in version 2. + for (byte version = 3; version <= 4; version++) { + tag.Version = version; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Initial (IsEmpty): " + m); + Assert.IsNull (t.PodcastFeed, "Initial (Null): " + m); + }, 3); + + tag.PodcastFeed = val_sing; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsFalse (t.IsEmpty, "Value Set (!IsEmpty): " + m); + Assert.AreEqual (val_sing, t.PodcastFeed, "Value Set (!Null): " + m); + }, 3); + + tag.PodcastFeed = string.Empty; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Value Cleared (IsEmpty): " + m); + Assert.IsNull (t.PodcastFeed, "Value Cleared (Null): " + m); + }, 3); + } + } + + [Test] + public void TestPodcastDescription () + { + var tag = new Tag (); + + // This property isn't supported in version 2. + for (byte version = 3; version <= 4; version++) { + tag.Version = version; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Initial (IsEmpty): " + m); + Assert.IsNull (t.PodcastDescription, "Initial (Null): " + m); + }, 3); + + tag.PodcastDescription = val_sing; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsFalse (t.IsEmpty, "Value Set (!IsEmpty): " + m); + Assert.AreEqual (val_sing, t.PodcastDescription, "Value Set (!Null): " + m); + }, 3); + + tag.PodcastDescription = string.Empty; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Value Cleared (IsEmpty): " + m); + Assert.IsNull (t.PodcastDescription, "Value Cleared (Null): " + m); + }, 3); + } + } + [Test] public void TestIsCompilation () { @@ -968,6 +1090,35 @@ public void TestPublisher () } } + + [Test] + public void TestEncodedBy () + { + Tag tag = new Tag (); + for (byte version = 2; version <= 4; version++) { + tag.Version = version; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Initial (IsEmpty): " + m); + Assert.IsNull (t.EncodedBy, "Initial (Null): " + m); + }); + + tag.EncodedBy = val_sing; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsFalse (t.IsEmpty, "Value Set (!IsEmpty): " + m); + Assert.AreEqual (val_sing, t.EncodedBy, "Value Set (!Null): " + m); + }); + + tag.EncodedBy = string.Empty; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Value Cleared (IsEmpty): " + m); + Assert.IsNull (t.EncodedBy, "Value Cleared (Null): " + m); + }); + } + } + [Test] public void TestISRC () { @@ -996,6 +1147,34 @@ public void TestISRC () } } + [Test] + public void TestReleaseDate () + { + Tag tag = new Tag (); + for (byte version = 4; version <= 4; version++) { + tag.Version = version; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Initial (IsEmpty): " + m); + Assert.IsNull (t.ReleaseDate, "Initial (Null): " + m); + }, 4); + + tag.ReleaseDate = val_date; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsFalse (t.IsEmpty, "Value Set (!IsEmpty): " + m); + Assert.AreEqual (val_date, t.ReleaseDate.Value, "Value Set (!Null): " + m); + }, 4); + + tag.ReleaseDate = null; + + TagTestWithSave (ref tag, delegate (Tag t, string m) { + Assert.IsTrue (t.IsEmpty, "Value Cleared (IsEmpty): " + m); + Assert.IsNull (t.ReleaseDate, "Value Cleared (Null): " + m); + }, 4); + } + } + [Test] public void TestLength () { @@ -1078,7 +1257,11 @@ public void TestClear () Publisher = "L", ISRC = "M", Length = "L", - RemixedBy = "N" + RemixedBy = "N", + PodcastFlag = true, + PodcastDescription = "description here", + PodcastFeed = "https://example.org/feed.rss", + PodcastIdentifier = "unique id" }; @@ -1109,6 +1292,10 @@ public void TestClear () Assert.IsNull (tag.ISRC, "ISRC"); Assert.IsNull (tag.Length, "Length"); Assert.IsNull (tag.RemixedBy, "RemixedBy"); + Assert.IsFalse (tag.PodcastFlag, "PodcastFlag"); + Assert.IsNull (tag.PodcastDescription, "PodcastDescription"); + Assert.IsNull (tag.PodcastFeed, "PodcastFeed"); + Assert.IsNull (tag.PodcastIdentifier, "PodcastIdentifier"); } [Test] @@ -1634,10 +1821,10 @@ public void TestInvolvedPersonsFrame () delegate void TagTestFunc (Tag tag, string msg); - void TagTestWithSave (ref Tag tag, TagTestFunc testFunc) + void TagTestWithSave (ref Tag tag, TagTestFunc testFunc, byte minVersion = 2) { testFunc (tag, "Before Save"); - for (byte version = 2; version <= 4; version++) { + for (byte version = minVersion; version <= 4; version++) { tag.Version = version; tag = new Tag (tag.Render ()); testFunc (tag, "After Save, Version: " + version); diff --git a/src/TaglibSharp/Id3v2/FrameFactory.cs b/src/TaglibSharp/Id3v2/FrameFactory.cs index 9a1b9c5c6..309f3c2c9 100644 --- a/src/TaglibSharp/Id3v2/FrameFactory.cs +++ b/src/TaglibSharp/Id3v2/FrameFactory.cs @@ -289,6 +289,10 @@ public static Frame CreateFrame (ByteVector data, File file, ref int offset, byt // Table of Contents (ID3v2 Chapter Frame Addendum) if (header.FrameId == FrameType.CTOC) return new TableOfContentsFrame (data, position, header, version); + + // Podcast Flag Frame + if (header.FrameId == FrameType.PCST) + return new PodcastFlagFrame(data, position, header, version); return new UnknownFrame (data, position, header, version); } diff --git a/src/TaglibSharp/Id3v2/FrameTypes.cs b/src/TaglibSharp/Id3v2/FrameTypes.cs index 2fda85de8..8cc155154 100644 --- a/src/TaglibSharp/Id3v2/FrameTypes.cs +++ b/src/TaglibSharp/Id3v2/FrameTypes.cs @@ -101,5 +101,13 @@ static class FrameType public static readonly ReadOnlyByteVector WPUB = "WPUB"; public static readonly ReadOnlyByteVector WXXX = "WXXX"; public static readonly ReadOnlyByteVector ETCO = "ETCO"; + public static readonly ReadOnlyByteVector TDRL = "TDRL"; // Release Time Frame + public static readonly ReadOnlyByteVector TENC = "TENC"; // Encoded By Frame. + public static readonly ReadOnlyByteVector PCST = "PCST"; // Podcast Flag Frame. + public static readonly ReadOnlyByteVector TDES = "TDES"; // Podcast Description Frame. + public static readonly ReadOnlyByteVector TGID = "TGID"; // Podcast Identifier Frame. + public static readonly ReadOnlyByteVector WFED = "WFED"; // Podcast Feed Url Frame. + public static readonly ReadOnlyByteVector TCAT = "TCAT"; // Podcast Category Frame. + public static readonly ReadOnlyByteVector TKWD = "TKWD"; // Podcast Keywords Frame. } } diff --git a/src/TaglibSharp/Id3v2/Frames/PodcastFlagFrame.cs b/src/TaglibSharp/Id3v2/Frames/PodcastFlagFrame.cs new file mode 100644 index 000000000..ea41a3030 --- /dev/null +++ b/src/TaglibSharp/Id3v2/Frames/PodcastFlagFrame.cs @@ -0,0 +1,263 @@ +// +// PodcastFlagFrame.cs: +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +namespace TagLib.Id3v2 +{ + /// + /// This class extends , implementing support for + /// Podcast Flag (PCST) Frames. + /// + /// + /// Getting and setting the podcast flag of a file. + /// + /// using TagLib; + /// using TagLib.Id3v2; + /// + /// public static class TrackUtil + /// { + /// public static bool GetPodcastFlag (string filename) + /// { + /// File file = File.Create (filename, ReadStyle.None); + /// Id3v2.Tag tag = file.GetTag (TagTypes.Id3v2, false) as Id3v2.Tag; + /// if (tag == null) + /// return false; + /// + /// PodcastFlagFrame frame = PodcastFlagFrame.Get (tag, false); + /// if (frame == null) + /// return false; + /// + /// return true; + /// } + /// + /// public static void SetPodcastFlag (string filename) + /// { + /// File file = File.Create (filename, ReadStyle.None); + /// Id3v2.Tag tag = file.GetTag (TagTypes.Id3v2, true) as Id3v2.Tag; + /// if (tag == null) + /// return; + /// + /// PodcastFlagFrame.Get (tag, true); + /// file.Save (); + /// } + /// } + /// + /// + /// #using <System.dll> + /// #using <taglib-sharp.dll> + /// + /// using System; + /// using TagLib; + /// using TagLib::Id3v2; + /// + /// public ref class TrackUtil abstract sealed + /// { + /// public: + /// static bool GetPodcastFlag (String^ filename) + /// { + /// File^ file = File.Create (filename, ReadStyle.None); + /// Id3v2::Tag^ tag = dynamic_cast<Id3v2::Tag^> (file.GetTag (TagTypes::Id3v2, false)); + /// if (tag == null) + /// return false; + /// + /// PodcastFlagFrame^ frame = PodcastFlagFrame::Get (tag, false); + /// if (frame == null) + /// return false; + /// + /// return true; + /// } + /// + /// static void SetPodcastFlag (String^ filename) + /// { + /// File^ file = File::Create (filename, ReadStyle::None); + /// Id3v2.Tag^ tag = dynamic_cast<Id3v2::Tag^> (file.GetTag (TagTypes::Id3v2, true)); + /// if (tag == null) + /// return; + /// + /// PodcastFlagFrame::Get (tag, true); + /// file->Save (); + /// } + /// } + /// + /// + public class PodcastFlagFrame : Frame + { + #region Constants + + /// + /// The PodcastFlagFrame data is simply an array of 4 null bytes. + /// + private readonly static ReadOnlyByteVector ExpectedData = new ReadOnlyByteVector(new byte[] { 0x0, 0x0, 0x0, 0x0 }); + + #endregion + + #region Constructors + + /// + /// Constructs and initializes a new instance of . + /// + /// + /// When a frame is created, it is not automatically added to + /// the tag. Consider using for more + /// integrated frame creation. + /// + public PodcastFlagFrame () : base (FrameType.PCST, 4) + { + } + + /// + /// Constructs and initializes a new instance of by reading its raw data in a + /// specified ID3v2 version. + /// + /// + /// A object starting with the raw + /// representation of the new frame. + /// + /// + /// A indicating the ID3v2 version the + /// raw frame is encoded in. + /// + public PodcastFlagFrame (ByteVector data, byte version) + : base (data, version) + { + SetData (data, 0, version, true); + } + + /// + /// Constructs and initializes a new instance of by reading its raw data in a + /// specified ID3v2 version. + /// + /// + /// A object containing the raw + /// representation of the new frame. + /// + /// + /// A indicating at what offset in + /// the frame actually begins. + /// + /// + /// A containing the header of the + /// frame found at in the data. + /// + /// + /// A indicating the ID3v2 version the + /// raw frame is encoded in. + /// + protected internal PodcastFlagFrame (ByteVector data, int offset, FrameHeader header, byte version) + : base (header) + { + SetData (data, offset, version, false); + } + + #endregion + + #region Public Static Methods + + /// + /// Gets a podcast flag frame from a specified tag, optionally + /// creating it if it does not exist. + /// + /// + /// A object to search in. + /// + /// + /// A specifying whether or not to create + /// and add a new frame to the tag if a match is not found. + /// + /// + /// A object containing the + /// matching frame, or if a match + /// wasn't found and is . + /// + public static PodcastFlagFrame Get (Tag tag, bool create) + { + PodcastFlagFrame pcst; + foreach (Frame frame in tag) { + pcst = frame as PodcastFlagFrame; + + if (pcst != null) + return pcst; + } + + if (!create) + return null; + + pcst = new PodcastFlagFrame (); + tag.AddFrame (pcst); + return pcst; + } + + #endregion + + #region Protected Methods + + /// + /// Populates the values in the current instance by parsing + /// its field data in a specified version. + /// + /// + /// A object containing the + /// extracted field data. + /// + /// + /// A indicating the ID3v2 version the + /// field data is encoded in. + /// + protected override void ParseFields (ByteVector data, byte version) + { + if (data.CompareTo(ExpectedData) != 0) + throw new CorruptFileException ("Podcast flag value is incorrect."); + } + + /// + /// Renders the values in the current instance into field + /// data for a specified version. + /// + /// + /// A indicating the ID3v2 version the + /// field data is to be encoded in. + /// + /// + /// A object containing the + /// rendered field data. + /// + protected override ByteVector RenderFields (byte version) + { + ByteVector data = new ByteVector(ExpectedData); + + return data; + } + + #endregion + + #region ICloneable + + /// + /// Creates a deep copy of the current instance. + /// + /// + /// A new object identical to the + /// current instance. + /// + public override Frame Clone () + { + Frame frame = new PodcastFlagFrame(); + + return frame; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/TaglibSharp/Id3v2/Frames/UrlLinkFrame.cs b/src/TaglibSharp/Id3v2/Frames/UrlLinkFrame.cs index 3a7bee876..86722d0e7 100644 --- a/src/TaglibSharp/Id3v2/Frames/UrlLinkFrame.cs +++ b/src/TaglibSharp/Id3v2/Frames/UrlLinkFrame.cs @@ -386,6 +386,11 @@ protected void ParseRawData () ByteVector delim = ByteVector.TextDelimiter (encoding); + // The WFED has a leading null byte so we remove it during parsing. + if (FrameId == FrameType.WFED && data.Count > 1 && data[0] == (byte) 0) { + data.RemoveAt(0); + } + if (FrameId != FrameType.WXXX) { field_list.AddRange (data.ToStrings (StringType.Latin1, 0)); } else if (data.Count > 1 && !data.Mid (0, @@ -439,6 +444,9 @@ protected override ByteVector RenderFields (byte version) if (wxxx) v = new ByteVector ((byte)encoding); + // The WFED has a leading null byte so we add it during render. + else if (FrameId == FrameType.WFED) + v = new ByteVector ((byte)0); else v = new ByteVector (); string[] text = text_fields; diff --git a/src/TaglibSharp/Id3v2/Tag.cs b/src/TaglibSharp/Id3v2/Tag.cs index 1c3676ff7..201954571 100644 --- a/src/TaglibSharp/Id3v2/Tag.cs +++ b/src/TaglibSharp/Id3v2/Tag.cs @@ -31,6 +31,7 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text; namespace TagLib.Id3v2 @@ -2183,7 +2184,6 @@ IEnumerator IEnumerable.GetEnumerator () } set { if (double.IsNaN (value)) { - SetUserTextAsString ("REPLAYGAIN_ALBUM_GAIN", null, false); } else { string text = value.ToString ("0.00 dB", CultureInfo.InvariantCulture); SetUserTextAsString ("REPLAYGAIN_ALBUM_GAIN", text, false); @@ -2265,6 +2265,20 @@ IEnumerator IEnumerable.GetEnumerator () set { SetTextFrame (FrameType.TPUB, value); } } + /// + /// Gets and sets the TENC (Encoded by) of the song. + /// + /// + /// A object containing the TENC of the song. + /// + /// + /// This property is implemented using the "TENC" field. + /// + public string EncodedBy { + get { return GetTextAsString (FrameType.TENC); } + set { SetTextFrame (FrameType.TENC, value); } + } + /// /// Gets and sets the ISRC (International Standard Recording Code) of the song. /// @@ -2279,6 +2293,48 @@ IEnumerator IEnumerable.GetEnumerator () set { SetTextFrame (FrameType.TSRC, value); } } + /// + /// Gets and sets the date at which the song has been released. + /// + /// + /// A nullable object containing the + /// date at which the song has been released, or if no value present. + /// + /// + /// This property is implemented using the "TDRL" field. + /// This is a ID3v2.4 type tag. + /// + public DateTime? ReleaseDate { + get { + string value = GetTextAsString (FrameType.TDRL); + + if (String.IsNullOrWhiteSpace(value)) { + return null; + } else if (DateTime.TryParseExact (value.Replace ('T', ' '), "yyyy-MM-dd HH:mm:ss", null, DateTimeStyles.None, out DateTime exactDate)) { + return exactDate; + } else if (DateTime.TryParse(value, out DateTime parsedDate)) { + return parsedDate; + } + + return null; + } + set { + string date = null; + + if (value != null) { + date = $"{value:yyyy-MM-dd HH:mm:ss}"; + date = date.Replace (' ', 'T'); + } + + if (date == null) { + RemoveFrames(FrameType.TDRL); + } else { + SetTextFrame(FrameType.TDRL, date); + } + } + } + /// /// Gets and sets the length of the media represented /// by the current instance. @@ -2331,6 +2387,106 @@ IEnumerator IEnumerable.GetEnumerator () } } + /// + /// Gets and sets the podcast flag of the media represented by the + /// current instance. + /// + /// + /// A object containing the podcast flag of the song. + /// + /// + /// This property is implemented using the "PCST" field. + /// This property is supported in version 3 forward. + /// + public bool PodcastFlag + { + get + { + IEnumerable items = this.GetFrames(FrameType.PCST); + + if (items == null || items.Count() <= 0) { + return false; + } else { + return true; + } + } + set + { + if (Version < 3) + throw new InvalidOperationException("Version must be at least 3."); + + if (PodcastFlag == value) { + // No change + } else if (value) { + PodcastFlagFrame frame = new PodcastFlagFrame(); + AddFrame(frame); + } else { + RemoveFrames(FrameType.PCST); + } + } + } + + /// + /// Gets and sets the podcast identifier of the song. + /// + /// + /// A object containing the podcast identifier of the song. + /// + /// + /// This property is implemented using the "TGID" field. + /// This property is supported in version 3 forward. + /// + public string PodcastIdentifier { + get { return GetTextAsString (FrameType.TGID); } + set + { + if (Version < 3) + throw new InvalidOperationException("Version must be at least 3."); + + SetTextFrame (FrameType.TGID, value); + } + } + + /// + /// Gets and sets the podcast feed of the song. + /// + /// + /// A object containing the podcast feed of the song. + /// + /// + /// This property is implemented using the "WFED" field. + /// This property is supported in version 3 forward. + /// + public string PodcastFeed { + get { return GetTextAsString (FrameType.WFED); } + set { + if (Version < 3) + throw new InvalidOperationException("Version must be at least 3."); + + SetTextFrame (FrameType.WFED, value); + } + } + + /// + /// Gets and sets the podcast description of the song. + /// + /// + /// A object containing the podcast description of the song. + /// + /// + /// This property is implemented using the "TDES" field. + /// This property is supported in version 3 forward. + /// + public string PodcastDescription { + get { return GetTextAsString (FrameType.TDES); } + set { + if (Version < 3) + throw new InvalidOperationException("Version must be at least 3."); + + SetTextFrame (FrameType.TDES, value); + } + } + /// /// Gets whether or not the current instance is empty. ///