artist.d 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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.artist;
  18. import pd.configuration;
  19. import pd.pixiv;
  20. int artistHandle(string[] args, in Config config)
  21. {
  22. import std.algorithm.searching : all, find;
  23. import std.getopt : getopt, GetoptOptions = config;
  24. import std.stdio : stderr, stdout;
  25. import app.util : sleep;
  26. /* artist <id> */
  27. if (args.length < 2) {
  28. displayArtistHelp();
  29. return 3;
  30. }
  31. string enteredType;
  32. int mangaOffset;
  33. int illustOffset;
  34. int novelOffset;
  35. const helpInformation = getopt(args,
  36. GetoptOptions.passThrough,
  37. "type|t", &enteredType,
  38. "skip-illust", &illustOffset,
  39. "skip-manga", &mangaOffset,
  40. "skip-novel", &novelOffset);
  41. if (helpInformation.helpWanted) {
  42. displayArtistHelp();
  43. return 0;
  44. }
  45. bool pred(string arg) { return arg.all!("a >= '0' && a <= '9'"); }
  46. string[] ids = find!(pred)(args);
  47. if (enteredType != "illust" && enteredType != "manga" &&
  48. enteredType != "novel" && enteredType != "") {
  49. stderr.writefln("%s: Invalid Content Type: %s", args[0], enteredType);
  50. stderr.writeln("Valid Content Types are: 'illust', 'manga', and 'novel'.");
  51. stderr.writeln("To download all content, do not use the --type option.");
  52. return 3;
  53. }
  54. // --skip-manga and --skip-illust are only valid when there is one
  55. // account ID provided.
  56. if (ids.length > 1 && (mangaOffset != 0 || illustOffset != 0 || novelOffset != 0)) {
  57. stderr.writefln("%s: Cannot provide an offset for more than one artist.",
  58. args[0]);
  59. stderr.writefln("Run '%s artist --help' for more information.", args[0]);
  60. return 1;
  61. }
  62. foreach (id; ids) {
  63. switch (enteredType) {
  64. case "illust":
  65. downloadArtist(id, "illust", illustOffset, config);
  66. break;
  67. case "manga":
  68. downloadArtist(id, "manga", mangaOffset, config);
  69. break;
  70. case "novel":
  71. downloadArtist(id, "novel", novelOffset, config);
  72. break;
  73. default:
  74. downloadArtist(id, "novel", novelOffset, config);
  75. sleep(5, 10);
  76. downloadArtist(id, "manga", mangaOffset, config);
  77. sleep(5, 10);
  78. downloadArtist(id, "illust", illustOffset, config);
  79. break;
  80. }
  81. sleep(10, 20);
  82. }
  83. return 0;
  84. }
  85. ///
  86. /// Display the help message for the "artist" command.
  87. ///
  88. void displayArtistHelp()
  89. {
  90. import std.stdio : stderr;
  91. stderr.writeln("pixiv_down artist - Download content from specified artists\n" ~
  92. "\nUsage:\tpixiv_down artist [options] <IDs...>\n" ~
  93. "\nBy default, running the command with no options will result in\n" ~
  94. "downloading all content from each supported content type. You\n" ~
  95. "can change this by using the \"--type\" option.\n" ~
  96. "\nOptions:\n" ~
  97. " -h, --help \tDisplay this help message and exit.\n" ~
  98. " -t, --type \tLimit the type of content to download.\n" ~
  99. " \t Valid values are: illust, manga, novel\n" ~
  100. " --skip-manga \tThe number of manga to skip when\n" ~
  101. " \tdownloading the specified artist.\n" ~
  102. " --skip-illust\tThe number of illustrations to skip when\n" ~
  103. " \tdownloading the specified artist.\n" ~
  104. " --skip-novel \tThe number of novels to skip when\n" ~
  105. " \tdownloading the specified artist.\n" ~
  106. "\nExamples:\n" ~
  107. "\n Download all content from an artist:\n" ~
  108. " pixiv_down artist 10109777\n" ~
  109. "\n Download all manga from an artist:\n" ~
  110. " pixiv_down artist --type manga 10109777\n" ~
  111. "\n Download all illustrations from multiple artists:\n" ~
  112. " pixiv_down artist --type illust 10109777 4938312\n" ~
  113. "\n Skip the first 10 illustrations from an artist.\n" ~
  114. " pixiv_down artist --skip-illust 10 10109777\n" ~
  115. "\nNotes:\n" ~
  116. "The --skip-manga, --skip-illust, and --skip-novel options only work\n" ~
  117. "when a single artist ID has been provided. They will NOT restrict\n" ~
  118. "the content type downloaded.");
  119. }
  120. private:
  121. void downloadArtist(string id, string type, int offset, in Config config)
  122. {
  123. import std.experimental.logger;
  124. import std.net.curl : CurlException;
  125. import std.stdio : stdout, stderr;
  126. import pd.pixiv;
  127. import pd.pixiv_downloader;
  128. import mlib.term;
  129. import app.util : sleep;
  130. const user = fetchUser(id, config);
  131. const userProfile = fetchUserProfie(id, config);
  132. if ("illust" == type) {
  133. infof("number of illustrations to download: %d", userProfile.illusts.length);
  134. if (0 == userProfile.illusts.length) {
  135. stdout.writefln("No illustrations to download from %s.", user.userName);
  136. return;
  137. } else if (offset >= userProfile.illusts.length) {
  138. stdout.writefln("No illustrations to download from %s - they were all skipped!", user.userName);
  139. return;
  140. }
  141. stdout.writefln("Downloading illustrations by %s.", user.userName);
  142. foreach (elemNum, illustId; userProfile.illusts[offset..$]) {
  143. auto info = fetchArtworkInfo(illustId, config);
  144. try {
  145. downloadArtwork(info, config);
  146. } catch (CurlException ce) {
  147. errorf("exception in curl: %s", ce.msg);
  148. Term.clearCurrentLine(Yes.useStderr);
  149. stderr.writefln("Failed to download %s. Will retry.", info.type);
  150. sleep(20, 30);
  151. downloadArtwork(info, config);
  152. }
  153. if (elemNum == 0 || elemNum % 10 != 0) {
  154. sleep(4, 10);
  155. } else {
  156. sleep(10, 20);
  157. }
  158. Term.goUpAndClearLine(1, Yes.useStderr);
  159. }
  160. stdout.writefln("Downloaded illustrations by %s.", user.userName);
  161. }
  162. else if ("manga" == type) {
  163. infof("Number of manga to download: %d", userProfile.manga.length);
  164. if (0 == userProfile.manga.length) {
  165. stdout.writefln("No manga to download from %s.", user.userName);
  166. return;
  167. } else if (offset >= userProfile.manga.length) {
  168. stdout.writefln("No manga to download from %s - they were all skipped!", user.userName);
  169. return;
  170. }
  171. stdout.writefln("Downloading manga by %s.", user.userName);
  172. foreach (elemNum, illustId; userProfile.manga[offset..$]) {
  173. ArtworkInfo info = fetchArtworkInfo(illustId, config);
  174. try {
  175. downloadArtwork(info, config);
  176. } catch (CurlException ce) {
  177. errorf("exception in curl: %s", ce.msg);
  178. Term.clearCurrentLine(Yes.useStderr);
  179. stderr.writefln("Failed to download %s. Will retry.", info.type);
  180. sleep(20, 30);
  181. downloadArtwork(info, config);
  182. }
  183. if (elemNum == 0 || elemNum % 10 != 0) {
  184. sleep(4, 10);
  185. } else {
  186. sleep(10, 20);
  187. }
  188. Term.goUpAndClearLine(1, Yes.useStderr);
  189. }
  190. stdout.writefln("Downloaded manga by %s.", user.userName);
  191. }
  192. else if ("novel" == type) {
  193. infof("Number of novels to download: %d", userProfile.novels.length);
  194. if (0 == userProfile.novels.length) {
  195. stdout.writefln("No novels to download from %s.", user.userName);
  196. return;
  197. }
  198. if (offset >= userProfile.novels.length) {
  199. stdout.writefln("No novels to download from %s -- they were all skipped!", user.userName);
  200. return;
  201. }
  202. stdout.writefln("Downloading novels by %s", user.userName);
  203. foreach(elemNum, novelId; userProfile.novels[offset..$]) {
  204. auto info = fetchNovelInfo(novelId, config);
  205. try {
  206. downloadNovel(info, config);
  207. } catch (CurlException ce) {
  208. errorf("exception in curl: %s", ce.msg);
  209. Term.clearCurrentLine(Yes.useStderr);
  210. stderr.writeln("Failed to download novel. Will retry.");
  211. sleep(20, 30);
  212. downloadNovel(info, config);
  213. }
  214. if (elemNum == 0 || elemNum % 10 != 0) {
  215. sleep(4, 10);
  216. } else {
  217. sleep(10, 20);
  218. }
  219. Term.goUpAndClearLine(1, Yes.useStderr);
  220. }
  221. stdout.writefln("Downloaded novels by %s.", user.userName);
  222. }
  223. else
  224. {
  225. import std.format : format;
  226. string msg = format!"Unsupported operation (DOWNLOADING %s)"(type);
  227. throw new Exception(msg);
  228. }
  229. }