Skip to main content

libflo_audio/core/
metadata.rs

1//! flo Metadata
2//!
3//! Supports most commonly used ID3v2.4 fields plus flo-unique extensions
4//! Uses MessagePack serialization for efficiency and flexibility
5
6extern crate alloc;
7
8use alloc::collections::BTreeMap;
9use alloc::string::{String, ToString};
10use alloc::vec;
11use alloc::vec::Vec;
12use serde::{Deserialize, Serialize};
13
14// ============================================================================
15// Picture Types (ID3v2.4 APIC)
16// ============================================================================
17
18/// Picture type (matches ID3v2.4 APIC picture types)
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
20#[serde(rename_all = "snake_case")]
21pub enum PictureType {
22    Other,
23    FileIcon, // 32x32 PNG only
24    OtherFileIcon,
25    #[default]
26    CoverFront,
27    CoverBack,
28    LeafletPage,
29    Media, // e.g. label side of CD
30    LeadArtist,
31    Artist,
32    Conductor,
33    Band,
34    Composer,
35    Lyricist,
36    RecordingLocation,
37    DuringRecording,
38    DuringPerformance,
39    VideoScreenCapture,
40    BrightColouredFish, // Yes, this is real in ID3v2.4 🐟
41    Illustration,
42    BandLogo,
43    PublisherLogo,
44}
45
46/// Attached picture (album art, etc.)
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Picture {
49    /// MIME type (e.g., "image/jpeg", "image/png")
50    pub mime_type: String,
51    /// Picture type
52    pub picture_type: PictureType,
53    /// Description
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub description: Option<String>,
56    /// Binary picture data
57    #[serde(with = "serde_bytes")]
58    pub data: Vec<u8>,
59}
60
61// ============================================================================
62// Text Structures
63// ============================================================================
64
65/// Comment with optional language and description (COMM)
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Comment {
68    /// ISO-639-2 language code (e.g., "eng", "jpn")
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub language: Option<String>,
71    /// Short content description
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub description: Option<String>,
74    /// The actual comment text
75    pub text: String,
76}
77
78/// Unsynchronized lyrics (USLT)
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Lyrics {
81    /// ISO-639-2 language code
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub language: Option<String>,
84    /// Content description
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub description: Option<String>,
87    /// Lyrics text
88    pub text: String,
89}
90
91/// Synchronized lyrics content type (SYLT)
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
93#[serde(rename_all = "snake_case")]
94pub enum SyncedLyricsContentType {
95    Other,
96    #[default]
97    Lyrics,
98    TextTranscription,
99    PartName, // e.g., "Adagio"
100    Events,   // e.g., "Don Quijote enters the stage"
101    Chord,    // e.g., "Bb F Fsus"
102    Trivia,   // Pop-up information
103    WebpageUrl,
104    ImageUrl,
105}
106
107/// A single line of synchronized lyrics with timestamp
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct SyncedLyricsLine {
110    /// Timestamp in milliseconds from start
111    pub timestamp_ms: u64,
112    /// Text/syllable at this timestamp
113    pub text: String,
114}
115
116/// Synchronized lyrics/text (SYLT): flo first-party support!
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct SyncedLyrics {
119    /// ISO-639-2 language code
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub language: Option<String>,
122    /// Content type
123    #[serde(default)]
124    pub content_type: SyncedLyricsContentType,
125    /// Content description
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub description: Option<String>,
128    /// Lines with timestamps
129    pub lines: Vec<SyncedLyricsLine>,
130}
131
132/// User-defined text field (TXXX)
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct UserText {
135    /// Description/key
136    pub description: String,
137    /// Value
138    pub value: String,
139}
140
141/// User-defined URL (WXXX)
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct UserUrl {
144    /// Description
145    pub description: String,
146    /// URL
147    pub url: String,
148}
149
150/// Popularimeter (POPM)
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct Popularimeter {
153    /// Email/identifier of the user
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub email: Option<String>,
156    /// Rating (1-255, 0 = unknown)
157    pub rating: u8,
158    /// Play counter
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub play_count: Option<u64>,
161}
162
163// ============================================================================
164// flo-Unique Structures
165// ============================================================================
166
167/// Pre-computed waveform data for instant visualization
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct WaveformData {
170    /// Number of peak values per second of audio
171    pub peaks_per_second: u32,
172    /// Peak values (0.0 to 1.0): for stereo, interleaved L/R or combined
173    pub peaks: Vec<f32>,
174    /// Number of channels in peaks (1 = mono/combined, 2 = stereo)
175    #[serde(default = "default_one")]
176    pub channels: u8,
177}
178
179fn default_one() -> u8 {
180    1
181}
182
183/// Section type for track structure
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "snake_case")]
186pub enum SectionType {
187    Intro,
188    Verse,
189    PreChorus,
190    Chorus,
191    PostChorus,
192    Bridge,
193    Breakdown,
194    Drop,
195    Buildup,
196    Solo,
197    Instrumental,
198    Outro,
199    Silence,
200    Other,
201}
202
203/// Section marker with timestamp and label
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct SectionMarker {
206    /// Timestamp in milliseconds
207    pub timestamp_ms: u64,
208    /// Section type
209    pub section_type: SectionType,
210    /// Optional custom label (e.g., "Verse 2", "Guitar Solo")
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub label: Option<String>,
213}
214
215/// BPM change point for tempo mapping
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct BpmChange {
218    /// Timestamp in milliseconds
219    pub timestamp_ms: u64,
220    /// BPM at this point (supports fractional BPM)
221    pub bpm: f32,
222}
223
224/// Key change point
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct KeyChange {
227    /// Timestamp in milliseconds
228    pub timestamp_ms: u64,
229    /// Musical key (e.g., "Am", "F#m", "Bb")
230    pub key: String,
231}
232
233/// Loudness measurement point for dynamic visualization
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct LoudnessPoint {
236    /// Timestamp in milliseconds
237    pub timestamp_ms: u64,
238    /// Loudness in LUFS
239    pub lufs: f32,
240}
241
242/// Creator/producer note with optional timestamp
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct CreatorNote {
245    /// Optional timestamp (None = applies to whole track)
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub timestamp_ms: Option<u64>,
248    /// Note text
249    pub text: String,
250}
251
252/// Collaboration credit with role and optional timestamp
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct CollaborationCredit {
255    /// Role (e.g., "Lead Vocals", "Bass Guitar", "Mixing")
256    pub role: String,
257    /// Person's name
258    pub name: String,
259    /// Optional timestamp for when they appear (e.g., guitar solo at 2:00)
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub timestamp_ms: Option<u64>,
262}
263
264/// Entry in remix/sample chain for tracking lineage
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct RemixChainEntry {
267    /// Original track title
268    pub title: String,
269    /// Original artist
270    pub artist: String,
271    /// Year of original (if known)
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub year: Option<u32>,
274    /// ISRC of original (if known)
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub isrc: Option<String>,
277    /// Relationship type: "original", "remix", "sample", "cover", "mashup"
278    pub relationship: String,
279}
280
281/// Animated cover art (GIF, animated WebP, or short video)
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct AnimatedCover {
284    /// MIME type (image/gif, image/webp, video/mp4)
285    pub mime_type: String,
286    /// Binary data
287    #[serde(with = "serde_bytes")]
288    pub data: Vec<u8>,
289    /// Duration in milliseconds (if applicable)
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub duration_ms: Option<u32>,
292    /// Loop count (0 = infinite, None = play once)
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub loop_count: Option<u32>,
295}
296
297/// Cover variant type
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(rename_all = "snake_case")]
300pub enum CoverVariantType {
301    Standard,
302    Explicit,
303    Clean,
304    Remix,
305    Deluxe,
306    Limited,
307    Vinyl,
308    Cassette,
309    Digital,
310    Other,
311}
312
313/// Alternative cover art variant
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct CoverVariant {
316    /// Variant type
317    pub variant_type: CoverVariantType,
318    /// MIME type
319    pub mime_type: String,
320    /// Binary data
321    #[serde(with = "serde_bytes")]
322    pub data: Vec<u8>,
323    /// Description
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub description: Option<String>,
326}
327
328// ============================================================================
329// Main Metadata Structure
330// ============================================================================
331
332#[derive(Debug, Clone, Default, Serialize, Deserialize)]
333pub struct FloMetadata {
334    // ==================== IDENTIFICATION ====================
335    /// Title/songname (TIT2)
336    #[serde(default, skip_serializing_if = "Option::is_none")]
337    pub title: Option<String>,
338
339    /// Subtitle/description refinement (TIT3)
340    #[serde(default, skip_serializing_if = "Option::is_none")]
341    pub subtitle: Option<String>,
342
343    /// Content group description (TIT1)
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub content_group: Option<String>,
346
347    /// Album/movie/show title (TALB)
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub album: Option<String>,
350
351    /// Original album (TOAL)
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub original_album: Option<String>,
354
355    /// Set subtitle (TSST)
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub set_subtitle: Option<String>,
358
359    /// Track number (TRCK)
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub track_number: Option<u32>,
362
363    /// Total tracks
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub track_total: Option<u32>,
366
367    /// Disc number (TPOS)
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub disc_number: Option<u32>,
370
371    /// Total discs
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub disc_total: Option<u32>,
374
375    /// ISRC code (TSRC)
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub isrc: Option<String>,
378
379    // ==================== INVOLVED PERSONS ====================
380    /// Lead artist/performer (TPE1)
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub artist: Option<String>,
383
384    /// Album artist/band (TPE2)
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub album_artist: Option<String>,
387
388    /// Conductor (TPE3)
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub conductor: Option<String>,
391
392    /// Remixer/modifier (TPE4)
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub remixer: Option<String>,
395
396    /// Original artist (TOPE)
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub original_artist: Option<String>,
399
400    /// Composer (TCOM)
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub composer: Option<String>,
403
404    /// Lyricist/text writer (TEXT)
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    pub lyricist: Option<String>,
407
408    /// Original lyricist (TOLY)
409    #[serde(default, skip_serializing_if = "Option::is_none")]
410    pub original_lyricist: Option<String>,
411
412    /// Encoded by (TENC)
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    pub encoded_by: Option<String>,
415
416    /// Involved people list (TIPL)
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub involved_people: Option<Vec<(String, String)>>,
419
420    /// Musician credits (TMCL)
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub musician_credits: Option<Vec<(String, String)>>,
423
424    // ==================== PROPERTIES ====================
425    /// Genre (TCON)
426    #[serde(default, skip_serializing_if = "Option::is_none")]
427    pub genre: Option<String>,
428
429    /// Mood (TMOO)
430    #[serde(default, skip_serializing_if = "Option::is_none")]
431    pub mood: Option<String>,
432
433    /// BPM (TBPM)
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    pub bpm: Option<u32>,
436
437    /// Initial musical key (TKEY)
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    pub key: Option<String>,
440
441    /// Language (TLAN)
442    #[serde(default, skip_serializing_if = "Option::is_none")]
443    pub language: Option<String>,
444
445    /// Length in milliseconds (TLEN)
446    #[serde(default, skip_serializing_if = "Option::is_none")]
447    pub length_ms: Option<u64>,
448
449    // ==================== DATES/TIMES ====================
450    /// Year
451    #[serde(default, skip_serializing_if = "Option::is_none")]
452    pub year: Option<u32>,
453
454    /// Recording time (TDRC)
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub recording_time: Option<String>,
457
458    /// Release time (TDRL)
459    #[serde(default, skip_serializing_if = "Option::is_none")]
460    pub release_time: Option<String>,
461
462    /// Original release time (TDOR)
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub original_release_time: Option<String>,
465
466    /// Encoding time (TDEN)
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub encoding_time: Option<String>,
469
470    /// Tagging time (TDTG)
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub tagging_time: Option<String>,
473
474    // ==================== RIGHTS/LICENSE ====================
475    /// Copyright message (TCOP)
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub copyright: Option<String>,
478
479    /// Production copyright (TPRO)
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub produced_notice: Option<String>,
482
483    /// Publisher (TPUB)
484    #[serde(default, skip_serializing_if = "Option::is_none")]
485    pub publisher: Option<String>,
486
487    /// File owner/licensee (TOWN)
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub file_owner: Option<String>,
490
491    /// Internet radio station name (TRSN)
492    #[serde(default, skip_serializing_if = "Option::is_none")]
493    pub radio_station: Option<String>,
494
495    /// Internet radio station owner (TRSO)
496    #[serde(default, skip_serializing_if = "Option::is_none")]
497    pub radio_station_owner: Option<String>,
498
499    // ==================== SORT ORDER ====================
500    /// Album sort order (TSOA)
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub album_sort: Option<String>,
503
504    /// Performer sort order (TSOP)
505    #[serde(default, skip_serializing_if = "Option::is_none")]
506    pub artist_sort: Option<String>,
507
508    /// Title sort order (TSOT)
509    #[serde(default, skip_serializing_if = "Option::is_none")]
510    pub title_sort: Option<String>,
511
512    // ==================== OTHER TEXT ====================
513    /// Original filename (TOFN)
514    #[serde(default, skip_serializing_if = "Option::is_none")]
515    pub original_filename: Option<String>,
516
517    /// Playlist delay in ms (TDLY)
518    #[serde(default, skip_serializing_if = "Option::is_none")]
519    pub playlist_delay: Option<u32>,
520
521    /// Encoder software/settings (TSSE)
522    #[serde(default, skip_serializing_if = "Option::is_none")]
523    pub encoder_settings: Option<String>,
524
525    // ==================== URLS ====================
526    /// Commercial info URL (WCOM)
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub url_commercial: Option<String>,
529
530    /// Copyright/legal URL (WCOP)
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    pub url_copyright: Option<String>,
533
534    /// Official audio file URL (WOAF)
535    #[serde(default, skip_serializing_if = "Option::is_none")]
536    pub url_audio_file: Option<String>,
537
538    /// Official artist URL (WOAR)
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    pub url_artist: Option<String>,
541
542    /// Official audio source URL (WOAS)
543    #[serde(default, skip_serializing_if = "Option::is_none")]
544    pub url_audio_source: Option<String>,
545
546    /// Official radio station URL (WORS)
547    #[serde(default, skip_serializing_if = "Option::is_none")]
548    pub url_radio_station: Option<String>,
549
550    /// Payment URL (WPAY)
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub url_payment: Option<String>,
553
554    /// Publisher URL (WPUB)
555    #[serde(default, skip_serializing_if = "Option::is_none")]
556    pub url_publisher: Option<String>,
557
558    /// User-defined URLs (WXXX)
559    #[serde(default, skip_serializing_if = "Vec::is_empty")]
560    pub user_urls: Vec<UserUrl>,
561
562    // ==================== COMPLEX FRAMES ====================
563    /// Comments (COMM)
564    #[serde(default, skip_serializing_if = "Vec::is_empty")]
565    pub comments: Vec<Comment>,
566
567    /// Unsynchronized lyrics (USLT)
568    #[serde(default, skip_serializing_if = "Vec::is_empty")]
569    pub lyrics: Vec<Lyrics>,
570
571    /// Synchronized lyrics (SYLT)
572    #[serde(default, skip_serializing_if = "Vec::is_empty")]
573    pub synced_lyrics: Vec<SyncedLyrics>,
574
575    /// Attached pictures (APIC)
576    #[serde(default, skip_serializing_if = "Vec::is_empty")]
577    pub pictures: Vec<Picture>,
578
579    /// User-defined text (TXXX)
580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
581    pub user_text: Vec<UserText>,
582
583    /// Play counter (PCNT)
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub play_count: Option<u64>,
586
587    /// Popularimeter/rating (POPM)
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    pub popularimeter: Option<Popularimeter>,
590
591    // ==================== VISUALIZATION (flo-unique) ====================
592    /// Pre-computed waveform peaks for instant visualization
593    #[serde(default, skip_serializing_if = "Option::is_none")]
594    pub waveform_data: Option<WaveformData>,
595
596    /// Spectral analysis data / audio fingerprint for visual EQ
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    #[serde(with = "serde_bytes_option")]
599    pub spectrum_fingerprint: Option<Vec<u8>>,
600
601    // ==================== TIMING & ANALYSIS (flo-unique) ====================
602    /// BPM changes throughout the track
603    #[serde(default, skip_serializing_if = "Vec::is_empty")]
604    pub bpm_map: Vec<BpmChange>,
605
606    /// Musical key changes with timestamps
607    #[serde(default, skip_serializing_if = "Vec::is_empty")]
608    pub key_changes: Vec<KeyChange>,
609
610    /// Frame-by-frame loudness profile (ReplayGain dynamic)
611    #[serde(default, skip_serializing_if = "Vec::is_empty")]
612    pub loudness_profile: Vec<LoudnessPoint>,
613
614    /// Integrated loudness (LUFS): EBU R128
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub integrated_loudness_lufs: Option<f32>,
617
618    /// Loudness range (LU)
619    #[serde(default, skip_serializing_if = "Option::is_none")]
620    pub loudness_range_lu: Option<f32>,
621
622    /// True peak (dBTP)
623    #[serde(default, skip_serializing_if = "Option::is_none")]
624    pub true_peak_dbtp: Option<f32>,
625
626    /// Section markers (intro/verse/chorus/etc.)
627    #[serde(default, skip_serializing_if = "Vec::is_empty")]
628    pub section_markers: Vec<SectionMarker>,
629
630    // ==================== CREATOR INFO (flo-unique) ====================
631    /// Producer commentary with timestamps
632    #[serde(default, skip_serializing_if = "Vec::is_empty")]
633    pub creator_notes: Vec<CreatorNote>,
634
635    /// Detailed collaboration credits
636    #[serde(default, skip_serializing_if = "Vec::is_empty")]
637    pub collaboration_credits: Vec<CollaborationCredit>,
638
639    /// Remix/sample lineage chain
640    #[serde(default, skip_serializing_if = "Vec::is_empty")]
641    pub remix_chain: Vec<RemixChainEntry>,
642
643    // ==================== COVERS (flo-unique) ====================
644    /// Animated cover art (GIF/WebP/short video)
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub animated_cover: Option<AnimatedCover>,
647
648    /// Alternative cover variants (explicit, remix, etc.)
649    #[serde(default, skip_serializing_if = "Vec::is_empty")]
650    pub cover_variants: Vec<CoverVariant>,
651
652    /// Artist signature image
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub artist_signature: Option<Picture>,
655
656    // ==================== flo-SPECIFIC ====================
657    /// flo encoder version used
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub flo_encoder_version: Option<String>,
660
661    /// Source format (e.g., "MP3", "FLAC", "WAV")
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub source_format: Option<String>,
664
665    /// Custom key-value pairs for extensions
666    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
667    pub custom: BTreeMap<String, String>,
668}
669
670// Helper for Option<Vec<u8>> serialization
671mod serde_bytes_option {
672    use alloc::vec::Vec;
673    use serde::{Deserialize, Deserializer, Serialize, Serializer};
674
675    pub fn serialize<S>(data: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
676    where
677        S: Serializer,
678    {
679        match data {
680            Some(bytes) => serde_bytes::serialize(bytes, serializer),
681            None => Option::<&[u8]>::None.serialize(serializer),
682        }
683    }
684
685    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
686    where
687        D: Deserializer<'de>,
688    {
689        let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(deserializer)?;
690        Ok(opt.map(|b| b.into_vec()))
691    }
692}
693
694impl FloMetadata {
695    /// Create empty metadata
696    pub fn new() -> Self {
697        Self::default()
698    }
699
700    /// Create metadata with basic fields
701    pub fn with_basic(
702        title: Option<String>,
703        artist: Option<String>,
704        album: Option<String>,
705    ) -> Self {
706        Self {
707            title,
708            artist,
709            album,
710            ..Default::default()
711        }
712    }
713
714    /// Serialize to MessagePack bytes
715    pub fn to_msgpack(
716        &self,
717    ) -> Result<Vec<u8>, messagepack_serde::ser::Error<core::convert::Infallible>> {
718        messagepack_serde::to_vec(self)
719    }
720
721    /// Deserialize from MessagePack bytes
722    pub fn from_msgpack(
723        data: &[u8],
724    ) -> Result<Self, messagepack_serde::de::Error<messagepack_serde::messagepack_core::io::RError>>
725    {
726        messagepack_serde::from_slice(data)
727    }
728
729    /// Check if metadata is empty (no significant fields set)
730    pub fn is_empty(&self) -> bool {
731        self.title.is_none()
732            && self.artist.is_none()
733            && self.album.is_none()
734            && self.pictures.is_empty()
735            && self.comments.is_empty()
736            && self.lyrics.is_empty()
737            && self.synced_lyrics.is_empty()
738    }
739
740    // ==================== PICTURE HELPERS ====================
741
742    /// Add a picture
743    pub fn add_picture(&mut self, mime_type: &str, picture_type: PictureType, data: Vec<u8>) {
744        self.pictures.push(Picture {
745            mime_type: mime_type.to_string(),
746            picture_type,
747            description: None,
748            data,
749        });
750    }
751
752    /// Get the front cover picture if present
753    pub fn front_cover(&self) -> Option<&Picture> {
754        self.pictures
755            .iter()
756            .find(|p| p.picture_type == PictureType::CoverFront)
757    }
758
759    /// Get the first picture of any type
760    pub fn any_picture(&self) -> Option<&Picture> {
761        self.pictures.first()
762    }
763
764    // ==================== TEXT HELPERS ====================
765
766    /// Add a comment
767    pub fn add_comment(&mut self, text: &str, language: Option<&str>) {
768        self.comments.push(Comment {
769            language: language.map(|s| s.to_string()),
770            description: None,
771            text: text.to_string(),
772        });
773    }
774
775    /// Add unsynchronized lyrics
776    pub fn add_lyrics(&mut self, text: &str, language: Option<&str>) {
777        self.lyrics.push(Lyrics {
778            language: language.map(|s| s.to_string()),
779            description: None,
780            text: text.to_string(),
781        });
782    }
783
784    /// Add synchronized lyrics line
785    pub fn add_synced_lyrics_line(
786        &mut self,
787        timestamp_ms: u64,
788        text: &str,
789        language: Option<&str>,
790    ) {
791        let lang = language.map(|s| s.to_string());
792        if let Some(synced) = self.synced_lyrics.iter_mut().find(|s| s.language == lang) {
793            synced.lines.push(SyncedLyricsLine {
794                timestamp_ms,
795                text: text.to_string(),
796            });
797        } else {
798            self.synced_lyrics.push(SyncedLyrics {
799                language: lang,
800                content_type: SyncedLyricsContentType::Lyrics,
801                description: None,
802                lines: vec![SyncedLyricsLine {
803                    timestamp_ms,
804                    text: text.to_string(),
805                }],
806            });
807        }
808    }
809
810    // ==================== CUSTOM FIELD HELPERS ====================
811
812    /// Set a custom field
813    pub fn set_custom(&mut self, key: &str, value: &str) {
814        self.custom.insert(key.to_string(), value.to_string());
815    }
816
817    /// Get a custom field
818    pub fn get_custom(&self, key: &str) -> Option<&str> {
819        self.custom.get(key).map(|s| s.as_str())
820    }
821
822    // ==================== HELPERS (flo-unique) ====================
823
824    /// Add a section marker
825    pub fn add_section(
826        &mut self,
827        timestamp_ms: u64,
828        section_type: SectionType,
829        label: Option<&str>,
830    ) {
831        self.section_markers.push(SectionMarker {
832            timestamp_ms,
833            section_type,
834            label: label.map(|s| s.to_string()),
835        });
836    }
837
838    /// Add a BPM change point
839    pub fn add_bpm_change(&mut self, timestamp_ms: u64, bpm: f32) {
840        self.bpm_map.push(BpmChange { timestamp_ms, bpm });
841    }
842
843    /// Add a key change point
844    pub fn add_key_change(&mut self, timestamp_ms: u64, key: &str) {
845        self.key_changes.push(KeyChange {
846            timestamp_ms,
847            key: key.to_string(),
848        });
849    }
850
851    /// Add a creator note
852    pub fn add_creator_note(&mut self, text: &str, timestamp_ms: Option<u64>) {
853        self.creator_notes.push(CreatorNote {
854            timestamp_ms,
855            text: text.to_string(),
856        });
857    }
858
859    /// Add collaboration credit
860    pub fn add_collaboration(&mut self, role: &str, name: &str, timestamp_ms: Option<u64>) {
861        self.collaboration_credits.push(CollaborationCredit {
862            role: role.to_string(),
863            name: name.to_string(),
864            timestamp_ms,
865        });
866    }
867}