1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Items for FLAC
//!
//! ## File notes
//!
//! * See [`FlacFile`]

pub(crate) mod block;
pub(crate) mod properties;
mod read;
pub(crate) mod write;

use crate::config::WriteOptions;
use crate::error::{LoftyError, Result};
use crate::file::{FileType, TaggedFile};
use crate::id3::v2::tag::Id3v2Tag;
use crate::ogg::tag::VorbisCommentsRef;
use crate::ogg::{OggPictureStorage, VorbisComments};
use crate::picture::{Picture, PictureInformation};
use crate::tag::TagExt;
use crate::util::io::{FileLike, Length, Truncate};

use lofty_attr::LoftyFile;

// Exports
pub use properties::FlacProperties;

/// A FLAC file
///
/// ## Notes
///
/// * The ID3v2 tag is **read only**, and it's use is discouraged by spec
/// * Pictures are stored in the `FlacFile` itself, rather than the tag. Any pictures inside the tag will
///   be extracted out and stored in their own picture blocks.
/// * It is possible to put pictures inside of the tag, that will not be accessible using the available
///   methods on `FlacFile` ([`FlacFile::pictures`], [`FlacFile::remove_picture_type`], etc.)
/// * When converting to [`TaggedFile`], all pictures will be put inside of a [`VorbisComments`] tag, even if the
///   file did not originally contain one.
#[derive(LoftyFile)]
#[lofty(read_fn = "read::read_from")]
#[lofty(write_fn = "Self::write_to")]
#[lofty(no_into_taggedfile_impl)]
pub struct FlacFile {
	/// An ID3v2 tag
	#[lofty(tag_type = "Id3v2")]
	pub(crate) id3v2_tag: Option<Id3v2Tag>,
	/// The vorbis comments contained in the file
	#[lofty(tag_type = "VorbisComments")]
	pub(crate) vorbis_comments_tag: Option<VorbisComments>,
	pub(crate) pictures: Vec<(Picture, PictureInformation)>,
	/// The file's audio properties
	pub(crate) properties: FlacProperties,
}

impl FlacFile {
	// We need a special write fn to append our pictures into a `VorbisComments` tag
	fn write_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
	where
		F: FileLike,
		LoftyError: From<<F as Truncate>::Error>,
		LoftyError: From<<F as Length>::Error>,
	{
		if let Some(ref id3v2) = self.id3v2_tag {
			id3v2.save_to(file, write_options)?;
			file.rewind()?;
		}

		// We have an existing vorbis comments tag, we can just append our pictures to it
		if let Some(ref vorbis_comments) = self.vorbis_comments_tag {
			return VorbisCommentsRef {
				vendor: vorbis_comments.vendor.as_str(),
				items: vorbis_comments
					.items
					.iter()
					.map(|(k, v)| (k.as_str(), v.as_str())),
				pictures: vorbis_comments
					.pictures
					.iter()
					.map(|(p, i)| (p, *i))
					.chain(self.pictures.iter().map(|(p, i)| (p, *i))),
			}
			.write_to(file, write_options);
		}

		// We have pictures, but no vorbis comments tag, we'll need to create a dummy one
		if !self.pictures.is_empty() {
			return VorbisCommentsRef {
				vendor: "",
				items: std::iter::empty(),
				pictures: self.pictures.iter().map(|(p, i)| (p, *i)),
			}
			.write_to(file, write_options);
		}

		Ok(())
	}
}

impl OggPictureStorage for FlacFile {
	fn pictures(&self) -> &[(Picture, PictureInformation)] {
		&self.pictures
	}
}

impl From<FlacFile> for TaggedFile {
	fn from(mut value: FlacFile) -> Self {
		TaggedFile {
			ty: FileType::Flac,
			properties: value.properties.into(),
			tags: {
				let mut tags = Vec::with_capacity(2);

				if let Some(id3v2) = value.id3v2_tag {
					tags.push(id3v2.into());
				}

				// Move our pictures into a `VorbisComments` tag, creating one if necessary
				match value.vorbis_comments_tag {
					Some(mut vorbis_comments) => {
						vorbis_comments.pictures.append(&mut value.pictures);
						tags.push(vorbis_comments.into());
					},
					None if !value.pictures.is_empty() => tags.push(
						VorbisComments {
							vendor: String::new(),
							items: Vec::new(),
							pictures: value.pictures,
						}
						.into(),
					),
					_ => {},
				}

				tags
			},
		}
	}
}