1
2use std::io::Read;
10use once_cell::sync::Lazy;
11use parking_lot::Mutex;
12use serde::{Deserialize, Serialize};
13use ureq::Request;
14use Filter::*;
15
16static USER_AGENT: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
20
21static API_URL: &'static str = "https://tasvideos.org/api/v1/";
23
24pub fn set_user_agent(agent: &str) {
34 *USER_AGENT.try_lock().expect("unable to get lock while setting user agent") = agent.to_string();
35}
36
37pub const MAX_PAGE_LENGTH: u32 = 100;
38
39#[derive(Debug)]
40pub enum Error {
41 Ureq(ureq::Error),
42 Io(std::io::Error),
43}
44impl From<ureq::Error> for Error {
45 fn from(value: ureq::Error) -> Self {
46 Self::Ureq(value)
47 }
48}
49impl From<std::io::Error> for Error {
50 fn from(value: std::io::Error) -> Self {
51 Self::Io(value)
52 }
53}
54
55pub type Result<T> = std::result::Result<T, Error>;
56
57
58#[derive(Clone, Eq, PartialEq, Debug)]
65pub enum Filter {
66 Statuses(Vec<String>),
67 Users(Vec<String>),
68 Systems(Vec<String>),
69 ClassNames(Vec<String>),
70 StartYear(i32),
71 EndYear(i32),
72 GenreNames(Vec<String>),
73 TagNames(Vec<String>),
74 FlagNames(Vec<String>),
75 AuthorIds(Vec<i32>),
76 ShowObsoleted(bool),
77 GameIds(Vec<i32>),
78 Games(Vec<i32>),
79 GameGroupIds(Vec<i32>),
80 PageSize(u32),
81 CurrentPage(u32),
82 Sorts(Vec<String>),
83}
84impl Filter {
85 pub fn as_query(&self) -> (&'static str, String) {
86 match self {
87 Statuses(x) => ("Statuses", x.join(",")),
88 Users(x) => ("User", x.join(",")),
89 Systems(x) => ("Systems", x.join(",")),
90 ClassNames(x) => ("ClassNames", x.join(",")),
91 StartYear(x) => ("StartYear", x.to_string()),
92 EndYear(x) => ("EndYear", x.to_string()),
93 GenreNames(x) => ("GenreNames", x.join(",")),
94 TagNames(x) => ("TagNames", x.join(",")),
95 FlagNames(x) => ("FlagNames", x.join(",")),
96 AuthorIds(x) => ("AuthorIds", itertools::intersperse(x.iter().map(|x| x.to_string()), ",".into()).collect()),
97 ShowObsoleted(x) => ("ShowObsoleted", x.to_string()),
98 GameIds(x) => ("GameIds", itertools::intersperse(x.iter().map(|x| x.to_string()), ",".into()).collect()),
99 Games(x) => ("Games", itertools::intersperse(x.iter().map(|x| x.to_string()), ",".into()).collect()),
100 GameGroupIds(x) => ("GameGroupIds", itertools::intersperse(x.iter().map(|x| x.to_string()), ",".into()).collect()),
101 PageSize(x) => ("PageSize", x.to_string()),
102 CurrentPage(x) => ("CurrentPage", x.to_string()),
103 Sorts(x) => ("Sort", x.join(",")),
104 }
105 }
106}
107
108#[derive(Clone, Deserialize, Serialize, Eq, PartialEq, Debug, Default)]
110#[serde(rename_all = "camelCase")]
111pub struct GameVersion {
112 pub id: Option<i32>,
113 pub md5: Option<String>,
114 pub sha1: Option<String>,
115 pub name: Option<String>,
116 #[serde(rename = "type")]
117 pub kind: Option<i32>,
118 pub region: Option<String>,
119 pub version: Option<String>,
120 pub system_code: Option<String>,
121}
122
123#[derive(Clone, Deserialize, Serialize, Eq, PartialEq, Debug, Default)]
125#[serde(rename_all = "camelCase")]
126pub struct Game {
127 pub id: Option<i32>,
128 pub versions: Option<Vec<GameVersion>>,
129 pub display_name: Option<String>,
130 pub abbreviation: Option<String>,
131 pub aliases: Option<String>,
132 pub screenshot_url: Option<String>,
133}
134
135#[derive(Clone, Deserialize, Serialize, PartialEq, Debug, Default)]
137#[serde(rename_all = "camelCase")]
138pub struct Publication {
139 pub id: Option<i32>,
140 pub title: Option<String>,
141 pub branch: Option<String>,
142 pub emulator_version: Option<String>,
143 pub class: Option<String>,
144 pub system_code: Option<String>,
145 pub submission_id: Option<i32>,
146 pub game_id: Option<i32>,
147 pub game_version_id: Option<i32>,
148 pub obsoleted_by_id: Option<i32>,
149 pub frames: Option<i32>,
150 pub rerecord_count: Option<i32>,
151 pub system_frame_rate: Option<f64>,
152 pub movie_file_name: Option<String>,
153 pub additional_authors: Option<String>,
154 pub authors: Option<Vec<String>>,
155 pub tags: Option<Vec<String>>,
156 pub flags: Option<Vec<String>>,
157 pub urls: Option<Vec<String>>,
158 pub file_paths: Option<Vec<String>>,
159 pub create_timestamp: Option<String>,
160}
161
162#[derive(Clone, Deserialize, Serialize, PartialEq, Debug, Default)]
164#[serde(rename_all = "camelCase")]
165pub struct Submission {
166 pub id: Option<i32>,
167 pub publication_id: Option<i32>,
168 pub title: Option<String>,
169 pub intended_class: Option<String>,
170 pub judge: Option<String>,
171 pub publisher: Option<String>,
172 pub status: Option<String>,
173 pub movie_extension: Option<String>,
174 pub game_id: Option<i32>,
175 pub game_name: Option<String>,
176 pub game_version_id: Option<i32>,
177 pub game_version: Option<String>,
178 pub system_code: Option<String>,
179 pub system_frame_rate: Option<f64>,
180 pub frames: Option<i32>,
181 pub rerecord_count: Option<i32>,
182 pub encode_embed_link: Option<String>,
183 pub branch: Option<String>,
184 pub rom_name: Option<String>,
185 pub emulator_version: Option<String>,
186 pub movie_start_type: Option<i32>,
187 pub additional_authors: Option<String>,
188 pub authors: Option<Vec<String>>,
189 pub create_timestamp: Option<String>,
190}
191
192#[derive(Clone, Deserialize, Serialize, PartialEq, Debug, Default)]
194#[serde(rename_all = "camelCase")]
195pub struct SystemFrameRate {
196 pub id: Option<i32>,
197 pub frame_rate: Option<f64>,
198 pub region_code: Option<String>,
199 pub preliminary: Option<bool>,
200 pub obsolete: Option<bool>,
201}
202
203#[derive(Clone, Deserialize, Serialize, PartialEq, Debug, Default)]
205#[serde(rename_all = "camelCase")]
206pub struct System {
207 pub id: Option<i32>,
208 pub code: Option<String>,
209 pub display_name: Option<String>,
210 pub system_frame_rates: Option<Vec<SystemFrameRate>>,
211}
212
213
214pub fn get(endpoint: &str) -> Request {
221 let mut builder = ureq::get(&[API_URL, endpoint].concat()).set("accept", "application/json");
222
223 let Some(agent) = USER_AGENT.try_lock() else { return builder };
224
225 if !agent.is_empty() {
226 builder = builder.set("user-agent", agent.as_str());
227 }
228
229 builder
230}
231
232
233pub fn get_game(id: i32) -> Result<Game> {
242 let resp = get(&format!("Games/{id}")).call()?;
243
244 Ok(resp.into_json()?)
245}
246
247pub fn get_games<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Game>> {
261 let filters = filters.into_iter()
262 .filter(|f| match f {
263 Systems(_) | PageSize(_) | CurrentPage(_) | Sorts(_) => true,
264 _ => false,
265 })
266 .map(|f| f.as_query());
267
268 let mut req = get("Games");
269 for f in filters {
270 req = req.query(f.0, &f.1);
271 }
272
273 let resp = req.call()?;
274
275 Ok(resp.into_json()?)
276}
277
278pub fn get_games_all<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Game>> {
293 let mut filters: Vec<Filter> = filters.into_iter().collect();
294 filters.retain(|f| match f {
295 PageSize(_) | CurrentPage(_) => false,
296 _ => true,
297 });
298
299 filters.push(PageSize(MAX_PAGE_LENGTH));
300 filters.push(CurrentPage(1));
301
302 let mut games = Vec::new();
303 loop {
304 let data = get_games(filters.clone())?;
305
306 let len = data.len();
307 games.extend(data);
308
309 if len < MAX_PAGE_LENGTH as usize {
310 break
311 }
312
313 for filter in filters.iter_mut() { match filter {
314 CurrentPage(x) => *x += 1,
315 _ => ()
316 }}
317 }
318
319 Ok(games)
320}
321
322
323
324pub fn get_publication(id: i32) -> Result<Publication> {
333 let resp = get(&format!("Publications/{id}")).call()?;
334
335 Ok(resp.into_json()?)
336}
337
338pub fn get_publications<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Publication>> {
354 let filters = filters.into_iter()
355 .filter(|f| match f {
356 Systems(_) | ClassNames(_) | StartYear(_) | EndYear(_) | GenreNames(_) |
357 TagNames(_) | FlagNames(_) | AuthorIds(_) | ShowObsoleted(_) | GameIds(_) |
358 GameGroupIds(_) | PageSize(_) | CurrentPage(_) | Sorts(_) => true,
359 _ => false,
360 })
361 .map(|f| f.as_query());
362
363 let mut req = get("Publications");
364 for f in filters {
365 req = req.query(f.0, &f.1);
366 }
367
368 let resp = req.call()?;
369
370 Ok(resp.into_json()?)
371}
372
373pub fn get_publications_all<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Publication>> {
389 let mut filters: Vec<Filter> = filters.into_iter().collect();
390 filters.retain(|f| match f {
391 PageSize(_) | CurrentPage(_) => false,
392 _ => true,
393 });
394
395 filters.push(PageSize(MAX_PAGE_LENGTH));
396 filters.push(CurrentPage(1));
397
398 let mut publications = Vec::new();
399 loop {
400 let data = get_publications(filters.clone())?;
401
402 let len = data.len();
403 publications.extend(data);
404
405 if len < MAX_PAGE_LENGTH as usize {
406 break
407 }
408
409 for filter in filters.iter_mut() { match filter {
410 CurrentPage(x) => *x += 1,
411 _ => ()
412 }}
413 }
414
415 Ok(publications)
416}
417
418pub fn get_publication_movie(id: i32) -> Result<Vec<u8>> {
422 let resp = ureq::get(&format!("https://tasvideos.org/{id}M?handler=Download"))
423 .set("user-agent", USER_AGENT.try_lock().unwrap().as_str())
424 .call()?;
425
426 let cap = resp
427 .header("content-length")
428 .map(|s| usize::from_str_radix(s, 10).ok())
429 .flatten()
430 .unwrap_or(8192);
431
432 let mut buf = Vec::with_capacity(cap);
433 resp.into_reader().read_to_end(&mut buf)?;
434
435 Ok(buf)
436}
437
438
439pub fn get_submission(id: i32) -> Result<Submission> {
448 let resp = get(&format!("Submissions/{id}")).call()?;
449
450 Ok(resp.into_json()?)
451}
452
453pub fn get_submissions<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Submission>> {
468 let filters = filters.into_iter()
469 .filter(|f| match f {
470 Statuses(_) | Users(_) | StartYear(_) | EndYear(_) | Systems(_) |
471 Games(_) | PageSize(_) | CurrentPage(_) | Sorts(_) => true,
472 _ => false,
473 })
474 .map(|f| f.as_query());
475
476 let mut req = get("Submissions");
477 for f in filters {
478 req = req.query(f.0, &f.1);
479 }
480
481 let resp = req.call()?;
482
483 Ok(resp.into_json()?)
484}
485
486pub fn get_submissions_all<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Submission>> {
501 let mut filters: Vec<Filter> = filters.into_iter().collect();
502 filters.retain(|f| match f {
503 PageSize(_) | CurrentPage(_) => false,
504 _ => true,
505 });
506
507 filters.push(PageSize(MAX_PAGE_LENGTH));
508 filters.push(CurrentPage(1));
509
510 let mut submissions = Vec::new();
511 loop {
512 let data = get_submissions(filters.clone())?;
513
514 let len = data.len();
515 submissions.extend(data);
516
517 if len < MAX_PAGE_LENGTH as usize {
518 break
519 }
520
521 for filter in filters.iter_mut() { match filter {
522 CurrentPage(x) => *x += 1,
523 _ => ()
524 }}
525 }
526
527 Ok(submissions)
528}
529
530pub fn get_submission_movie(id: i32) -> Result<Vec<u8>> {
534 let resp = ureq::get(&format!("https://tasvideos.org/{id}S?handler=Download"))
535 .set("user-agent", USER_AGENT.try_lock().unwrap().as_str())
536 .call()?;
537
538 let cap = resp
539 .header("content-length")
540 .map(|s| usize::from_str_radix(s, 10).ok())
541 .flatten()
542 .unwrap_or(8192);
543
544 let mut buf = Vec::with_capacity(cap);
545 resp.into_reader().read_to_end(&mut buf)?;
546
547 Ok(buf)
548}
549
550pub fn get_userfile(id: u64) -> Result<(Vec<u8>, Option<String>)> {
554 let resp = ureq::get(&format!("https://tasvideos.org/{id}S?handler=Download"))
555 .set("user-agent", USER_AGENT.try_lock().unwrap().as_str())
556 .call()?;
557
558 let filename = resp.header("content-disposition")
559 .filter(|header| !header.is_empty())
560 .map(|header| {
561 header.split(';')
562 .map(|pair| pair.split_once('='))
563 .filter_map(|pair| pair)
564 .find(|(key, _)| key == &"filename")
565 .map(|(_, val)| val.to_string())
566 })
567 .flatten();
568
569 let cap = resp
570 .header("content-length")
571 .map(|s| usize::from_str_radix(s, 10).ok())
572 .flatten()
573 .unwrap_or(8192);
574
575 let mut buf = Vec::with_capacity(cap);
576 resp.into_reader().read_to_end(&mut buf)?;
577
578 Ok((buf, filename))
579}
580
581pub fn get_system(id: i32) -> Result<System> {
590 let resp = get(&format!("Systems/{id}")).call()?;
591
592 Ok(resp.into_json()?)
593}
594
595pub fn get_systems() -> Result<Vec<System>> {
606 let resp = get("Systems").call()?;
607
608 Ok(resp.into_json()?)
609}