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}