Leetcode solution's saver

Script saves the file to a folder with corresponding difficulty, name and file extension

  1. // ==UserScript==
  2. // @name Leetcode solution's saver
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.4
  5. // @description Script saves the file to a folder with corresponding difficulty, name and file extension
  6. // @author https://github.com/ruvn-1fgas
  7. // @match https://leetcode.com/*
  8. // @require https://code.jquery.com/jquery-latest.js
  9. // @require https://unpkg.com/turndown/dist/turndown.js
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=leetcode.com
  11. // @grant GM_download
  12. // @grant GM_xmlhttpRequest
  13. // @license MIT
  14.  
  15. // ==/UserScript==
  16. /* globals jQuery, $, waitForKeyElements */
  17.  
  18. const API_URL = 'https://leetcode.com/graphql/';
  19. const LANG_LIST = [{ 'name': 'C++', 'slug': 'cpp', 'id': 0, 'extension': '.cpp' }, { 'name': 'Java', 'slug': 'java', 'id': 1, 'extension': '.java' }, { 'name': 'Python', 'slug': 'python', 'id': 2, 'extension': '.py' }, { 'name': 'MySQL', 'slug': 'mysql', 'id': 3, 'extension': '.sql' }, { 'name': 'C', 'slug': 'c', 'id': 4, 'extension': '.c' }, { 'name': 'C#', 'slug': 'csharp', 'id': 5, 'extension': '.cs' }, { 'name': 'JavaScript', 'slug': 'javascript', 'id': 6, 'extension': '.js' }, { 'name': 'Ruby', 'slug': 'ruby', 'id': 7, 'extension': '.rb' }, { 'name': 'Bash', 'slug': 'bash', 'id': 8, 'extension': '.sh' }, { 'name': 'Swift', 'slug': 'swift', 'id': 9, 'extension': '.swift' }, { 'name': 'Go', 'slug': 'golang', 'id': 10, 'extension': '.go' }, { 'name': 'Python3', 'slug': 'python3', 'id': 11, 'extension': '.py' }, { 'name': 'Scala', 'slug': 'scala', 'id': 12, 'extension': '.scala' }, { 'name': 'Kotlin', 'slug': 'kotlin', 'id': 13, 'extension': '.kt' }, { 'name': 'MS SQL Server', 'slug': 'mssql', 'id': 14, 'extension': '.sql' }, { 'name': 'Oracle', 'slug': 'oraclesql', 'id': 15, 'extension': '.sql' }, { 'name': 'HTML', 'slug': 'html', 'id': 16, 'extension': '.html' }, { 'name': 'Python ML (beta)', 'slug': 'pythonml', 'id': 17, 'extension': '.py' }, { 'name': 'Rust', 'slug': 'rust', 'id': 18, 'extension': '.rs' }, { 'name': 'PHP', 'slug': 'php', 'id': 19, 'extension': '.php' }, { 'name': 'TypeScript', 'slug': 'typescript', 'id': 20, 'extension': '.ts' }, { 'name': 'Racket', 'slug': 'racket', 'id': 21, 'extension': '.rkt' }, { 'name': 'Erlang', 'slug': 'erlang', 'id': 22, 'extension': '.erl' }, { 'name': 'Elixir', 'slug': 'elixir', 'id': 23, 'extension': '.ex' }, { 'name': 'Dart', 'slug': 'dart', 'id': 24, 'extension': '.dart' }, { 'name': 'Python Data Science (beta)', 'slug': 'pythondata', 'id': 25, 'extension': '.py' }, { 'name': 'React', 'slug': 'react', 'id': 26, 'extension': '.js' }, { 'name': 'Vanilla JS', 'slug': 'vanillajs', 'id': 27, 'extension': '.js' }];
  20.  
  21. let turndownService = new TurndownService();
  22. turndownService.addRule('pre', {
  23. filter: ['pre'],
  24. replacement: function (content) {
  25. return '\n```\n' + content + '```'
  26. }
  27. });
  28.  
  29. (function (open) {
  30. XMLHttpRequest.prototype.open = function () {
  31. this.addEventListener("readystatechange", function () {
  32. if (this.readyState == 4 && window.location.href.search('/problems/') != -1) {
  33. main();
  34. }
  35. }, false);
  36. open.apply(this, arguments);
  37. };
  38. })(XMLHttpRequest.prototype.open);
  39.  
  40. function main() {
  41. 'use strict'
  42. let buttons = $('.relative.ml-auto.flex.items-center.gap-3');
  43. if (buttons === undefined) {
  44. return;
  45. }
  46.  
  47. if (buttons.children().length > 3) {
  48. return;
  49. }
  50.  
  51. let downloadButton = createButton('Download', buttons.children().first().attr('class'), { 'margin-right': '4px' }, download);
  52. buttons.prepend(downloadButton);
  53. }
  54.  
  55. function createButton(buttonName, buttonClass, buttonStyle, buttonClick) {
  56. let button = $('<button>' + buttonName + '</button>');
  57. button.addClass(buttonClass);
  58. button.removeClass('cursor-not-allowed');
  59. button.css(buttonStyle);
  60. button.click(buttonClick);
  61. return button;
  62. }
  63.  
  64. async function download() {
  65. const taskInfo = await getTaskInfo();
  66. for (let key in taskInfo) {
  67. if (taskInfo[key] === undefined) {
  68. return;
  69. }
  70. }
  71. const taskDesc = await getTaskDesc(taskInfo);
  72. const taskCode = await getTaskCode(taskInfo);
  73. if (taskCode === undefined) {
  74. return;
  75. }
  76.  
  77. save(taskInfo, taskCode, taskDesc);
  78. }
  79.  
  80. async function fetchData(body, operationName) {
  81. const csrftoken = getCsrfToken();
  82. try {
  83. const response = await fetch(API_URL, {
  84. body,
  85. method: "POST",
  86. headers: {
  87. "x-csrftoken": csrftoken,
  88. "content-type": "application/json",
  89. }
  90. });
  91. const data = await response.json();
  92.  
  93. return data.data[operationName];
  94. } catch (error) {
  95. console.log(error);
  96. }
  97. }
  98.  
  99. async function getTaskInfo() {
  100. const slug = window.location.href.split('/')[4];
  101. let body = `{\"query\":\"\\n query questionTitle($titleSlug: String!) {\\n question(titleSlug: $titleSlug) {\\n questionId\\n questionFrontendId\\n title\\n titleSlug\\n isPaidOnly\\n difficulty\\n likes\\n dislikes\\n }\\n}\\n \",\"variables\":{\"titleSlug\":\"${slug}\"},\"operationName\":\"questionTitle\"}`
  102. const question = await fetchData(body, "question");
  103. const currentLanguageHolder = $('.flex.items-center').filter(function () {
  104. return $(this).attr('class') === 'flex items-center';
  105. });
  106. const childs = Array.from(currentLanguageHolder.children());
  107. const currentLanguage = LANG_LIST.find(({ name }) => childs.some(child => $(child).text().trim() === name));
  108. const { questionId, questionFrontendId, title, titleSlug, difficulty } = question;
  109. return {
  110. taskId: questionId,
  111. taskName: `${questionFrontendId}. ${title}`,
  112. titleSlug: titleSlug,
  113. level: difficulty,
  114. language: currentLanguage?.name,
  115. langId: currentLanguage?.id,
  116. fileExt: currentLanguage?.extension
  117. };
  118. }
  119.  
  120. async function getTaskDesc(taskInfo) {
  121. const { titleSlug } = taskInfo;
  122. const body = `{\"query\":\"\\n query questionContent($titleSlug: String!) {\\n question(titleSlug: $titleSlug) {\\n content\\n mysqlSchemas\\n dataSchemas\\n }\\n}\\n \",\"variables\":{\"titleSlug\":\"${titleSlug}\"},\"operationName\":\"questionContent\"}`
  123.  
  124. const { content } = await fetchData(body, "question");
  125. const markdown = turndownService.turndown(content);
  126. return markdown;
  127. }
  128.  
  129. async function getTaskCode(taskInfo) {
  130. const { langId, taskId } = taskInfo;
  131. const body = `{\"query\":\"\\n query syncedCode($questionId: Int!, $lang: Int!) {\\n syncedCode(questionId: $questionId, lang: $lang) {\\n timestamp\\n code\\n }\\n}\\n \",\"variables\":{\"lang\":${taskInfo.langId},\"questionId\":${taskInfo.taskId}},\"operationName\":\"syncedCode\"}`;
  132.  
  133. const { code } = await fetchData(body, "syncedCode");
  134. return code;
  135. }
  136.  
  137. function getCsrfToken() {
  138. let csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken')).split('=')[1];
  139. return csrfToken;
  140. }
  141.  
  142. async function saveCode(taskInfo, taskCode) {
  143. const { taskName, level, fileExt } = taskInfo;
  144. const filename = `Leetcode/${level}/${taskName}${fileExt}`;
  145. const bl = new Blob([taskCode], { type: `text/${fileExt}` });
  146. const download = {
  147. url: URL.createObjectURL(bl),
  148. name: filename
  149. };
  150. GM_download(download);
  151. }
  152.  
  153. async function save(taskInfo, taskCode, taskDesc) {
  154. const { taskName, level, fileExt } = taskInfo;
  155. const codeFilename = `Leetcode/${level}/${taskName}${fileExt}`;
  156. // check if file exists
  157. let solvedTasks = localStorage.getItem('solved_tasks');
  158. solvedTasks = solvedTasks === null ? [] : solvedTasks.split(',');
  159. let isExists = solvedTasks === null ? false : solvedTasks.includes(`${taskName}${fileExt}`);
  160.  
  161. if (!isExists) {
  162. const codeBl = new Blob([taskCode], { type: `text/${fileExt}` });
  163. const codeDownload = {
  164. url: URL.createObjectURL(codeBl),
  165. name: codeFilename
  166. };
  167. GM_download(codeDownload);
  168.  
  169. if (solvedTasks === null) {
  170. solvedTasks = [];
  171. }
  172.  
  173. solvedTasks.push(`${taskName}${fileExt}`);
  174.  
  175. localStorage.setItem('solved_tasks', solvedTasks);
  176. }
  177.  
  178. let solvedTasksDescs = localStorage.getItem('solved_tasks_descs');
  179. solvedTasksDescs = solvedTasksDescs === null ? [] : solvedTasksDescs.split(',');
  180. isExists = solvedTasksDescs === null ? false : solvedTasksDescs.includes(taskName);
  181.  
  182. if (!isExists) {
  183. const descFilename = `Leetcode/${level}/${taskName}.md`;
  184. const descBl = new Blob([taskDesc], { type: 'text/md' });
  185. const descDownload = {
  186. url: URL.createObjectURL(descBl),
  187. name: descFilename
  188. };
  189. GM_download(descDownload);
  190.  
  191. if (solvedTasksDescs === null) {
  192. solvedTasksDescs = [];
  193. }
  194.  
  195. solvedTasksDescs.push(taskName);
  196.  
  197. localStorage.setItem('solved_tasks_descs', solvedTasksDescs);
  198. }
  199. }