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}