anni_vgmdb/
models.rs

1use crate::utils::{parse_date, parse_multi_language};
2use crate::{Result, VGMClient, VGMError};
3use select::document::Document;
4use select::predicate::{Attr, Class, Name, Predicate};
5use std::collections::HashMap;
6use std::fmt::{Debug, Formatter};
7use std::str::FromStr;
8
9pub struct SearchResponse<'client> {
10    client: &'client VGMClient,
11    inner: SearchResult,
12}
13
14impl<'client> SearchResponse<'client> {
15    pub(crate) fn new(client: &'client VGMClient, inner: SearchResult) -> Self {
16        SearchResponse { client, inner }
17    }
18
19    pub fn is_empty(&self) -> bool {
20        self.len() == 0
21    }
22
23    pub fn len(&self) -> usize {
24        match &self.inner {
25            SearchResult::Album(_) => 1,
26            SearchResult::List(list) => list.len(),
27        }
28    }
29
30    /// Get the list of search results.
31    pub fn albums(&self) -> Vec<&AlbumInfo> {
32        match &self.inner {
33            SearchResult::Album(album) => vec![&album.info],
34            SearchResult::List(list) => list.iter().collect(),
35        }
36    }
37
38    pub async fn into_album(self, index: Option<usize>) -> Result<AlbumDetail> {
39        match self.inner {
40            SearchResult::Album(data) => Ok(data),
41            SearchResult::List(list) => {
42                let index = index.unwrap_or(0);
43                if list.len() > index {
44                    let id = &list[index].id;
45                    let album_detail = self.client.album(id).await?;
46                    Ok(album_detail)
47                } else {
48                    Err(VGMError::NoAlbumFound)
49                }
50            }
51        }
52    }
53}
54
55impl<'client> Debug for SearchResponse<'client> {
56    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
57        self.inner.fmt(f)
58    }
59}
60
61#[derive(Debug)]
62pub enum SearchResult {
63    Album(AlbumDetail),
64    List(Vec<AlbumInfo>),
65}
66
67#[derive(Debug)]
68pub struct AlbumInfo {
69    pub id: String,
70
71    pub title: MultiLanguageString,
72    pub catalog: Option<String>,
73    pub release_date: String,
74}
75
76#[derive(Debug)]
77pub struct AlbumDetail {
78    pub link: String,
79    info: AlbumInfo,
80    pub discs: Vec<Disc>,
81}
82
83impl FromStr for AlbumDetail {
84    type Err = VGMError;
85
86    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
87        let document = Document::from(s);
88
89        // 1. title
90        let title = document.find(Name("h1")).next().unwrap();
91        let title = parse_multi_language(&title);
92
93        let info = document
94            .find(Attr("id", "album_infobit_large"))
95            .nth(0)
96            .unwrap();
97
98        // 2. catalog
99        let mut catalog = None;
100        // 3. release date
101        let mut release_date = None;
102
103        for line in info.find(Name("tr")) {
104            if let Some(key) = line
105                .find(Name("span").and(Class("label")).descendant(Name("b")))
106                .next()
107            {
108                let key = key.text();
109                if key == "Catalog Number" {
110                    let value = line.last_child().unwrap();
111                    let value = if let Some(value) = value
112                        .find(Attr("id", "childbrowse").descendant(Name("a")))
113                        .next()
114                    {
115                        value.text()
116                    } else {
117                        value.text()
118                    };
119                    catalog = Some(value.trim().to_string());
120                    if let Some("N/A") = catalog.as_deref() {
121                        catalog = None;
122                    }
123                } else if key == "Release Date" {
124                    let value = line.last_child().unwrap().text();
125                    release_date = parse_date(value.trim()).ok();
126                }
127            }
128        }
129
130        let mut album = AlbumDetail {
131            link: "".to_string(), // TODO: get link
132            info: AlbumInfo {
133                id: "".to_string(), // TODO: add id
134                title,
135                catalog,
136                release_date: release_date.unwrap(),
137            },
138            discs: vec![],
139        };
140
141        // 4. track_list
142        let track_list_nav = document.find(Attr("id", "tlnav")).next().unwrap();
143        let track_list = document.find(Attr("id", "tracklist")).next().unwrap();
144        for list in track_list.find(Attr("class", "tl")) {
145            let reference = list.attr("id").unwrap();
146            let language = track_list_nav
147                .find(Attr("rel", reference))
148                .next()
149                .unwrap()
150                .text();
151
152            let mut discs = Vec::new();
153            for disc in list.find(Attr("style", "font-size:8pt").descendant(Name("b"))) {
154                let disc_title = disc.text();
155                let mut table = disc.parent().unwrap();
156                loop {
157                    table = table.next().unwrap();
158                    if let Some("table") = table.name() {
159                        break;
160                    }
161                }
162                let mut tracks = Vec::new();
163                for track in table.find(Name("tr")) {
164                    let track_name = track
165                        .find(Name("td").and(Attr("width", "100%")))
166                        .next()
167                        .unwrap()
168                        .text();
169                    let track_name = track_name.trim().to_string();
170                    tracks.push(track_name);
171                }
172                discs.push((disc_title, tracks));
173            }
174
175            if album.discs.is_empty() {
176                // initialize MultiLanguage tracks
177                album.discs.append(
178                    &mut discs
179                        .into_iter()
180                        .map(|(title, tracks)| {
181                            let tracks = tracks
182                                .into_iter()
183                                .map(|track| {
184                                    let mut tracks = MultiLanguageString::default();
185                                    tracks.insert(language.to_string(), track);
186                                    tracks
187                                })
188                                .collect();
189                            Disc { title, tracks }
190                        })
191                        .collect::<Vec<_>>(),
192                );
193            } else {
194                for (disc, (_, tracks)) in album.discs.iter_mut().zip(discs.into_iter()) {
195                    for (track, new_track) in disc.tracks.iter_mut().zip(tracks.into_iter()) {
196                        track.insert(language.to_string(), new_track);
197                    }
198                }
199            }
200        }
201
202        Ok(album)
203    }
204}
205
206impl AlbumDetail {
207    pub fn title(&self) -> Option<&str> {
208        self.info.title.get()
209    }
210
211    pub fn catalog(&self) -> Option<&str> {
212        self.info.catalog.as_deref()
213    }
214
215    pub fn release_date(&self) -> &str {
216        &self.info.release_date
217    }
218}
219
220#[derive(Debug)]
221pub struct Disc {
222    pub title: String,
223    pub tracks: Vec<MultiLanguageString>,
224}
225
226#[derive(Debug, Default)]
227pub struct MultiLanguageString(HashMap<String, String>);
228
229impl MultiLanguageString {
230    pub fn insert(&mut self, language: String, value: String) {
231        self.0.insert(language, value);
232    }
233
234    pub fn get(&self) -> Option<&str> {
235        self.0
236            .get("ja")
237            .or_else(|| self.0.get("Japanese"))
238            .or_else(|| self.0.get("English"))
239            .or_else(|| self.0.values().next())
240            .map(|s| s.as_str())
241    }
242}