bookmarked.d 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /*
  2. * pixiv_down - CLI-based downloading tool for https://www.pixiv.net.
  3. * Copyright (C) 2024 Mio
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, version 3 of the License.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. */
  17. module app.cmds.bookmarked;
  18. import pd.configuration: Config;
  19. import std.stdio;
  20. public void displayBookmarkedHelp()
  21. {
  22. stderr.writefln(
  23. "pixiv_down bookmarked - download bookmarked works.\n" ~
  24. "\nUsage:\tpixiv_down bookmarked [options]\n" ~
  25. "\nThis command allows you to download all of your bookmarked works.\n" ~
  26. "A list containing the works that are no longer available (because\n" ~
  27. "they've been removed or made private) can be found in a file called\n" ~
  28. "'pixiv_down-missing.txt' after running the `bookmarked' command.\n" ~
  29. "\nOptions:\n" ~
  30. " -h, --help \tDisplay this help message and exit.\n" ~
  31. " -s, --skip OFFSET \tSkip downloading the first OFFSET creators.\n" ~
  32. " --novels \tDownload bookmarked novels instead of artworks.\n" ~
  33. " --private \tDownload your privately bookmarked works instead.\n" ~
  34. " --remove-invalid \tRemove bookmarks for works that are no longer available.\n" ~
  35. " --remove-from-file FILE\tRemove bookmarks for works listed in FILE.\n" ~
  36. "\nThe --remove-invalid and --remove-from-file options DO NOT remove any\n" ~
  37. "files from your system, they only remove the \"bookmarked\" status on\n" ~
  38. "pixiv. The FILE for --remove-from-file is expected to be the generated\n" ~
  39. "pixiv_down-missing.txt.");
  40. }
  41. /**
  42. * Download bookmarked content.
  43. *
  44. * The expected format of *args* is: `["pixiv_down", "bookmarked", ...options]`
  45. *
  46. * Params:
  47. * args = The arguments passed to pixiv_down.
  48. * config = pixiv_down configuration
  49. * Returns: 0 on success, non-zero on error
  50. */
  51. public int bookmarkedHandle(string[] args, in Config config)
  52. {
  53. import std.file: exists;
  54. import std.getopt: getopt, GetOptException;
  55. Options options;
  56. try {
  57. auto helpInformation = getopt(args,
  58. "private|p", &options.privateRequested,
  59. "skip|s", &options.offset,
  60. "remove-invalid", &options.removeInvalid,
  61. "novels", &options.novelsRequested,
  62. "remove-from-file", &options.removalFilePath);
  63. if (!options.removeInvalid) {
  64. // --remove-invalid is a flag. Not providing it means it's false.
  65. options.removeInvalid = config.bookmarked.alwaysRemoveInvalid;
  66. }
  67. if (helpInformation.helpWanted) {
  68. displayBookmarkedHelp();
  69. return 0;
  70. }
  71. } catch (GetOptException e) {
  72. stderr.writefln("pixiv_down bookmarked: %s", e.msg);
  73. stderr.writefln("Run 'pixiv_down bookmarked --help' for more information.");
  74. return 1;
  75. }
  76. if ((options.removalFilePath != string.init) && (exists(options.removalFilePath))) {
  77. removeBookmarksFromFile(options, config);
  78. return 0;
  79. }
  80. fetchAndDownloadBookmarks(options, config);
  81. return 0;
  82. }
  83. private:
  84. import pd.pixiv;
  85. import pd.pixiv_downloader;
  86. import std.experimental.logger;
  87. import app.util;
  88. import mlib.term;
  89. struct Options
  90. {
  91. bool privateRequested;
  92. bool removeInvalid;
  93. bool novelsRequested;
  94. long offset;
  95. string removalFilePath;
  96. }
  97. void fetchAndDownloadBookmarks(in Options options, in Config config)
  98. {
  99. long totalIDs;
  100. long processedIDs = options.offset;
  101. long numberOfMissingIDs = 0;
  102. const csrfToken = fetchCSRFToken(config.sessionid);
  103. do {
  104. trace("fetching user bookmarks...");
  105. Bookmarks bookmarks = fetchUserBookmarks(
  106. options.novelsRequested ? "novels" : "illusts",
  107. options.privateRequested, processedIDs, config);
  108. totalIDs = bookmarks.total;
  109. /* Break early incase someone has manually removed bookmarks
  110. * while we've been downloading. */
  111. if (totalIDs <= processedIDs) {
  112. warningf("totalIDs (≈%d) has changed to be less than processedIDs (%d)", totalIDs,
  113. processedIDs);
  114. break;
  115. }
  116. string[] missingIDs = downloadBookmarks(bookmarks, csrfToken, options, config);
  117. numberOfMissingIDs += missingIDs.length;
  118. if (missingIDs.length > 0) {
  119. writeMissingIDs(missingIDs, options.removeInvalid);
  120. }
  121. processedIDs += bookmarks.works.length;
  122. sleep(5, 10);
  123. Term.goUpAndClearLine(Yes.useStderr);
  124. writefln("-- Downloaded %d of %d bookmarks. --", processedIDs, totalIDs);
  125. } while (processedIDs < totalIDs);
  126. trace("finished downloading bookmarks");
  127. if (numberOfMissingIDs > 0) {
  128. info("missing IDs were found");
  129. writefln("Warning: %d works are no longer available.", numberOfMissingIDs);
  130. writeln( " You can find a list of work IDs at 'pixiv_down-missing.txt'.");
  131. if (false == options.removeInvalid) {
  132. writeln("\nTip: Use the '--remove-invalid' flag to un-bookmark unavailable works.");
  133. }
  134. }
  135. writeln("Finished downloading bookmarked works.");
  136. }
  137. /// Returns an array containing all Missing IDs.
  138. string[] downloadBookmarks(Bookmarks bookmarks, in string csrfToken, in Options options, in Config config)
  139. {
  140. import std.format : format;
  141. string[] missingIDs;
  142. const type = options.novelsRequested ? "novels" : "illusts";
  143. foreach(bookmark; bookmarks.works) {
  144. if (bookmark.isMasked) {
  145. warningf("masked bookmark ID:%s reason:%s", bookmark.id, bookmark.maskReason);
  146. missingIDs ~= format("%s\t%s\t%s", bookmark.id, bookmark.bookmarkData.id, type);
  147. if (options.removeInvalid) {
  148. const success = postBookmarksDelete(bookmark.bookmarkData.id, csrfToken, type, config);
  149. if (success) {
  150. info("successfully removed masked bookmark");
  151. }
  152. sleep(1, 3, false);
  153. }
  154. continue;
  155. }
  156. if (options.novelsRequested) {
  157. NovelInfo novelInfo = fetchNovelInfo(bookmark.id, config);
  158. downloadNovel(novelInfo, config);
  159. } else {
  160. ArtworkInfo artworkInfo = fetchArtworkInfo(bookmark.id, config);
  161. downloadArtwork(artworkInfo, config);
  162. }
  163. sleep(4, 9);
  164. Term.goUpAndClearLine(Yes.useStderr);
  165. }
  166. return missingIDs;
  167. }
  168. void writeMissingIDs(string[] ids, bool removeInvalid)
  169. {
  170. import std.datetime.systime : Clock;
  171. import app.pd_version: VCS_TAG;
  172. import app.logger: PIXIV_DOWN_VERSION_STRING;
  173. static bool loggedHeader = false;
  174. string openMode = loggedHeader ? "a+" : "w+";
  175. File idFile = File("pixiv_down-missing.txt", openMode);
  176. if (false == loggedHeader) {
  177. idFile.writefln("# List created on %s by pixiv_down/%s (%s)", Clock.currTime.toSimpleString(),
  178. PIXIV_DOWN_VERSION_STRING, VCS_TAG);
  179. if (removeInvalid) {
  180. idFile.writefln("# The following IDs were un-bookmarked because they have been");
  181. idFile.writeln("# removed from pixiv and pixiv_down was run with --remove-invalid");
  182. } else {
  183. idFile.writefln("# The following IDs were found to be removed from pixiv.");
  184. idFile.writeln("# You can run 'pixiv_down bookmarked --remove-invalid' to un-bookmark them.");
  185. }
  186. loggedHeader = true;
  187. }
  188. foreach(id; ids) {
  189. idFile.writeln(id);
  190. }
  191. }
  192. void removeBookmarksFromFile(in Options options, in Config config)
  193. {
  194. import std.algorithm.searching : any;
  195. import std.string : split, stripRight;
  196. import pd.pixiv : postBookmarksDelete;
  197. const csrfToken = fetchCSRFToken(config.sessionid);
  198. bool encounteredErrors = false;
  199. auto missingIDs = File(options.removalFilePath, "r");
  200. foreach(string line; missingIDs.byLineCopy(No.keepTerminator)) {
  201. // Skip empty lines and comment lines
  202. if (line.length == 0 || line[0] == '#') {
  203. continue;
  204. }
  205. // <work_id>\t<bookmark_id>\t<work_type>
  206. const segments = line.split("\t");
  207. if (segments.length != 3) {
  208. continue;
  209. }
  210. const workID = segments[0];
  211. const bookmarkID = segments[1];
  212. const workType = segments[2];
  213. if (any!"a < '0' || a > '9'"(workID)) {
  214. stderr.writefln("Failed to remove bookmark: Invalid work ID: ", workID);
  215. continue;
  216. }
  217. if (any!"a < '0' || a > '9'"(bookmarkID)) {
  218. stderr.writefln("Failed to remove bookmark: Invalid bookmark ID: ", bookmarkID);
  219. errorf("Invalid bookmark ID: %s (workID = %s, workType = %s)", bookmarkID, workID,
  220. workType);
  221. continue;
  222. }
  223. if (workType != "novels" && workType != "illusts") {
  224. stderr.writefln("Failed to remove bookmark: Invalid type: ", workType);
  225. errorf("Invalid bookmark type: %s (workID = %s, bookmarkID = %s)", workType, workID,
  226. bookmarkID);
  227. }
  228. const success = postBookmarksDelete(bookmarkID, csrfToken, workType, config);
  229. if (!success) {
  230. encounteredErrors = true;
  231. stderr.writefln("Failed to remove bookmark for ID %s", workID);
  232. errorf("Failed to unbookmark bookmarkID %s (workID = %s, workType = %s)", bookmarkID,
  233. workID, workType);
  234. }
  235. sleep(2, 7);
  236. }
  237. if (encounteredErrors) {
  238. stderr.writeln("There were some errors removing bookmarks.");
  239. }
  240. }