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 .and_then(|s| usize::from_str_radix(s, 10).ok())
429 .unwrap_or(8192);
430
431 let mut buf = Vec::with_capacity(cap);
432 resp.into_reader().read_to_end(&mut buf)?;
433
434 Ok(buf)
435}
436
437
438pub fn get_submission(id: i32) -> Result<Submission> {
447 let resp = get(&format!("Submissions/{id}")).call()?;
448
449 Ok(resp.into_json()?)
450}
451
452pub fn get_submissions<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Submission>> {
467 let filters = filters.into_iter()
468 .filter(|f| match f {
469 Statuses(_) | Users(_) | StartYear(_) | EndYear(_) | Systems(_) |
470 Games(_) | PageSize(_) | CurrentPage(_) | Sorts(_) => true,
471 _ => false,
472 })
473 .map(|f| f.as_query());
474
475 let mut req = get("Submissions");
476 for f in filters {
477 req = req.query(f.0, &f.1);
478 }
479
480 let resp = req.call()?;
481
482 Ok(resp.into_json()?)
483}
484
485pub fn get_submissions_all<F: IntoIterator<Item = Filter>>(filters: F) -> Result<Vec<Submission>> {
500 let mut filters: Vec<Filter> = filters.into_iter().collect();
501 filters.retain(|f| match f {
502 PageSize(_) | CurrentPage(_) => false,
503 _ => true,
504 });
505
506 filters.push(PageSize(MAX_PAGE_LENGTH));
507 filters.push(CurrentPage(1));
508
509 let mut submissions = Vec::new();
510 loop {
511 let data = get_submissions(filters.clone())?;
512
513 let len = data.len();
514 submissions.extend(data);
515
516 if len < MAX_PAGE_LENGTH as usize {
517 break
518 }
519
520 for filter in filters.iter_mut() { match filter {
521 CurrentPage(x) => *x += 1,
522 _ => ()
523 }}
524 }
525
526 Ok(submissions)
527}
528
529pub fn get_submission_movie(id: i32) -> Result<Vec<u8>> {
533 let resp = ureq::get(&format!("https://tasvideos.org/{id}S?handler=Download"))
534 .set("user-agent", USER_AGENT.try_lock().unwrap().as_str())
535 .call()?;
536
537 let cap = resp
538 .header("content-length")
539 .and_then(|s| usize::from_str_radix(s, 10).ok())
540 .unwrap_or(8192);
541
542 let mut buf = Vec::with_capacity(cap);
543 resp.into_reader().read_to_end(&mut buf)?;
544
545 Ok(buf)
546}
547
548pub fn get_userfile(id: u64) -> Result<(Vec<u8>, Option<String>)> {
550 let resp = ureq::get(&format!("https://tasvideos.org/UserFiles/Info/{id}?handler=Download"))
551 .set("user-agent", USER_AGENT.try_lock().unwrap().as_str())
552 .call()?;
553
554 let filename = resp.header("content-disposition")
555 .filter(|header| !header.is_empty())
556 .map(|header| {
557 header.split(';')
558 .filter_map(|pair| pair.trim().split_once('='))
559 .find(|(key, _)| key == &"filename")
560 .map(|(_, val)| val.trim_start_matches('"').trim_end_matches('"').to_string())
561 })
562 .flatten();
563
564 let cap = resp
565 .header("content-length")
566 .and_then(|s| usize::from_str_radix(s, 10).ok())
567 .unwrap_or(8192);
568
569 let mut buf = Vec::with_capacity(cap);
570 resp.into_reader().read_to_end(&mut buf)?;
571
572 Ok((buf, filename))
573}
574
575pub fn get_system(id: i32) -> Result<System> {
584 let resp = get(&format!("Systems/{id}")).call()?;
585
586 Ok(resp.into_json()?)
587}
588
589pub fn get_systems() -> Result<Vec<System>> {
600 let resp = get("Systems").call()?;
601
602 Ok(resp.into_json()?)
603}