Skip to content

Commit 6b6373d

Browse files
committed
Fetch lyrics online, edit custom metadata & way more
- Added an "Upload lyrics" dialog, where the user can upload a local LRC/TTML/Plain text file, or fetch the lyrics from LRCLib * Lyrics can be added to the currently-selected track, to all the tracks (if a match is found) or to the tracks that still don't have lyrics added - Updated the "Custom metadata" dialog * It's now possible to add subtags to MKV files * All the added metadata, including custom ones, are now shown, and the user can change both the key and the value * The user can also export them in a JSON or CSV (really experimental) format - Added the option to export album arts in a zip file * The user can save the single image, all the images embedded in the selected file, or all the images embedded in every loaded file - Improved metadata copying * The user can now copy metadata from files that have the same album * The user can exclude track-specific metadata from the ones to copy * Custom metadata can now also be copied - The user can now edit a single metadata for all files in the same album * The user can also choose to edit these fields only if they aren't specific to each track - Progress in many areas is now shown with a custom Toast, that displays a progress bar - Blazor's emulated file system is now used for storing files instead of the FileAbstraction * This improves the stability of TagLib-Sharp in certain cases * Changed the upload method, using native Fluent controls and enabling drop support for files * Now, uploading directories keep their folder structure [] The same folder structure is applied while saving zip files or directly on the user's drive - Added an alert before closing the webpage - Added "FrontCover" as the default picture type in the `Add album art` dialog - Updated the UI in various places - Other improvements that I currently don't remember - Bumped version to 2.0.0
1 parent 6e3a1ae commit 6b6373d

34 files changed

+2430
-378
lines changed

.DS_Store

0 Bytes
Binary file not shown.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
bin
22
obj
3-
publish
3+
publish
4+
taglibsharp-web.sln
5+
.DS_Store

App.razor

Lines changed: 267 additions & 127 deletions
Large diffs are not rendered by default.

CustomMetadataType.cs

Lines changed: 492 additions & 0 deletions
Large diffs are not rendered by default.

