Skip to main content

yt_dlp/model/
format.rs

1//! Formats-related models.
2
3use std::fmt;
4use std::hash::Hash;
5use std::str::FromStr;
6
7use ordered_float::OrderedFloat;
8use reqwest::header::{self, HeaderMap, HeaderValue};
9use serde::{Deserialize, Serialize};
10
11use crate::model::DrmStatus;
12use crate::model::utils::serde::json_none;
13
14/// Represents an available format of a video.
15/// It can be audio, video, both of them, a manifest, or a storyboard.
16///
17/// A manifest is a file that contains metadata about the video streams, and how to assemble them.
18/// A storyboard is a file that contains grid of images from the video, allowing users to preview the video.
19/// Usually, these formats are not meant to be downloaded, but to be used by the player.
20/// So, in most cases, you can ignore them.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct Format {
23    /// The display name of the format, e.g. '303 - 1920x1080 (1080p60)'.
24    pub format: String,
25    /// The format ID, e.g. '303'.
26    pub format_id: String,
27    /// The format note, e.g. '1080p60'.
28    pub format_note: Option<String>,
29
30    /// The type of the format.
31    #[serde(default)]
32    pub protocol: Protocol,
33    /// The language of the format.
34    pub language: Option<String>,
35
36    /// If the format has DRM.
37    pub has_drm: Option<DrmStatus>,
38    /// The extension of the file containing the format.
39    #[serde(default)]
40    pub container: Option<Container>,
41
42    /// The Unix timestamp when this format's stream URL became available (yt-dlp fetch time).
43    /// Used to detect CDN URL expiry: YouTube URLs typically last ~6 hours after this timestamp.
44    pub available_at: Option<i64>,
45    /// yt-dlp internal language preference score for this format.
46    pub language_preference: Option<i64>,
47    /// yt-dlp internal source preference score for this format.
48    pub source_preference: Option<i64>,
49
50    /// All the codec-related information.
51    #[serde(flatten)]
52    pub codec_info: CodecInfo,
53    /// All the video resolution-related information.
54    #[serde(flatten)]
55    pub video_resolution: VideoResolution,
56    /// All the download-related information.
57    #[serde(flatten)]
58    pub download_info: DownloadInfo,
59    /// All the quality-related information.
60    #[serde(flatten)]
61    pub quality_info: QualityInfo,
62    /// All the file-related information.
63    #[serde(flatten)]
64    pub file_info: FileInfo,
65    /// All the storyboard-related information.
66    #[serde(flatten)]
67    pub storyboard_info: StoryboardInfo,
68    /// All the rates-related information.
69    #[serde(flatten)]
70    pub rates_info: RatesInfo,
71
72    /// The ID of the video this format belongs to.
73    /// This field is not part of the yt-dlp output, but is added by the library
74    /// to associate formats with their videos for caching purposes.
75    #[serde(skip)]
76    pub video_id: Option<String>,
77}
78
79impl Format {
80    /// Checks if the format is a video format.
81    pub fn is_video(&self) -> bool {
82        let format_type = self.format_type();
83
84        format_type.is_video()
85    }
86
87    /// Checks if the format is an audio format.
88    pub fn is_audio(&self) -> bool {
89        let format_type = self.format_type();
90
91        format_type.is_audio()
92    }
93
94    /// Gets the type of the format.
95    /// It can be audio, video, both of them, a manifest, or a storyboard.
96    ///
97    /// # Returns
98    ///
99    /// The [`FormatType`] determined from the codec and manifest information.
100    pub fn format_type(&self) -> FormatType {
101        if self.download_info.manifest_url.is_some() {
102            return FormatType::Manifest;
103        }
104
105        if self.storyboard_info.fragments.is_some() {
106            return FormatType::Storyboard;
107        }
108
109        let audio = self.codec_info.audio_codec.is_some();
110        let video = self.codec_info.video_codec.is_some();
111
112        match (audio, video) {
113            (true, true) => FormatType::AudioVideo,
114            (true, false) => FormatType::Audio,
115            (false, true) => FormatType::Video,
116            _ => FormatType::Unknown,
117        }
118    }
119
120    /// Returns the decrypted URL for this format.
121    ///
122    /// # Errors
123    ///
124    /// Returns [`Error::FormatNoUrl`](crate::error::Error::FormatNoUrl) if the format has no URL.
125    ///
126    /// # Returns
127    ///
128    /// A reference to the format URL string.
129    pub fn url(&self) -> Result<&String, crate::error::Error> {
130        self.download_info
131            .url
132            .as_ref()
133            .ok_or_else(|| crate::error::Error::FormatNoUrl {
134                video_id: self.video_id.clone().unwrap_or_else(|| "unknown".to_string()),
135                format_id: self.format_id.clone(),
136            })
137    }
138}
139
140impl fmt::Display for Format {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "Format(id={}, format={:?})", self.format_id, self.format)
143    }
144}
145
146/// Represents the codec information of a format.
147#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148pub struct CodecInfo {
149    /// The name of the audio codec, e.g. 'opus' or 'mp4a.xx' (where 'xx' is the codec version).
150    #[serde(default)]
151    #[serde(rename = "acodec")]
152    #[serde(deserialize_with = "json_none")]
153    pub audio_codec: Option<String>,
154    /// The name of the video codec, e.g. 'vp9' or 'avc1.xx' (where 'xx' is the codec version).
155    #[serde(default)]
156    #[serde(rename = "vcodec")]
157    #[serde(deserialize_with = "json_none")]
158    pub video_codec: Option<String>,
159    /// The extension of the audio file.
160    #[serde(default)]
161    pub audio_ext: Extension,
162    /// The extension of the video file.
163    #[serde(default)]
164    pub video_ext: Extension,
165    /// The number of audio channels.
166    pub audio_channels: Option<i64>,
167    /// The audio sample rate.
168    pub asr: Option<i64>,
169}
170
171impl fmt::Display for CodecInfo {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(
174            f,
175            "CodecInfo(audio={}, video={})",
176            self.audio_codec.as_deref().unwrap_or("none"),
177            self.video_codec.as_deref().unwrap_or("none")
178        )
179    }
180}
181
182/// Represents the video resolution information of a format.
183#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
184pub struct VideoResolution {
185    /// The width of the video.
186    pub width: Option<u32>,
187    /// The height of the video.
188    pub height: Option<u32>,
189    /// The combined resolution of the video, e.g. '1920x1080' or 'audio only'.
190    pub resolution: Option<String>,
191    /// The frames per second of the video, e.g. '24' or '25'.
192    pub fps: Option<OrderedFloat<f64>>,
193    /// The aspect ratio of the video, e.g. '1.77' or '1.78' (corresponding to 16:9).
194    pub aspect_ratio: Option<OrderedFloat<f64>>,
195}
196
197impl fmt::Display for VideoResolution {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match (self.width, self.height) {
200            (Some(w), Some(h)) => write!(f, "VideoResolution(width={}, height={})", w, h),
201            _ => write!(f, "VideoResolution(unknown)"),
202        }
203    }
204}
205
206/// Represents the download information of a format.
207#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
208pub struct DownloadInfo {
209    /// The decrypted URL of the format.
210    pub url: Option<String>,
211    /// The extension of the format.
212    #[serde(default)]
213    pub ext: Extension,
214    /// The HTTP headers used by the downloader.
215    pub http_headers: HttpHeaders,
216    /// The manifest URL, if the format is a manifest.
217    pub manifest_url: Option<String>,
218    /// The options used by the downloader.
219    pub downloader_options: Option<DownloaderOptions>,
220}
221
222impl fmt::Display for DownloadInfo {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "DownloadInfo(url={})", self.url.as_deref().unwrap_or("none"))
225    }
226}
227
228/// Represents the quality information of a format.
229#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
230pub struct QualityInfo {
231    /// A relative quality score, e.g. '-1' (for example, if the format is a manifest) or '9.5'.
232    pub quality: Option<OrderedFloat<f64>>,
233    /// If the format is using a large dynamic range.
234    #[serde(default)]
235    pub dynamic_range: Option<DynamicRange>,
236}
237
238impl fmt::Display for QualityInfo {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        write!(
241            f,
242            "QualityInfo(quality={})",
243            self.quality
244                .map(|q| q.to_string())
245                .unwrap_or_else(|| "unknown".to_string())
246        )
247    }
248}
249
250/// Represents the file information of a format.
251#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
252pub struct FileInfo {
253    /// The approximate file size of the format.
254    pub filesize_approx: Option<i64>,
255    /// The exact file size of the format.
256    pub filesize: Option<i64>,
257}
258
259impl fmt::Display for FileInfo {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        if let Some(size) = self.filesize {
262            write!(f, "FileInfo(size={})", size)
263        } else if let Some(approx) = self.filesize_approx {
264            write!(f, "FileInfo(approx_size={})", approx)
265        } else {
266            write!(f, "FileInfo(size=unknown)")
267        }
268    }
269}
270
271/// Represents the rates information of a format.
272#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
273pub struct RatesInfo {
274    /// The video bitrate of the format.
275    #[serde(rename = "vbr")]
276    pub video_rate: Option<OrderedFloat<f64>>,
277    /// The audio bitrate of the format.
278    #[serde(rename = "abr")]
279    pub audio_rate: Option<OrderedFloat<f64>>,
280    /// The total bitrate (video + audio) of the format.
281    #[serde(rename = "tbr")]
282    pub total_rate: Option<OrderedFloat<f64>>,
283}
284
285impl fmt::Display for RatesInfo {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        write!(
288            f,
289            "RatesInfo(video={}, audio={}, total={})",
290            self.video_rate
291                .map(|r| r.to_string())
292                .unwrap_or_else(|| "none".to_string()),
293            self.audio_rate
294                .map(|r| r.to_string())
295                .unwrap_or_else(|| "none".to_string()),
296            self.total_rate
297                .map(|r| r.to_string())
298                .unwrap_or_else(|| "none".to_string())
299        )
300    }
301}
302
303/// Represents the storyboard information of a format.
304#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
305pub struct StoryboardInfo {
306    /// The number of rows in the storyboard.
307    pub rows: Option<i64>,
308    /// The number of columns in the storyboard.
309    pub columns: Option<i64>,
310    /// The fragments of the storyboard.
311    pub fragments: Option<Vec<Fragment>>,
312}
313
314impl fmt::Display for StoryboardInfo {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        match (self.rows, self.columns) {
317            (Some(r), Some(c)) => write!(f, "StoryboardInfo(rows={}, columns={})", r, c),
318            _ => write!(f, "StoryboardInfo(unknown)"),
319        }
320    }
321}
322
323/// Represents a fragment of a storyboard.
324#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
325pub struct Fragment {
326    /// The URL of the fragment.
327    pub url: String,
328    /// The duration of the fragment, in seconds.
329    pub duration: OrderedFloat<f64>,
330}
331
332impl fmt::Display for Fragment {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        write!(f, "Fragment(url={}, duration={})", self.url, self.duration)
335    }
336}
337
338/// Represents the options used by the downloader.
339#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
340pub struct DownloaderOptions {
341    /// The size of the HTTP chunk.
342    pub http_chunk_size: i64,
343}
344
345impl fmt::Display for DownloaderOptions {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        write!(f, "DownloaderOptions(chunk_size={})", self.http_chunk_size)
348    }
349}
350
351/// Represents the HTTP headers used by the downloader.
352#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
353#[serde(rename_all = "PascalCase")]
354pub struct HttpHeaders {
355    /// The user agent used by the downloader.
356    #[serde(rename = "User-Agent", default)]
357    pub user_agent: String,
358    /// The accept header used by the downloader.
359    #[serde(default)]
360    pub accept: String,
361    /// The accept language used by the downloader.
362    #[serde(rename = "Accept-Language", default)]
363    pub accept_language: String,
364    /// The accept encoding used by the downloader.
365    #[serde(rename = "Sec-Fetch-Mode", default)]
366    pub sec_fetch_mode: String,
367}
368
369impl HttpHeaders {
370    /// Creates default browser-like headers for the given user agent.
371    ///
372    /// # Arguments
373    ///
374    /// * `user_agent` - The user agent string to use.
375    ///
376    /// # Returns
377    ///
378    /// An `HttpHeaders` with sensible browser defaults.
379    pub fn browser_defaults(user_agent: String) -> Self {
380        Self {
381            user_agent,
382            accept: "*/*".to_string(),
383            accept_language: "en-US,en".to_string(),
384            sec_fetch_mode: "navigate".to_string(),
385        }
386    }
387
388    /// Converts these headers into a `reqwest::header::HeaderMap`.
389    ///
390    /// # Returns
391    ///
392    /// A `HeaderMap` with User-Agent, Accept, Accept-Language, and Sec-Fetch-Mode set.
393    pub fn to_header_map(&self) -> reqwest::header::HeaderMap {
394        let mut map = HeaderMap::new();
395        if let Ok(hv) = HeaderValue::from_str(&self.user_agent) {
396            map.insert(header::USER_AGENT, hv);
397        }
398        if let Ok(hv) = HeaderValue::from_str(&self.accept) {
399            map.insert(header::ACCEPT, hv);
400        }
401        if let Ok(hv) = HeaderValue::from_str(&self.accept_language) {
402            map.insert(header::ACCEPT_LANGUAGE, hv);
403        }
404        if let Ok(hv) = HeaderValue::from_bytes(self.sec_fetch_mode.as_bytes()) {
405            map.insert("Sec-Fetch-Mode", hv);
406        }
407        map
408    }
409}
410
411impl fmt::Display for HttpHeaders {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        write!(f, "HttpHeaders(user_agent={})", self.user_agent)
414    }
415}
416
417/// The available extensions of a format.
418#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum Extension {
421    /// The M4A extension.
422    #[serde(rename = "m4a")]
423    // Override: rename_all would produce "m4_a" (broken, digit-uppercase boundary)
424    M4A,
425    /// The MP3 extension.
426    Mp3,
427    /// The MP4 extension.
428    Mp4,
429    /// The Webm extension.
430    Webm,
431    /// The FLAC extension.
432    Flac,
433    /// The OGG extension (Vorbis/Opus).
434    Ogg,
435    /// The WAV extension.
436    Wav,
437    /// The AAC extension.
438    Aac,
439    /// The AIFF extension.
440    Aiff,
441    /// The AVI extension.
442    Avi,
443    /// The MPEG-TS extension.
444    Ts,
445    /// The FLV extension.
446    Flv,
447
448    /// The MHTML extension.
449    Mhtml,
450
451    /// If there is no extension.
452    None,
453    /// An unknown extension.
454    #[default]
455    #[serde(other)]
456    Unknown,
457}
458
459impl Extension {
460    /// Returns the lowercase file extension string for this variant.
461    /// Unknown/None variants return `"bin"` as a safe fallback.
462    ///
463    /// # Returns
464    ///
465    /// A static string slice with the file extension (e.g. `"mp4"`, `"webm"`).
466    pub fn as_str(&self) -> &'static str {
467        match self {
468            Extension::M4A => "m4a",
469            Extension::Mp3 => "mp3",
470            Extension::Mp4 => "mp4",
471            Extension::Webm => "webm",
472            Extension::Flac => "flac",
473            Extension::Ogg => "ogg",
474            Extension::Wav => "wav",
475            Extension::Aac => "aac",
476            Extension::Aiff => "aiff",
477            Extension::Avi => "avi",
478            Extension::Ts => "ts",
479            Extension::Flv => "flv",
480            Extension::Mhtml => "mhtml",
481            Extension::None | Extension::Unknown => "bin",
482        }
483    }
484}
485
486impl fmt::Display for Extension {
487    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488        match self {
489            Extension::M4A => f.write_str("M4A"),
490            Extension::Mp3 => f.write_str("Mp3"),
491            Extension::Mp4 => f.write_str("Mp4"),
492            Extension::Webm => f.write_str("Webm"),
493            Extension::Flac => f.write_str("Flac"),
494            Extension::Ogg => f.write_str("Ogg"),
495            Extension::Wav => f.write_str("Wav"),
496            Extension::Aac => f.write_str("Aac"),
497            Extension::Aiff => f.write_str("Aiff"),
498            Extension::Avi => f.write_str("Avi"),
499            Extension::Ts => f.write_str("Ts"),
500            Extension::Flv => f.write_str("Flv"),
501            Extension::Mhtml => f.write_str("Mhtml"),
502            Extension::None => f.write_str("None"),
503            Extension::Unknown => f.write_str("Unknown"),
504        }
505    }
506}
507
508impl FromStr for Extension {
509    type Err = ();
510
511    fn from_str(s: &str) -> Result<Self, Self::Err> {
512        match s.to_lowercase().as_str() {
513            "m4a" => Ok(Extension::M4A),
514            "mp3" => Ok(Extension::Mp3),
515            "mp4" => Ok(Extension::Mp4),
516            "webm" => Ok(Extension::Webm),
517            "flac" => Ok(Extension::Flac),
518            "ogg" | "oga" | "opus" => Ok(Extension::Ogg),
519            "wav" => Ok(Extension::Wav),
520            "aac" => Ok(Extension::Aac),
521            "aiff" | "aif" => Ok(Extension::Aiff),
522            "avi" => Ok(Extension::Avi),
523            "ts" | "m2ts" | "mts" => Ok(Extension::Ts),
524            "flv" => Ok(Extension::Flv),
525            "mhtml" => Ok(Extension::Mhtml),
526            "" | "none" => Ok(Extension::None),
527            _ => Ok(Extension::Unknown),
528        }
529    }
530}
531
532/// The available containers extensions of a format.
533#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
534#[serde(rename_all = "snake_case")]
535pub enum Container {
536    /// The Webm container.
537    #[serde(rename = "webm_dash")]
538    Webm,
539    /// The M4A container.
540    #[serde(rename = "m4a_dash")]
541    M4A,
542    /// The MP4 container.
543    #[serde(rename = "mp4_dash")]
544    Mp4,
545
546    /// An unknown container.
547    #[default]
548    #[serde(other)]
549    Unknown,
550}
551
552impl fmt::Display for Container {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        match self {
555            Container::Mp4 => f.write_str("Mp4"),
556            Container::Webm => f.write_str("Webm"),
557            Container::M4A => f.write_str("M4A"),
558            Container::Unknown => f.write_str("Unknown"),
559        }
560    }
561}
562
563/// The available protocols of a format.
564#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
565#[serde(rename_all = "snake_case")]
566pub enum Protocol {
567    /// The HTTP protocol, used for audio and video formats.
568    Https,
569    /// The M3U8 protocol, used for manifest formats.
570    #[serde(rename = "m3u8_native")]
571    M3U8Native,
572    /// The MHTML protocol, used for storyboard formats.
573    Mhtml,
574
575    /// An unknown protocol.
576    #[default]
577    #[serde(other)]
578    Unknown,
579}
580
581impl fmt::Display for Protocol {
582    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583        match self {
584            Protocol::Https => f.write_str("Https"),
585            Protocol::M3U8Native => f.write_str("HLS"),
586            Protocol::Mhtml => f.write_str("Mhtml"),
587            Protocol::Unknown => f.write_str("Unknown"),
588        }
589    }
590}
591
592/// The available dynamic ranges of a format.
593#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
594pub enum DynamicRange {
595    /// The SDR dynamic range.
596    SDR,
597    /// The HDR dynamic range.
598    HDR,
599
600    /// An unknown dynamic range.
601    #[default]
602    #[serde(other)]
603    Unknown,
604}
605
606impl fmt::Display for DynamicRange {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        match self {
609            DynamicRange::SDR => f.write_str("SDR"),
610            DynamicRange::HDR => f.write_str("HDR"),
611            DynamicRange::Unknown => f.write_str("Unknown"),
612        }
613    }
614}
615
616/// The available format types.
617/// It can be audio, video, both of them, a manifest, or a storyboard.
618#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
619pub enum FormatType {
620    /// The format contains only audio.
621    Audio,
622    /// The format contains only video.
623    Video,
624    /// The format contains both audio and video.
625    AudioVideo,
626    /// The format is a manifest.
627    Manifest,
628    /// The format is a storyboard.
629    Storyboard,
630
631    /// An unknown format type.
632    #[default]
633    #[serde(other)]
634    Unknown,
635}
636
637impl FormatType {
638    /// Checks if the format is an audio and video format.
639    ///
640    /// # Returns
641    ///
642    /// `true` if the format type is [`FormatType::AudioVideo`].
643    pub fn is_audio_and_video(&self) -> bool {
644        matches!(self, FormatType::AudioVideo)
645    }
646
647    /// Checks if the format is a video format.
648    ///
649    /// # Returns
650    ///
651    /// `true` if the format type is [`FormatType::Video`].
652    pub fn is_video(&self) -> bool {
653        matches!(self, FormatType::Video)
654    }
655
656    /// Checks if the format is an audio format.
657    ///
658    /// # Returns
659    ///
660    /// `true` if the format type is [`FormatType::Audio`].
661    pub fn is_audio(&self) -> bool {
662        matches!(self, FormatType::Audio)
663    }
664
665    /// Checks if the format is a storyboard format.
666    ///
667    /// # Returns
668    ///
669    /// `true` if the format type is [`FormatType::Storyboard`].
670    pub fn is_storyboard(&self) -> bool {
671        matches!(self, FormatType::Storyboard)
672    }
673
674    /// Checks if the format is a manifest format.
675    pub fn is_manifest(&self) -> bool {
676        matches!(self, FormatType::Manifest)
677    }
678}
679
680impl fmt::Display for FormatType {
681    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
682        match self {
683            FormatType::Audio => f.write_str("Audio"),
684            FormatType::Video => f.write_str("Video"),
685            FormatType::AudioVideo => f.write_str("AudioVideo"),
686            FormatType::Manifest => f.write_str("Manifest"),
687            FormatType::Storyboard => f.write_str("Storyboard"),
688            FormatType::Unknown => f.write_str("Unknown"),
689        }
690    }
691}