map_scanner.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import * as fs from 'fs/promises';
  2. import {constants} from 'fs';
  3. import fetch from 'node-fetch';
  4. import {Beatmap, Calculator} from 'rosu-pp';
  5. import {osu_fetch} from './api.js';
  6. import db from './database.js';
  7. // Promise queue to make sure we're only scanning one map at a time (eliminating race conditions)
  8. const queue = [];
  9. async function run_queue_loop() {
  10. while (queue.length > 0) {
  11. try {
  12. const res = await _get_map_info(queue[0].map_id, queue[0].api_res);
  13. queue[0].resolve(res);
  14. } catch (err) {
  15. queue[0].reject(err);
  16. }
  17. queue.shift();
  18. }
  19. }
  20. function get_map_info(map_id, api_res) {
  21. return new Promise((resolve, reject) => {
  22. queue.push({map_id, api_res, resolve, reject});
  23. // First item in the queue: init queue loop
  24. if (queue.length == 1) run_queue_loop();
  25. });
  26. }
  27. // Get metadata and pp from map ID (downloads it if not already downloaded)
  28. async function _get_map_info(map_id, api_res) {
  29. const map = db.prepare(`SELECT * FROM map WHERE map_id = ?`).get(map_id);
  30. if (map) {
  31. return map;
  32. }
  33. // 1. Download the map
  34. // Looking for .osu files? peppy provides monthly dumps here: https://data.ppy.sh/
  35. const file = `maps/${parseInt(map_id, 10)}.osu`;
  36. try {
  37. await fs.access(file, constants.F_OK);
  38. } catch (err) {
  39. console.log(`Beatmap id ${map_id} not found, downloading it.`);
  40. const new_file = await fetch(`https://osu.ppy.sh/osu/${map_id}`);
  41. const text = await new_file.text();
  42. if (text == '') {
  43. // While in most cases an empty page means the map ID doesn't exist, in
  44. // some rare cases osu! servers actually don't have the .osu file for a
  45. // valid map ID. But we can't do much about it.
  46. throw new Error('Invalid map ID');
  47. }
  48. await fs.writeFile(file, text);
  49. }
  50. // 2. Process it with rosu-pp
  51. const rosu_map = new Beatmap({path: file});
  52. const calc = new Calculator();
  53. const attrs = calc.mapAttributes(rosu_map);
  54. // 3. Get additionnal map info from osu!api
  55. // (we can't get the following just from the .osu file: set_id, length, ranked, dmca)
  56. if (!api_res) {
  57. console.info(`[API] Fetching map data for map ID ${map_id}`);
  58. api_res = await osu_fetch(`https://osu.ppy.sh/api/v2/beatmaps/lookup?id=${map_id}`);
  59. }
  60. // 4. Save map metadata
  61. db.prepare(`
  62. INSERT INTO map (
  63. map_id, name, mode, ar, cs, hp, od, bpm, set_id, length, ranked, dmca
  64. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
  65. map_id, api_res.beatmapset.title, api_res.mode_int,
  66. attrs.ar, attrs.cs, attrs.hp, attrs.od, attrs.bpm,
  67. api_res.beatmapset.id, api_res.total_length, api_res.beatmapset.ranked,
  68. api_res.beatmapset.availability.download_disabled ? 1 : 0,
  69. );
  70. // 5. Process all mod combinations
  71. console.info('Computing pp for map', map_id);
  72. const compute_and_insert = (mods) => {
  73. const calc = new Calculator({mods});
  74. const perf = calc.performance(rosu_map);
  75. db.prepare(
  76. `INSERT INTO pp (map_id, mods, stars, pp) VALUES (?, ?, ?, ?)`,
  77. ).run(
  78. map_id, mods, perf.difficulty.stars, perf.pp,
  79. );
  80. };
  81. const nm = 1 << 0;
  82. const ez = 1 << 1;
  83. const hd = 1 << 3;
  84. const hr = 1 << 4;
  85. const dt = 1 << 6;
  86. const ht = 1 << 8;
  87. const fl = 1 << 10;
  88. compute_and_insert(nm);
  89. compute_and_insert(hd);
  90. compute_and_insert(hr);
  91. compute_and_insert(dt);
  92. compute_and_insert(fl);
  93. compute_and_insert(ez);
  94. compute_and_insert(ht);
  95. compute_and_insert(hd | hr);
  96. compute_and_insert(hd | dt);
  97. compute_and_insert(hd | fl);
  98. compute_and_insert(hr | dt);
  99. compute_and_insert(hr | fl);
  100. compute_and_insert(dt | fl);
  101. compute_and_insert(hd | ez);
  102. compute_and_insert(hd | ht);
  103. compute_and_insert(ez | ht);
  104. compute_and_insert(ez | fl);
  105. compute_and_insert(ht | fl);
  106. compute_and_insert(ez | dt);
  107. compute_and_insert(hr | ht);
  108. compute_and_insert(hd | hr | dt);
  109. compute_and_insert(hd | hr | fl);
  110. compute_and_insert(hd | dt | fl);
  111. compute_and_insert(hr | dt | fl);
  112. compute_and_insert(hd | ez | ht);
  113. compute_and_insert(hd | ez | fl);
  114. compute_and_insert(hd | ht | fl);
  115. compute_and_insert(ez | ht | fl);
  116. compute_and_insert(hd | ez | dt);
  117. compute_and_insert(ez | dt | fl);
  118. compute_and_insert(hd | hr | ht);
  119. compute_and_insert(hr | ht | fl);
  120. compute_and_insert(hd | hr | dt | fl);
  121. compute_and_insert(hd | ez | ht | fl);
  122. compute_and_insert(hd | ez | dt | fl);
  123. compute_and_insert(hd | hr | ht | fl);
  124. return await _get_map_info(map_id, api_res);
  125. }
  126. export {get_map_info};