moosicbox_lofty/file.rs
1use crate::error::Result;
2use crate::probe::ParseOptions;
3use crate::properties::FileProperties;
4use crate::resolve::CUSTOM_RESOLVERS;
5use crate::tag::{Tag, TagType};
6use crate::traits::TagExt;
7
8use std::convert::TryInto;
9use std::ffi::OsStr;
10use std::fs::{File, OpenOptions};
11use std::io::{Read, Seek};
12use std::path::Path;
13
14/// Provides various methods for interaction with a file
15pub trait AudioFile: Into<TaggedFile> {
16 /// The struct the file uses for audio properties
17 ///
18 /// Not all formats can use [`FileProperties`] since they may contain additional information
19 type Properties;
20
21 /// Read a file from a reader
22 ///
23 /// # Errors
24 ///
25 /// Errors depend on the file and tags being read. See [`LoftyError`](crate::LoftyError)
26 fn read_from<R>(reader: &mut R, parse_options: ParseOptions) -> Result<Self>
27 where
28 R: Read + Seek,
29 Self: Sized;
30
31 /// Attempts to write all tags to a path
32 ///
33 /// # Errors
34 ///
35 /// * `path` does not exist
36 /// * `path` is not writable
37 /// * See [`AudioFile::save_to`]
38 ///
39 /// # Examples
40 ///
41 /// ```rust,no_run
42 /// use moosicbox_lofty::{AudioFile, TaggedFileExt};
43 ///
44 /// # fn main() -> moosicbox_lofty::Result<()> {
45 /// # let path = "tests/files/assets/minimal/full_test.mp3";
46 /// let mut tagged_file = moosicbox_lofty::read_from_path(path)?;
47 ///
48 /// // Edit the tags
49 ///
50 /// tagged_file.save_to_path(path)?;
51 /// # Ok(()) }
52 /// ```
53 fn save_to_path(&self, path: impl AsRef<Path>) -> Result<()> {
54 self.save_to(&mut OpenOptions::new().read(true).write(true).open(path)?)
55 }
56
57 /// Attempts to write all tags to a file
58 ///
59 /// # Errors
60 ///
61 /// See [`Tag::save_to`], however this is applicable to every tag in the file.
62 ///
63 /// # Examples
64 ///
65 /// ```rust,no_run
66 /// use moosicbox_lofty::{AudioFile, TaggedFileExt};
67 /// use std::fs::OpenOptions;
68 ///
69 /// # fn main() -> moosicbox_lofty::Result<()> {
70 /// # let path = "tests/files/assets/minimal/full_test.mp3";
71 /// let mut tagged_file = moosicbox_lofty::read_from_path(path)?;
72 ///
73 /// // Edit the tags
74 ///
75 /// let mut file = OpenOptions::new().read(true).write(true).open(path)?;
76 /// tagged_file.save_to(&mut file)?;
77 /// # Ok(()) }
78 /// ```
79 fn save_to(&self, file: &mut File) -> Result<()>;
80
81 /// Returns a reference to the file's properties
82 fn properties(&self) -> &Self::Properties;
83 /// Checks if the file contains any tags
84 fn contains_tag(&self) -> bool;
85 /// Checks if the file contains the given [`TagType`]
86 fn contains_tag_type(&self, tag_type: TagType) -> bool;
87}
88
89/// Provides a common interface between [`TaggedFile`] and [`BoundTaggedFile`]
90pub trait TaggedFileExt {
91 /// Returns the file's [`FileType`]
92 ///
93 /// # Examples
94 ///
95 /// ```rust
96 /// use moosicbox_lofty::{FileType, TaggedFileExt};
97 ///
98 /// # fn main() -> moosicbox_lofty::Result<()> {
99 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
100 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
101 ///
102 /// assert_eq!(tagged_file.file_type(), FileType::Mpeg);
103 /// # Ok(()) }
104 /// ```
105 fn file_type(&self) -> FileType;
106
107 /// Returns all tags
108 ///
109 /// # Examples
110 ///
111 /// ```rust
112 /// use moosicbox_lofty::{FileType, TaggedFileExt};
113 ///
114 /// # fn main() -> moosicbox_lofty::Result<()> {
115 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
116 /// // An MP3 file with 3 tags
117 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
118 ///
119 /// let tags = tagged_file.tags();
120 ///
121 /// assert_eq!(tags.len(), 3);
122 /// # Ok(()) }
123 /// ```
124 fn tags(&self) -> &[Tag];
125
126 /// Returns the file type's primary [`TagType`]
127 ///
128 /// See [`FileType::primary_tag_type`]
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// use moosicbox_lofty::{TagType, TaggedFileExt};
134 ///
135 /// # fn main() -> moosicbox_lofty::Result<()> {
136 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
137 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
138 ///
139 /// assert_eq!(tagged_file.primary_tag_type(), TagType::Id3v2);
140 /// # Ok(()) }
141 /// ```
142 fn primary_tag_type(&self) -> TagType {
143 self.file_type().primary_tag_type()
144 }
145
146 /// Determines whether the file supports the given [`TagType`]
147 ///
148 /// # Examples
149 ///
150 /// ```rust
151 /// use moosicbox_lofty::{TagType, TaggedFileExt};
152 ///
153 /// # fn main() -> moosicbox_lofty::Result<()> {
154 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
155 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
156 ///
157 /// assert!(tagged_file.supports_tag_type(TagType::Id3v2));
158 /// # Ok(()) }
159 /// ```
160 fn supports_tag_type(&self, tag_type: TagType) -> bool {
161 self.file_type().supports_tag_type(tag_type)
162 }
163
164 /// Get a reference to a specific [`TagType`]
165 ///
166 /// # Examples
167 ///
168 /// ```rust
169 /// use moosicbox_lofty::{TagType, TaggedFileExt};
170 ///
171 /// # fn main() -> moosicbox_lofty::Result<()> {
172 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
173 /// // Read an MP3 file with an ID3v2 tag
174 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
175 ///
176 /// // An ID3v2 tag
177 /// let tag = tagged_file.tag(TagType::Id3v2);
178 ///
179 /// assert!(tag.is_some());
180 /// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
181 /// # Ok(()) }
182 /// ```
183 fn tag(&self, tag_type: TagType) -> Option<&Tag>;
184
185 /// Get a mutable reference to a specific [`TagType`]
186 ///
187 /// # Examples
188 ///
189 /// ```rust
190 /// use moosicbox_lofty::{TagType, TaggedFileExt};
191 ///
192 /// # fn main() -> moosicbox_lofty::Result<()> {
193 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
194 /// // Read an MP3 file with an ID3v2 tag
195 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
196 ///
197 /// // An ID3v2 tag
198 /// let tag = tagged_file.tag(TagType::Id3v2);
199 ///
200 /// assert!(tag.is_some());
201 /// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
202 ///
203 /// // Alter the tag...
204 /// # Ok(()) }
205 /// ```
206 fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag>;
207
208 /// Returns the primary tag
209 ///
210 /// See [`FileType::primary_tag_type`]
211 ///
212 /// # Examples
213 ///
214 /// ```rust
215 /// use moosicbox_lofty::{TagType, TaggedFileExt};
216 ///
217 /// # fn main() -> moosicbox_lofty::Result<()> {
218 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
219 /// // Read an MP3 file with an ID3v2 tag
220 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
221 ///
222 /// // An ID3v2 tag
223 /// let tag = tagged_file.primary_tag();
224 ///
225 /// assert!(tag.is_some());
226 /// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
227 /// # Ok(()) }
228 /// ```
229 fn primary_tag(&self) -> Option<&Tag> {
230 self.tag(self.primary_tag_type())
231 }
232
233 /// Gets a mutable reference to the file's "Primary tag"
234 ///
235 /// See [`FileType::primary_tag_type`]
236 ///
237 /// # Examples
238 ///
239 /// ```rust
240 /// use moosicbox_lofty::{TagType, TaggedFileExt};
241 ///
242 /// # fn main() -> moosicbox_lofty::Result<()> {
243 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
244 /// // Read an MP3 file with an ID3v2 tag
245 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
246 ///
247 /// // An ID3v2 tag
248 /// let tag = tagged_file.primary_tag_mut();
249 ///
250 /// assert!(tag.is_some());
251 /// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
252 ///
253 /// // Alter the tag...
254 /// # Ok(()) }
255 /// ```
256 fn primary_tag_mut(&mut self) -> Option<&mut Tag> {
257 self.tag_mut(self.primary_tag_type())
258 }
259
260 /// Gets the first tag, if there are any
261 ///
262 /// NOTE: This will grab the first available tag, you cannot rely on the result being
263 /// a specific type
264 ///
265 /// # Examples
266 ///
267 /// ```rust
268 /// use moosicbox_lofty::TaggedFileExt;
269 ///
270 /// # fn main() -> moosicbox_lofty::Result<()> {
271 /// # let path = "tests/files/assets/minimal/full_test.mp3";
272 /// // A file we know has tags
273 /// let mut tagged_file = moosicbox_lofty::read_from_path(path)?;
274 ///
275 /// // A tag of a (currently) unknown type
276 /// let tag = tagged_file.first_tag();
277 /// assert!(tag.is_some());
278 /// # Ok(()) }
279 /// ```
280 fn first_tag(&self) -> Option<&Tag> {
281 self.tags().first()
282 }
283
284 /// Gets a mutable reference to the first tag, if there are any
285 ///
286 /// NOTE: This will grab the first available tag, you cannot rely on the result being
287 /// a specific type
288 ///
289 /// # Examples
290 ///
291 /// ```rust
292 /// use moosicbox_lofty::TaggedFileExt;
293 ///
294 /// # fn main() -> moosicbox_lofty::Result<()> {
295 /// # let path = "tests/files/assets/minimal/full_test.mp3";
296 /// // A file we know has tags
297 /// let mut tagged_file = moosicbox_lofty::read_from_path(path)?;
298 ///
299 /// // A tag of a (currently) unknown type
300 /// let tag = tagged_file.first_tag_mut();
301 /// assert!(tag.is_some());
302 ///
303 /// // Alter the tag...
304 /// # Ok(()) }
305 /// ```
306 fn first_tag_mut(&mut self) -> Option<&mut Tag>;
307
308 /// Inserts a [`Tag`]
309 ///
310 /// NOTE: This will do nothing if the [`FileType`] does not support
311 /// the [`TagType`]. See [`FileType::supports_tag_type`]
312 ///
313 /// If a tag is replaced, it will be returned
314 ///
315 /// # Examples
316 ///
317 /// ```rust
318 /// use moosicbox_lofty::{AudioFile, Tag, TagType, TaggedFileExt};
319 ///
320 /// # fn main() -> moosicbox_lofty::Result<()> {
321 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
322 /// // Read an MP3 file without an ID3v2 tag
323 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
324 /// # let _ = tagged_file.remove(TagType::Id3v2); // sneaky
325 ///
326 /// assert!(!tagged_file.contains_tag_type(TagType::Id3v2));
327 ///
328 /// // Insert the ID3v2 tag
329 /// let new_id3v2_tag = Tag::new(TagType::Id3v2);
330 /// tagged_file.insert_tag(new_id3v2_tag);
331 ///
332 /// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
333 /// # Ok(()) }
334 /// ```
335 fn insert_tag(&mut self, tag: Tag) -> Option<Tag>;
336
337 /// Removes a specific [`TagType`] and returns it
338 ///
339 /// # Examples
340 ///
341 /// ```rust
342 /// use moosicbox_lofty::{AudioFile, TagType, TaggedFileExt};
343 ///
344 /// # fn main() -> moosicbox_lofty::Result<()> {
345 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
346 /// // Read an MP3 file containing an ID3v2 tag
347 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
348 ///
349 /// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
350 ///
351 /// // Take the ID3v2 tag
352 /// let id3v2 = tagged_file.remove(TagType::Id3v2);
353 ///
354 /// assert!(!tagged_file.contains_tag_type(TagType::Id3v2));
355 /// # Ok(()) }
356 /// ```
357 fn remove(&mut self, tag_type: TagType) -> Option<Tag>;
358
359 /// Removes all tags from the file
360 ///
361 /// # Examples
362 ///
363 /// ```rust
364 /// use moosicbox_lofty::TaggedFileExt;
365 ///
366 /// # fn main() -> moosicbox_lofty::Result<()> {
367 /// # let path = "tests/files/assets/minimal/full_test.mp3";
368 /// let mut tagged_file = moosicbox_lofty::read_from_path(path)?;
369 ///
370 /// tagged_file.clear();
371 ///
372 /// assert!(tagged_file.tags().is_empty());
373 /// # Ok(()) }
374 /// ```
375 fn clear(&mut self);
376}
377
378/// A generic representation of a file
379///
380/// This is used when the [`FileType`] has to be guessed
381pub struct TaggedFile {
382 /// The file's type
383 pub(crate) ty: FileType,
384 /// The file's audio properties
385 pub(crate) properties: FileProperties,
386 /// A collection of the file's tags
387 pub(crate) tags: Vec<Tag>,
388}
389
390impl TaggedFile {
391 #[doc(hidden)]
392 /// This exists for use in `moosicbox_lofty_attr`, there's no real use for this externally
393 #[must_use]
394 pub const fn new(ty: FileType, properties: FileProperties, tags: Vec<Tag>) -> Self {
395 Self {
396 ty,
397 properties,
398 tags,
399 }
400 }
401
402 /// Changes the [`FileType`]
403 ///
404 /// NOTES:
405 ///
406 /// * This will remove any tag the format does not support. See [`FileType::supports_tag_type`]
407 /// * This will reset the [`FileProperties`]
408 ///
409 /// # Examples
410 ///
411 /// ```rust
412 /// use moosicbox_lofty::{AudioFile, FileType, TagType, TaggedFileExt};
413 ///
414 /// # fn main() -> moosicbox_lofty::Result<()> {
415 /// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
416 /// // Read an MP3 file containing an ID3v2 tag
417 /// let mut tagged_file = moosicbox_lofty::read_from_path(path_to_mp3)?;
418 ///
419 /// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
420 ///
421 /// // Remap our MP3 file to WavPack, which doesn't support ID3v2
422 /// tagged_file.change_file_type(FileType::WavPack);
423 ///
424 /// assert!(!tagged_file.contains_tag_type(TagType::Id3v2));
425 /// # Ok(()) }
426 /// ```
427 pub fn change_file_type(&mut self, file_type: FileType) {
428 self.ty = file_type;
429 self.properties = FileProperties::default();
430 self.tags
431 .retain(|t| self.ty.supports_tag_type(t.tag_type()));
432 }
433}
434
435impl TaggedFileExt for TaggedFile {
436 fn file_type(&self) -> FileType {
437 self.ty
438 }
439
440 fn tags(&self) -> &[Tag] {
441 self.tags.as_slice()
442 }
443
444 fn tag(&self, tag_type: TagType) -> Option<&Tag> {
445 self.tags.iter().find(|i| i.tag_type() == tag_type)
446 }
447
448 fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag> {
449 self.tags.iter_mut().find(|i| i.tag_type() == tag_type)
450 }
451
452 fn first_tag_mut(&mut self) -> Option<&mut Tag> {
453 self.tags.first_mut()
454 }
455
456 fn insert_tag(&mut self, tag: Tag) -> Option<Tag> {
457 let tag_type = tag.tag_type();
458
459 if self.supports_tag_type(tag_type) {
460 let ret = self.remove(tag_type);
461 self.tags.push(tag);
462
463 return ret;
464 }
465
466 None
467 }
468
469 fn remove(&mut self, tag_type: TagType) -> Option<Tag> {
470 self.tags
471 .iter()
472 .position(|t| t.tag_type() == tag_type)
473 .map(|pos| self.tags.remove(pos))
474 }
475
476 fn clear(&mut self) {
477 self.tags.clear()
478 }
479}
480
481impl AudioFile for TaggedFile {
482 type Properties = FileProperties;
483
484 fn read_from<R>(reader: &mut R, parse_options: ParseOptions) -> Result<Self>
485 where
486 R: Read + Seek,
487 Self: Sized,
488 {
489 crate::probe::Probe::new(reader)
490 .guess_file_type()?
491 .options(parse_options)
492 .read()
493 }
494
495 fn save_to(&self, file: &mut File) -> Result<()> {
496 for tag in &self.tags {
497 // TODO: This is a temporary solution. Ideally we should probe once and use
498 // the format-specific writing to avoid these rewinds.
499 file.rewind()?;
500 tag.save_to(file)?;
501 }
502
503 Ok(())
504 }
505
506 fn properties(&self) -> &Self::Properties {
507 &self.properties
508 }
509
510 fn contains_tag(&self) -> bool {
511 !self.tags.is_empty()
512 }
513
514 fn contains_tag_type(&self, tag_type: TagType) -> bool {
515 self.tags.iter().any(|t| t.tag_type() == tag_type)
516 }
517}
518
519impl From<BoundTaggedFile> for TaggedFile {
520 fn from(input: BoundTaggedFile) -> Self {
521 input.inner
522 }
523}
524
525/// A variant of [`TaggedFile`] that holds a [`File`] handle, and reflects changes
526/// such as tag removals.
527///
528/// For example:
529///
530/// ```rust,no_run
531/// use moosicbox_lofty::{AudioFile, Tag, TagType, TaggedFileExt};
532/// # fn main() -> moosicbox_lofty::Result<()> {
533/// # let path = "tests/files/assets/minimal/full_test.mp3";
534///
535/// // We create an empty tag
536/// let tag = Tag::new(TagType::Id3v2);
537///
538/// let mut tagged_file = moosicbox_lofty::read_from_path(path)?;
539///
540/// // Push our empty tag into the TaggedFile
541/// tagged_file.insert_tag(tag);
542///
543/// // After saving, our file still "contains" the ID3v2 tag, but if we were to read
544/// // "foo.mp3", it would not have an ID3v2 tag. Lofty does not write empty tags, but this
545/// // change will not be reflected in `TaggedFile`.
546/// tagged_file.save_to_path("foo.mp3")?;
547/// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
548/// # Ok(()) }
549/// ```
550///
551/// However, when using `BoundTaggedFile`:
552///
553/// ```rust,no_run
554/// use moosicbox_lofty::{AudioFile, BoundTaggedFile, ParseOptions, Tag, TagType, TaggedFileExt};
555/// use std::fs::OpenOptions;
556/// # fn main() -> moosicbox_lofty::Result<()> {
557/// # let path = "tests/files/assets/minimal/full_test.mp3";
558///
559/// // We create an empty tag
560/// let tag = Tag::new(TagType::Id3v2);
561///
562/// // We'll need to open our file for reading *and* writing
563/// let file = OpenOptions::new().read(true).write(true).open(path)?;
564/// let parse_options = ParseOptions::new();
565///
566/// let mut bound_tagged_file = BoundTaggedFile::read_from(file, parse_options)?;
567///
568/// // Push our empty tag into the TaggedFile
569/// bound_tagged_file.insert_tag(tag);
570///
571/// // Now when saving, we no longer have to specify a path, and the tags in the `BoundTaggedFile`
572/// // reflect those in the actual file on disk.
573/// bound_tagged_file.save()?;
574/// assert!(!bound_tagged_file.contains_tag_type(TagType::Id3v2));
575/// # Ok(()) }
576/// ```
577pub struct BoundTaggedFile {
578 inner: TaggedFile,
579 file_handle: File,
580}
581
582impl BoundTaggedFile {
583 /// Create a new [`BoundTaggedFile`]
584 ///
585 /// # Errors
586 ///
587 /// See [`AudioFile::read_from`]
588 ///
589 /// # Examples
590 ///
591 /// ```rust
592 /// use moosicbox_lofty::{AudioFile, BoundTaggedFile, ParseOptions, Tag, TagType, TaggedFileExt};
593 /// use std::fs::OpenOptions;
594 /// # fn main() -> moosicbox_lofty::Result<()> {
595 /// # let path = "tests/files/assets/minimal/full_test.mp3";
596 ///
597 /// // We'll need to open our file for reading *and* writing
598 /// let file = OpenOptions::new().read(true).write(true).open(path)?;
599 /// let parse_options = ParseOptions::new();
600 ///
601 /// let bound_tagged_file = BoundTaggedFile::read_from(file, parse_options)?;
602 /// # Ok(()) }
603 /// ```
604 pub fn read_from(mut file: File, parse_options: ParseOptions) -> Result<Self> {
605 let inner = TaggedFile::read_from(&mut file, parse_options)?;
606 file.rewind()?;
607
608 Ok(Self {
609 inner,
610 file_handle: file,
611 })
612 }
613
614 /// Save the tags to the file stored internally
615 ///
616 /// # Errors
617 ///
618 /// See [`TaggedFile::save_to`]
619 ///
620 /// # Examples
621 ///
622 /// ```rust,no_run
623 /// use moosicbox_lofty::{AudioFile, BoundTaggedFile, ParseOptions, Tag, TagType, TaggedFileExt};
624 /// use std::fs::OpenOptions;
625 /// # fn main() -> moosicbox_lofty::Result<()> {
626 /// # let path = "tests/files/assets/minimal/full_test.mp3";
627 ///
628 /// // We'll need to open our file for reading *and* writing
629 /// let file = OpenOptions::new().read(true).write(true).open(path)?;
630 /// let parse_options = ParseOptions::new();
631 ///
632 /// let mut bound_tagged_file = BoundTaggedFile::read_from(file, parse_options)?;
633 ///
634 /// // Do some work to the tags...
635 ///
636 /// // This will save the tags to the file we provided to `read_from`
637 /// bound_tagged_file.save()?;
638 /// # Ok(()) }
639 /// ```
640 pub fn save(&mut self) -> Result<()> {
641 self.inner.save_to(&mut self.file_handle)?;
642 self.inner.tags.retain(|tag| !tag.is_empty());
643
644 Ok(())
645 }
646}
647
648impl TaggedFileExt for BoundTaggedFile {
649 fn file_type(&self) -> FileType {
650 self.inner.file_type()
651 }
652
653 fn tags(&self) -> &[Tag] {
654 self.inner.tags()
655 }
656
657 fn tag(&self, tag_type: TagType) -> Option<&Tag> {
658 self.inner.tag(tag_type)
659 }
660
661 fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag> {
662 self.inner.tag_mut(tag_type)
663 }
664
665 fn first_tag_mut(&mut self) -> Option<&mut Tag> {
666 self.inner.first_tag_mut()
667 }
668
669 fn insert_tag(&mut self, tag: Tag) -> Option<Tag> {
670 self.inner.insert_tag(tag)
671 }
672
673 fn remove(&mut self, tag_type: TagType) -> Option<Tag> {
674 self.inner.remove(tag_type)
675 }
676
677 fn clear(&mut self) {
678 self.inner.clear()
679 }
680}
681
682impl AudioFile for BoundTaggedFile {
683 type Properties = FileProperties;
684
685 fn read_from<R>(_: &mut R, _: ParseOptions) -> Result<Self>
686 where
687 R: Read + Seek,
688 Self: Sized,
689 {
690 unimplemented!(
691 "BoundTaggedFile can only be constructed through `BoundTaggedFile::read_from`"
692 )
693 }
694
695 fn save_to(&self, file: &mut File) -> Result<()> {
696 self.inner.save_to(file)
697 }
698
699 fn properties(&self) -> &Self::Properties {
700 self.inner.properties()
701 }
702
703 fn contains_tag(&self) -> bool {
704 self.inner.contains_tag()
705 }
706
707 fn contains_tag_type(&self, tag_type: TagType) -> bool {
708 self.inner.contains_tag_type(tag_type)
709 }
710}
711
712/// The type of file read
713#[derive(PartialEq, Eq, Copy, Clone, Debug)]
714#[allow(missing_docs)]
715#[non_exhaustive]
716pub enum FileType {
717 Aac,
718 Aiff,
719 Ape,
720 Flac,
721 Mpeg,
722 Mp4,
723 Mpc,
724 Opus,
725 Vorbis,
726 Speex,
727 Wav,
728 WavPack,
729 Custom(&'static str),
730}
731
732impl FileType {
733 /// Returns the file type's "primary" [`TagType`], or the one most likely to be used in the target format
734 ///
735 /// | [`FileType`] | [`TagType`] |
736 /// |-----------------------------------|------------------|
737 /// | `Aac`, `Aiff`, `Mp3`, `Wav` | `Id3v2` |
738 /// | `Ape` , `Mpc`, `WavPack` | `Ape` |
739 /// | `Flac`, `Opus`, `Vorbis`, `Speex` | `VorbisComments` |
740 /// | `Mp4` | `Mp4Ilst` |
741 ///
742 /// # Panics
743 ///
744 /// If an unregistered `FileType` ([`FileType::Custom`]) is encountered. See [`register_custom_resolver`](crate::resolve::register_custom_resolver).
745 ///
746 /// # Examples
747 ///
748 /// ```rust
749 /// use moosicbox_lofty::{FileType, TagType};
750 ///
751 /// let file_type = FileType::Mpeg;
752 /// assert_eq!(file_type.primary_tag_type(), TagType::Id3v2);
753 /// ```
754 pub fn primary_tag_type(&self) -> TagType {
755 match self {
756 FileType::Aac | FileType::Aiff | FileType::Mpeg | FileType::Wav => TagType::Id3v2,
757 FileType::Ape | FileType::Mpc | FileType::WavPack => TagType::Ape,
758 FileType::Flac | FileType::Opus | FileType::Vorbis | FileType::Speex => {
759 TagType::VorbisComments
760 },
761 FileType::Mp4 => TagType::Mp4Ilst,
762 FileType::Custom(c) => {
763 let resolver = crate::resolve::lookup_resolver(c);
764 resolver.primary_tag_type()
765 },
766 }
767 }
768
769 /// Returns if the target `FileType` supports a [`TagType`]
770 ///
771 /// NOTE: This is feature dependent, meaning if you do not have the
772 /// `id3v2` feature enabled, [`FileType::Mpeg`] will return `false` for
773 /// [`TagType::Id3v2`].
774 ///
775 /// # Panics
776 ///
777 /// If an unregistered `FileType` ([`FileType::Custom`]) is encountered. See [`register_custom_resolver`](crate::resolve::register_custom_resolver).
778 ///
779 /// # Examples
780 ///
781 /// ```rust
782 /// use moosicbox_lofty::{FileType, TagType};
783 ///
784 /// let file_type = FileType::Mpeg;
785 /// assert!(file_type.supports_tag_type(TagType::Id3v2));
786 /// ```
787 pub fn supports_tag_type(&self, tag_type: TagType) -> bool {
788 if let FileType::Custom(c) = self {
789 let resolver = crate::resolve::lookup_resolver(c);
790 return resolver.supported_tag_types().contains(&tag_type);
791 }
792
793 match tag_type {
794 TagType::Ape => crate::ape::ApeTag::SUPPORTED_FORMATS.contains(self),
795 TagType::Id3v1 => crate::id3::v1::Id3v1Tag::SUPPORTED_FORMATS.contains(self),
796 TagType::Id3v2 => crate::id3::v2::Id3v2Tag::SUPPORTED_FORMATS.contains(self),
797 TagType::Mp4Ilst => crate::mp4::Ilst::SUPPORTED_FORMATS.contains(self),
798 TagType::VorbisComments => crate::ogg::VorbisComments::SUPPORTED_FORMATS.contains(self),
799 TagType::RiffInfo => crate::iff::wav::RIFFInfoList::SUPPORTED_FORMATS.contains(self),
800 TagType::AiffText => crate::iff::aiff::AIFFTextChunks::SUPPORTED_FORMATS.contains(self),
801 }
802 }
803
804 /// Attempts to extract a [`FileType`] from an extension
805 ///
806 /// # Examples
807 ///
808 /// ```rust
809 /// use moosicbox_lofty::FileType;
810 ///
811 /// let extension = "mp3";
812 /// assert_eq!(FileType::from_ext(extension), Some(FileType::Mpeg));
813 /// ```
814 pub fn from_ext<E>(ext: E) -> Option<Self>
815 where
816 E: AsRef<OsStr>,
817 {
818 let ext = ext.as_ref().to_str()?.to_ascii_lowercase();
819
820 match ext.as_str() {
821 "aac" => Some(Self::Aac),
822 "ape" => Some(Self::Ape),
823 "aiff" | "aif" | "afc" | "aifc" => Some(Self::Aiff),
824 "mp3" | "mp2" | "mp1" => Some(Self::Mpeg),
825 "wav" | "wave" => Some(Self::Wav),
826 "wv" => Some(Self::WavPack),
827 "opus" => Some(Self::Opus),
828 "flac" => Some(Self::Flac),
829 "ogg" => Some(Self::Vorbis),
830 "mp4" | "m4a" | "m4b" | "m4p" | "m4r" | "m4v" | "3gp" => Some(Self::Mp4),
831 "mpc" | "mp+" | "mpp" => Some(Self::Mpc),
832 "spx" => Some(Self::Speex),
833 e => {
834 if let Some((ty, _)) = CUSTOM_RESOLVERS
835 .lock()
836 .ok()?
837 .iter()
838 .find(|(_, f)| f.extension() == Some(e))
839 {
840 Some(Self::Custom(ty))
841 } else {
842 None
843 }
844 },
845 }
846 }
847
848 /// Attempts to determine a [`FileType`] from a path
849 ///
850 /// # Examples
851 ///
852 /// ```rust
853 /// use moosicbox_lofty::FileType;
854 /// use std::path::Path;
855 ///
856 /// let path = Path::new("path/to/my.mp3");
857 /// assert_eq!(FileType::from_path(path), Some(FileType::Mpeg));
858 /// ```
859 pub fn from_path<P>(path: P) -> Option<Self>
860 where
861 P: AsRef<Path>,
862 {
863 let ext = path.as_ref().extension();
864 ext.and_then(Self::from_ext)
865 }
866
867 /// Attempts to extract a [`FileType`] from a buffer
868 ///
869 /// NOTES:
870 ///
871 /// * This is for use in [`Probe::guess_file_type`], it
872 /// is recommended to use it that way
873 /// * This **will not** search past tags at the start of the buffer.
874 /// For this behavior, use [`Probe::guess_file_type`].
875 ///
876 /// [`Probe::guess_file_type`]: crate::Probe::guess_file_type
877 ///
878 /// # Examples
879 ///
880 /// ```rust
881 /// use moosicbox_lofty::FileType;
882 /// use std::fs::File;
883 /// use std::io::Read;
884 ///
885 /// # fn main() -> moosicbox_lofty::Result<()> {
886 /// # let path_to_opus = "tests/files/assets/minimal/full_test.opus";
887 /// let mut file = File::open(path_to_opus)?;
888 ///
889 /// let mut buf = [0; 50]; // Search the first 50 bytes of the file
890 /// file.read_exact(&mut buf)?;
891 ///
892 /// assert_eq!(FileType::from_buffer(&buf), Some(FileType::Opus));
893 /// # Ok(()) }
894 /// ```
895 pub fn from_buffer(buf: &[u8]) -> Option<Self> {
896 match Self::from_buffer_inner(buf) {
897 FileTypeGuessResult::Determined(file_ty) => Some(file_ty),
898 // We make no attempt to search past an ID3v2 tag or junk here, since
899 // we only provided a fixed-sized buffer to search from.
900 //
901 // That case is handled in `Probe::guess_file_type`
902 _ => None,
903 }
904 }
905
906 // TODO: APE tags in the beginning of the file
907 pub(crate) fn from_buffer_inner(buf: &[u8]) -> FileTypeGuessResult {
908 use crate::id3::v2::util::synchsafe::SynchsafeInteger;
909
910 // Start out with an empty return
911 let mut ret = FileTypeGuessResult::Undetermined;
912
913 if buf.is_empty() {
914 return ret;
915 }
916
917 match Self::quick_type_guess(buf) {
918 Some(f_ty) => ret = FileTypeGuessResult::Determined(f_ty),
919 // Special case for ID3, gets checked in `Probe::guess_file_type`
920 // The bare minimum size for an ID3v2 header is 10 bytes
921 None if buf.len() >= 10 && &buf[..3] == b"ID3" => {
922 // This is infallible, but preferable to an unwrap
923 if let Ok(arr) = buf[6..10].try_into() {
924 // Set the ID3v2 size
925 ret =
926 FileTypeGuessResult::MaybePrecededById3(u32::from_be_bytes(arr).unsynch());
927 }
928 },
929 None if buf.first().copied() == Some(0) => {
930 ret = FileTypeGuessResult::MaybePrecededByJunk
931 },
932 // We aren't able to determine a format
933 _ => {},
934 }
935
936 ret
937 }
938
939 fn quick_type_guess(buf: &[u8]) -> Option<Self> {
940 use crate::mpeg::header::verify_frame_sync;
941
942 // Safe to index, since we return early on an empty buffer
943 match buf[0] {
944 77 if buf.starts_with(b"MAC") => Some(Self::Ape),
945 255 if buf.len() >= 2 && verify_frame_sync([buf[0], buf[1]]) => {
946 // ADTS and MPEG frame headers are way too similar
947
948 // ADTS (https://wiki.multimedia.cx/index.php/ADTS#Header):
949 //
950 // AAAAAAAA AAAABCCX
951 //
952 // Letter Length (bits) Description
953 // A 12 Syncword, all bits must be set to 1.
954 // B 1 MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2.
955 // C 2 Layer, always set to 0.
956
957 // MPEG (http://www.mp3-tech.org/programmer/frame_header.html):
958 //
959 // AAAAAAAA AAABBCCX
960 //
961 // Letter Length (bits) Description
962 // A 11 Syncword, all bits must be set to 1.
963 // B 2 MPEG Audio version ID
964 // C 2 Layer description
965
966 // The subtle overlap in the ADTS header's frame sync and MPEG's version ID
967 // is the first condition to check. However, since 0b10 and 0b11 are valid versions
968 // in MPEG, we have to also check the layer.
969
970 // So, if we have a version 1 (0b11) or version 2 (0b10) MPEG frame AND a layer of 0b00,
971 // we can assume we have an ADTS header. Awesome!
972
973 if buf[1] & 0b10000 > 0 && buf[1] & 0b110 == 0 {
974 return Some(Self::Aac);
975 }
976
977 Some(Self::Mpeg)
978 },
979 70 if buf.len() >= 12 && &buf[..4] == b"FORM" => {
980 let id = &buf[8..12];
981
982 if id == b"AIFF" || id == b"AIFC" {
983 return Some(Self::Aiff);
984 }
985
986 None
987 },
988 79 if buf.len() >= 36 && &buf[..4] == b"OggS" => {
989 if &buf[29..35] == b"vorbis" {
990 return Some(Self::Vorbis);
991 } else if &buf[28..36] == b"OpusHead" {
992 return Some(Self::Opus);
993 } else if &buf[28..36] == b"Speex " {
994 return Some(Self::Speex);
995 }
996
997 None
998 },
999 102 if buf.starts_with(b"fLaC") => Some(Self::Flac),
1000 82 if buf.len() >= 12 && &buf[..4] == b"RIFF" => {
1001 if &buf[8..12] == b"WAVE" {
1002 return Some(Self::Wav);
1003 }
1004
1005 None
1006 },
1007 119 if buf.len() >= 4 && &buf[..4] == b"wvpk" => Some(Self::WavPack),
1008 _ if buf.len() >= 8 && &buf[4..8] == b"ftyp" => Some(Self::Mp4),
1009 _ if buf.starts_with(b"MPCK") || buf.starts_with(b"MP+") => Some(Self::Mpc),
1010 _ => None,
1011 }
1012 }
1013}
1014
1015/// The result of a `FileType` guess
1016///
1017/// External callers of `FileType::from_buffer()` will only ever see `Determined` cases.
1018/// The remaining cases are used internally in `Probe::guess_file_type()`.
1019pub(crate) enum FileTypeGuessResult {
1020 /// The `FileType` was guessed
1021 Determined(FileType),
1022 /// The stream starts with an ID3v2 tag
1023 MaybePrecededById3(u32),
1024 /// The stream starts with junk zero bytes
1025 MaybePrecededByJunk,
1026 /// The `FileType` could not be guessed
1027 Undetermined,
1028}