ug_scraper/
lib.rs

1// UG-Scraper - A basic rust API for getting data from Ultimate Guitar
2// Copyright (C) 2025  Linus Tibert
3//
4// This program was originally published under the MIT licence as seen
5// here: https://github.com/Lich-Corals/ug-tab-scraper-rs/blob/mistress/LICENCE
6
7/// API for getting a tab from UG
8pub mod tab_scraper;
9/// API for searching tabs on UG
10pub mod search_scraper;
11/// Functions used by other modules for network access
12pub mod network;
13
14/// Errors possibly occuring in the crate
15pub mod error {
16        use std::error::Error;
17        use std::fmt;
18
19        /// Possible errors
20        #[derive(Debug, PartialEq, Clone, Eq, Hash)]
21        pub enum UGError {
22                /// Occurs when an unsupported HTML is attempted to be evaluated.
23                InvalidHTMLError,
24                /// Occurs when an unsupported URL is attempted to be downloaded as a tab.
25                InvalidURLError,
26                /// Occurs when a tab without any available metadata is attempted to be downloaded.
27                NoBasicDataMatchError,
28                /// Occurs when any data extracting function gets unexpected data from UG.
29                /// 
30                /// E.g. if a string value is found in a place where a float is expected.
31                UnexpectedWebResultError,
32                /// Is returned by types_and_values::get_data_type() if the provided string does not match any known type of tab.
33                UnknownTypeError,
34        }
35
36        impl fmt::Display for UGError {
37                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38                        write!(f, "{}", self.clone().to_string())
39                }
40        }
41
42        impl Error for UGError {}
43
44        impl UGError {
45                /// Returns a brief description of the error in string format.
46                pub fn to_string(self) -> String {
47                        match self {
48                                UGError::InvalidHTMLError => "The type of this page is not readable for this API.".to_string(),
49                                UGError::InvalidURLError => "The URL does not match any known UG sites.".to_string(),
50                                UGError::NoBasicDataMatchError => "Could not find any basic data for the page.".to_string(),
51                                UGError::UnexpectedWebResultError => "Failed to analyze downloaded results.".to_string(),
52                                UGError::UnknownTypeError => "The type supplied by UG is not known.".to_string(),
53                        }
54                }
55        }
56}
57
58/// Types, constants and closely associated functions which are used by across the crate
59/// 
60/// All of the defined types have the [`serde::Serialize`] and [`serde::Deserialize`] traits.
61pub mod types {
62        use std::fmt;
63        use crate::error::UGError;
64        use serde::{Deserialize, Serialize};
65
66        /// A list of tab types supported for downloading
67        /// 
68        /// Includes Chords, Tabs, Bass Tabs, Ukulele Chords and Drum Tabs
69        pub const SUPPORTED_DOWNLOAD_TYPES: [DataSetType; 5] = [DataSetType::Chords, DataSetType::Tab, DataSetType::Bass, DataSetType::Ukulele, DataSetType::Drums];
70
71        /// Known types of tab on UG. Includes unsupported ones.
72        #[derive(Debug, PartialEq, Default, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
73        pub enum DataSetType {
74                #[default]
75                Unknown,
76                /// Downloading supported
77                Chords,
78                /// Downloading supported
79                Tab,
80                /// Downloading supported
81                Ukulele,
82                /// Downloading supported
83                Bass,
84                /// Downloading supported
85                Drums,
86                /// Downloading not supported
87                Official,
88                /// Downloading not supported
89                Pro,
90                /// Downloading not supported
91                Power,
92                /// Downloading not supported
93                Video,
94        }
95
96        impl fmt::Display for DataSetType {
97                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98                        write!(f, "{:?}", self )
99                }
100        }
101
102        /// Possible types of line in a `Song`
103        #[derive(Debug, PartialEq, Eq, Default, Clone, Copy, Hash, Serialize, Deserialize)]
104        pub enum DataType {
105                #[default]
106                /// Lines with Chords detected by UG
107                Chord,
108                /// Plain text
109                Lyric,
110                /// The title of a song section
111                /// 
112                /// (e.g.: \[chorus\], \[intro\], etc.)
113                SectionTitle,
114        }
115
116        impl fmt::Display for DataType {
117                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118                        write!(f, "{:?}", self )
119                }
120        }
121
122        /// A set of data returned as sarch result
123        /// 
124        /// This struct is normally automatically generated by [`crate::search_scraper::get_search_results`] or [`crate::search_scraper::search_page`].
125        #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
126        pub struct SearchResult {
127                /// The basic metadata of the search result (tab)
128                pub basic_data: BasicSongData,
129                /// Amount of ratings given by users on UG
130                pub rating_count: u32,
131                /// Rating on UG (0.0 - 5.0)
132                pub rating_value: f32,
133        }
134
135        impl fmt::Display for SearchResult {
136                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137                        write!(f, "{:?}", self )
138                }
139        }
140
141        /// A single line of a tab
142        /// 
143        /// This struct is normally generated by [`crate::tab_scraper::get_tab_lines`].
144        #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
145        pub struct Line {
146                /// Type data stored on the line
147                pub line_type: DataType,
148                /// The contents on the line as plain text
149                pub text_data: String,
150        }
151
152        impl Line {
153                /// Replace german chord names with english ones
154                /// 
155                /// Musical notation is one of the things Germans did their own, slightly more complicated way.
156                /// This function will replace german names for chords with their english equivalents.
157                /// 
158                /// ## Example:
159                /// ```
160                /// use ug_scraper::types::{Line, DataType};
161                /// 
162                /// // Create a line with German chord names H, Dmoll, Fis, Es and B
163                /// let mut line: Line = Line { line_type: DataType::Chord,
164                ///         text_data: "A    H      C Dmoll    Fis Es B".to_string() };
165                /// 
166                /// // Replace the weïrd chord names
167                /// line = line.replace_german_names();
168                /// // Returns:
169                /// // "A    B      C Dm       F#  Eb Bb"
170                /// 
171                /// ```
172                /// 
173                /// Note:
174                /// Some of the German chord notations (e.g. Fes or Dmoll) are rarely found in any tabs. 
175                /// But to ensure they don't confuse anyone, they are included in this function too.
176                pub fn replace_german_names(mut self) -> Line {
177                        if self.line_type == DataType::Chord {
178                                let entries: [&'static str; 18]      = ["Ces","Cis","Des","Dis","Es","Eis","Fes","Fis","Ges","Gis","As","Ais","H","Bes","His","Bis","dur","moll"];
179                                let replacements: [&'static str; 18] = ["Cb ","C# ","Db ","D# ","Eb","E# ","Fb ","F# ","Gb ","G# ","Ab","A# ","B","Bb ","B# ","B# ","maj","m   "];
180                                if entries.iter().any(|entry: &&str| self.text_data.contains(entry)) {
181                                        self.text_data = self.text_data.replace("B", "Bb")
182                                                                       .replace("Bb  ", "Bb ");
183                                                                       
184                                        for i in 0..entries.len() {
185                                                self.text_data = self.text_data.replace(entries[i], replacements[i]);
186                                        }
187                                }
188                        }
189                        self
190                }
191        }
192
193        impl fmt::Display for Line {
194                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
195                        write!(f, "{:?}", self )
196                }
197        }
198
199        /// A full set of available data about a tab on UG
200        /// 
201        /// This struct is normally generated by [`crate::tab_scraper::get_song_data`].
202        #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
203        pub struct Song {
204                /// A vector of all lines in the tab
205                pub lines: Vec<Line>,
206                /// The detailed metadata of the song.
207                /// 
208                /// This data is optional, because some types of tab (e.g. Drum) don't have any metadata.
209                pub metadata: Option<SongMetaData>,
210                /// Basic data about the tab
211                pub basic_data: BasicSongData,
212        }
213
214        impl fmt::Display for Song {
215                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
216                        write!(f, "{:?}", self )
217                }
218        }
219
220        /// Basic metadata every tab has
221        /// 
222        /// This struct is normally generated by [`crate::tab_scraper::get_basic_metadata`].
223        #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
224        pub struct BasicSongData {
225                /// Title of the song
226                pub title: String,
227                /// Name of the artist
228                pub artist: String,
229                /// Link to the tab
230                pub tab_link: String,
231                /// UG ID of the song
232                /// 
233                /// Don't confuse this with the tab ID, which is only for a single tab!
234                pub song_id: u32,
235                /// UG ID of the tab
236                /// 
237                /// Don't confuse this with the song ID, which is for every tab of the song!
238                pub tab_id: u32,
239                /// The type of tab
240                pub data_type: DataSetType,
241        }
242
243        impl fmt::Display for BasicSongData {
244                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245                        write!(f, "{:?}", self )
246                }
247        }
248
249        /// Special metadata which is not available for every tab (type)
250        /// 
251        /// Tabs of the type `Drums` never have this. Bass tabs often don't have.
252        /// Many tabs are missing values of the metadata; thus, they are all options.
253        /// 
254        /// This struct is normally automatically generated when using [`crate::tab_scraper::get_song_data`].
255        #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
256        pub struct SongMetaData {
257                pub capo: Option<String>,
258                pub tonality: Option<String>,
259                pub tuning_name: Option<String>,
260                pub tuning: Option<String>,
261        }
262
263        impl fmt::Display for SongMetaData {
264                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
265                        write!(f, "{:?}", self )
266                }
267        }
268
269        /// Get the data type associated with a string scraped from UG
270        /// 
271        /// ## Example:
272        /// ```
273        /// use ug_scraper::types::get_data_type;
274        /// 
275        /// get_data_type("Chords");
276        /// // Returns:
277        /// // enum variant DataSetType::Chords
278        /// ```
279        /// 
280        /// ## Supported strings:
281        /// * Chords
282        /// * Tabs
283        /// * Bass Tabs
284        /// * Ukulele Chords
285        /// * Drum Tabs
286        /// * Official
287        /// * Pro
288        /// * Power
289        /// * Video
290        ///
291        /// Returns `UGError::UnknownTypeError` if type is unknown.
292        pub fn get_data_type(type_string: &str) -> Result<DataSetType, UGError> {
293                match type_string {
294                        "Chords" => Ok(DataSetType::Chords),
295                        "Tabs" => Ok(DataSetType::Tab),
296                        "Bass Tabs" => Ok(DataSetType::Bass),
297                        "Ukulele Chords" => Ok(DataSetType::Ukulele),
298                        "Drum Tabs" => Ok(DataSetType::Drums),
299                        "Official" => Ok(DataSetType::Official),
300                        "Pro" => Ok(DataSetType::Pro),
301                        "Power" => Ok(DataSetType::Power),
302                        "Video" => Ok(DataSetType::Video),
303                        _ => Err(UGError::UnknownTypeError),
304                }
305        }
306
307        #[cfg(test)]
308        mod tests {
309            use crate::types::Line;
310
311                #[test]
312                fn german_names_replacement() {
313                        let german_line: Line = Line { line_type: super::DataType::Chord,
314                                text_data: "A    H      C Dmoll    Fis Es B  B".to_string() };
315                        let english_line: Line = Line { line_type: super::DataType::Chord,
316                                text_data: "A    B      C Dm       F#  Eb Bb Bb".to_string() };
317                                assert_eq!(german_line.replace_german_names().text_data, english_line.text_data);
318                }
319        }
320}