JSONExportationOptions.cs

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
using System.Text;
2+
using Newtonsoft.Json;
3+
using Newtonsoft.Json.Serialization;
4+
5+
namespace MetadataChange
6+
{
7+
/// <summary>
8+
/// Remove byte arrays when serializing the JSON
9+
/// </summary>
10+
/// <param name="AdvancedMetadata">What is being exported</param>
11+
public class IgnoreByteArrayResolver(MetadataExportType AdvancedMetadata) : DefaultContractResolver
12+
{
13+
protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
14+
{
15+
var property = base.CreateProperty(member, memberSerialization);
16+
// We'll have a lot of if switches here, but we just need to change the Converter to include metadata in various tags. And also, we need to remove byte array references.
17+
if (property.PropertyName == "FrameId" || (property.PropertyName == "BoxType" && property.PropertyType == typeof(TagLib.ByteVector)))
18+
{
19+
property.Converter = new ByteVectorToStringConverter();
20+
}
21+
else if (property.PropertyName == "BoxType" && property.PropertyType == typeof(TagLib.ByteVector[]))
22+
{
23+
property.Converter = new ByteVectorDoubleArrayToStringConverter();
24+
}
25+
else if (property.PropertyType == typeof(IEnumerable<TagLib.Ogg.XiphComment>))
26+
{
27+
property.Converter = new XiphCommentConverter(true);
28+
29+
}
30+
else if (property.PropertyType == typeof(TagLib.Asf.Tag))
31+
{
32+
property.Converter = new AsfCommentConverter();
33+
}
34+
else if (property.PropertyType == typeof(Dictionary<string, List<TagLib.Matroska.SimpleTag>>))
35+
{
36+
property.Converter = new MatroskaSimpleTagConverter();
37+
}
38+
else if (property.PropertyType == typeof(byte[]) || property.PropertyType == typeof(TagLib.ByteVector) || (AdvancedMetadata != MetadataExportType.ALL_APP && property.PropertyType == typeof(TagLib.NonContainer.StartTag)) || (AdvancedMetadata != MetadataExportType.ALL_APP && property.PropertyType == typeof(TagLib.NonContainer.EndTag) || (AdvancedMetadata == MetadataExportType.COMMON && property.PropertyType == typeof(TagLib.Tag[]))))
39+
{
40+
property.ShouldSerialize = _ => false;
41+
}
42+
else if (property.PropertyType != null && !property.PropertyType.IsPrimitive && property.PropertyType != typeof(string))
43+
{
44+
property.Converter = null; // Prevent Newtonsoft from caching wrong converter
45+
}
46+
return property;
47+
}
48+
49+
}
50+
51+
/// <summary>
52+
/// Only fixes serialization of some TagLib libraries. This is done also by the other two ContractResolvers. Without this, metadata values of certain tags might be lost.
53+
/// It also deletes StartTag and EndTag if not required.
54+
/// </summary>
55+
/// <param name="AdvancedMetadata">What is being exported</param>
56+
public class StandardJsonResolver(MetadataExportType AdvancedMetadata) : DefaultContractResolver
57+
{
58+
protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
59+
{
60+
var property = base.CreateProperty(member, memberSerialization);
61+
if (property.PropertyName == "FrameId" || (property.PropertyName == "BoxType" && property.PropertyType == typeof(TagLib.ByteVector)))
62+
{
63+
property.Converter = new ByteVectorToStringConverter();
64+
}
65+
else if (property.PropertyName == "BoxType" && property.PropertyType == typeof(TagLib.ByteVector[]))
66+
{
67+
property.Converter = new ByteVectorDoubleArrayToStringConverter();
68+
}
69+
else if (property.PropertyType == typeof(IEnumerable<TagLib.Ogg.XiphComment>))
70+
{
71+
property.Converter = new XiphCommentConverter(false);
72+
73+
}
74+
else if (property.PropertyType == typeof(Dictionary<string, List<TagLib.Matroska.SimpleTag>>))
75+
{
76+
property.Converter = new MatroskaSimpleTagConverter();
77+
}
78+
else if (property.PropertyType == typeof(TagLib.Asf.Tag))
79+
{
80+
property.Converter = new AsfCommentConverter();
81+
}
82+
else if (AdvancedMetadata != MetadataExportType.ALL_APP && (property.PropertyType == typeof(TagLib.NonContainer.StartTag) || property.PropertyType == typeof(TagLib.NonContainer.EndTag)) || (AdvancedMetadata == MetadataExportType.COMMON && property.PropertyType == typeof(TagLib.Tag[])))
83+
{
84+
property.ShouldSerialize = _ => false;
85+
}
86+
return property;
87+
}
88+
}
89+
90+
91+
/// <summary>
92+
/// Convert byte arrays when serializing the JSON
93+
/// </summary>
94+
/// <param name="AdvancedMetadata">What is being exported</param>
95+
public class ByteArrayBase64Resolver(MetadataExportType AdvancedMetadata) : DefaultContractResolver
96+
{
97+
protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
98+
{
99+
var property = base.CreateProperty(member, memberSerialization);
100+
Console.WriteLine(property.PropertyName);
101+
Console.WriteLine(property.PropertyType);
102+
if (property.PropertyName == "FrameId" || (property.PropertyName == "BoxType" && property.PropertyType == typeof(TagLib.ByteVector)))
103+
{
104+
property.Converter = new ByteVectorToStringConverter();
105+
}
106+
else if (property.PropertyName == "BoxType" && property.PropertyType == typeof(TagLib.ByteVector[]))
107+
{
108+
property.Converter = new ByteVectorDoubleArrayToStringConverter();
109+
}
110+
else if (property.PropertyType == typeof(IEnumerable<TagLib.Ogg.XiphComment>))
111+
{
112+
property.Converter = new XiphCommentConverter(false);
113+
114+
}
115+
else if (property.PropertyType == typeof(Dictionary<string, List<TagLib.Matroska.SimpleTag>>))
116+
{
117+
property.Converter = new MatroskaSimpleTagConverter();
118+
}
119+
else if (property.PropertyType == typeof(TagLib.Asf.Tag))
120+
{
121+
property.Converter = new AsfCommentConverter();
122+
}
123+
else if (AdvancedMetadata != MetadataExportType.ALL_APP && (property.PropertyType == typeof(TagLib.NonContainer.StartTag) || property.PropertyType == typeof(TagLib.NonContainer.EndTag)) || (AdvancedMetadata == MetadataExportType.COMMON && property.PropertyType == typeof(TagLib.Tag[])))
124+
{
125+
property.ShouldSerialize = _ => false;
126+
}
127+
if (property.PropertyType == typeof(byte[]))
128+
{
129+
property.Converter = new ByteArrayToBase64Converter();
130+
}
131+
else if (property.PropertyType == typeof(TagLib.ByteVector))
132+
{
133+
property.Converter = new TagLibByteVectorToBase64Converter();
134+
}
135+
return property;
136+
}
137+
}
138+
139+
/// <summary>
140+
/// Convert a Byte Array to a base64
141+
/// </summary>
142+
public class ByteArrayToBase64Converter : JsonConverter<byte[]>
143+
{
144+
public override void WriteJson(JsonWriter writer, byte[] value, JsonSerializer serializer)
145+
{
146+
writer.WriteValue(Convert.ToBase64String(value));
147+
}
148+
149+
public override byte[] ReadJson(JsonReader reader, Type objectType, byte[] existingValue, bool hasExistingValue, JsonSerializer serializer)
150+
{
151+
return [];
152+
}
153+
}
154+
/// <summary>
155+
/// Decode a byte vector to a string
156+
/// </summary>
157+
public class ByteVectorToStringConverter : JsonConverter<TagLib.ByteVector>
158+
{
159+
public override void WriteJson(JsonWriter writer, TagLib.ByteVector value, JsonSerializer serializer)
160+
{
161+
writer.WriteValue(Encoding.UTF8.GetString(value.ToArray()));
162+
}
163+
164+
public override TagLib.ByteVector ReadJson(JsonReader reader, Type objectType, TagLib.ByteVector existingValue, bool hasExistingValue, JsonSerializer serializer)
165+
{
166+
return [];
167+
}
168+
}
169+
/// <summary>
170+
/// Convert an array of ByteVectors to a String
171+
/// </summary>
172+
public class ByteVectorDoubleArrayToStringConverter : JsonConverter<TagLib.ByteVector[]>
173+
{
174+
public override void WriteJson(JsonWriter writer, TagLib.ByteVector[] value, JsonSerializer serializer)
175+
{
176+
string[] output = value.Select(i => Encoding.UTF8.GetString(i.ToArray())).ToArray();
177+
writer.WriteValue(output);
178+
}
179+
180+
public override TagLib.ByteVector[] ReadJson(JsonReader reader, Type objectType, TagLib.ByteVector[] existingValue, bool hasExistingValue, JsonSerializer serializer)
181+
{
182+
return [[]];
183+
}
184+
}
185+
186+
/// <summary>
187+
/// Convert a Matroska SimpleTag to a serializable Dictionary of keys and values.
188+
/// In case subkeys are there, the key will be a JSON-stringified array with the tag and the subtag.
189+
/// </summary>
190+
public class MatroskaSimpleTagConverter : JsonConverter<Dictionary<String, List<TagLib.Matroska.SimpleTag>>>
191+
{
192+
public override void WriteJson(JsonWriter writer, Dictionary<String, List<TagLib.Matroska.SimpleTag>> value, JsonSerializer serializer)
193+
{
194+
Dictionary<string, string> output = [];
195+
foreach (var singleTag in value)
196+
{
197+
foreach (var tag in singleTag.Value)
198+
{
199+
if (tag.SimpleTags == null) output[singleTag.Key] = Encoding.UTF8.GetString(tag.Value.ToArray()); // Simple key/value metadata
200+
else
201+
{
202+
foreach (var innerTags in tag.SimpleTags) // Metadata with key, subkey, and value
203+
{
204+
output[Newtonsoft.Json.JsonConvert.SerializeObject(new string[] { singleTag.Key, innerTags.Key })] = Encoding.UTF8.GetString(innerTags.Value.First().Value.ToArray());
205+
}
206+
}
207+
}
208+
}
209+
serializer.Serialize(writer, output);
210+
}
211+
212+
public override Dictionary<string, List<TagLib.Matroska.SimpleTag>> ReadJson(JsonReader reader, Type objectType, Dictionary<String, List<TagLib.Matroska.SimpleTag>> existingValue, bool hasExistingValue, JsonSerializer serializer)
213+
{
214+
return [];
215+
}
216+
}
217+
218+
/// <summary>
219+
/// Convert Xiph comments to a serializable dictionary
220+
/// </summary>
221+
/// <param name="noBinary"></param>
222+
public class XiphCommentConverter(bool noBinary) : JsonConverter<IEnumerable<TagLib.Ogg.XiphComment>>
223+
{
224+
public override void WriteJson(JsonWriter writer, IEnumerable<TagLib.Ogg.XiphComment> value, JsonSerializer serializer)
225+
{
226+
Dictionary<string, string[]> output = [];
227+
foreach (TagLib.Ogg.XiphComment comment in value)
228+
{
229+
foreach (var key in comment)
230+
{
231+
if (key == "METADATA_BLOCK_PICTURE" && noBinary) continue;
232+
output[key] = comment.GetField(key);
233+
}
234+
}
235+
serializer.Serialize(writer, output);
236+
}
237+
238+
public override IEnumerable<TagLib.Ogg.XiphComment> ReadJson(JsonReader reader, Type objectType, IEnumerable<TagLib.Ogg.XiphComment> existingValue, bool hasExistingValue, JsonSerializer serializer)
239+
{
240+
return [];
241+
}
242+
}
243+
244+
/// <summary>
245+
/// Convert ASF tags in a serializable dictionarues
246+
/// </summary>
247+
public class AsfCommentConverter : JsonConverter<TagLib.Asf.Tag>
248+
{
249+
public override void WriteJson(JsonWriter writer, TagLib.Asf.Tag value, JsonSerializer serializer)
250+
{
251+
Dictionary<string, string> output = [];
252+
foreach (var comment in value) output[comment.Name] = comment.ToString();
253+
serializer.Serialize(writer, output);
254+
}
255+
256+
public override TagLib.Asf.Tag ReadJson(JsonReader reader, Type objectType, TagLib.Asf.Tag existingValue, bool hasExistingValue, JsonSerializer serializer)
257+
{
258+
return null;
259+
}
260+
}
261+
262+
263+
264+
/// <summary>
265+
/// Convert a TagLibVector to a Base64 string
266+
/// </summary>
267+
public class TagLibByteVectorToBase64Converter : JsonConverter<TagLib.ByteVector>
268+
{
269+
public override void WriteJson(JsonWriter writer, TagLib.ByteVector value, JsonSerializer serializer)
270+
{
271+
writer.WriteValue(Convert.ToBase64String(value.ToArray()));
272+
}
273+
274+
public override TagLib.ByteVector ReadJson(JsonReader reader, Type objectType, TagLib.ByteVector existingValue, bool hasExistingValue, JsonSerializer serializer)
275+
{
276+
return [];
277+
}
278+
}
279+
280+
/// <summary>
281+
/// What should be exported in the output JSON/CSV metadata file.
282+
/// </summary>
283+
public enum MetadataExportType
284+
{
285+
/// <summary>
286+
/// Common files fetched by TagLib
287+
/// </summary>
288+
COMMON = 0,
289+
/// <summary>
290+
/// Everything fetched by TagLib
291+
/// </summary>
292+
ALL_TAGLIB = 1,
293+
/// <summary>
294+
/// Everything fetched by TagLib, but in the container-specific syntax
295+
/// </summary>
296+
ALL_TAGLIB_SPECIFIC_ONLY = 2,
297+
/// <summary>
298+
/// Everything fetched by TagLib, plus the custom metadata array created by TagLibSharp-Web
299+
/// </summary>
300+
ALL_APP = 3
301+
}
302+
303+
}

LRCLibJson.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace MetadataChange
2+
{
3+
/// <summary>
4+
/// Content of the response obtained from LRCLib.
5+
/// </summary>
6+
public class LRCLibJson
7+
{
8+
public int? id { get; set; }
9+
public string? trackName { get; set; }
10+
public string? artistName { get; set; }
11+
public string? albumName { get; set; }
12+
public float? duration { get; set; }
13+
public bool? instrumental { get; set; }
14+
public string? plainLyrics { get; set; }
15+
public string? syncedLyrics { get; set; }
16+
}
17+
}

0 commit comments

Comments
 (0)