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