daily.d 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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.daily;
  18. import pd.configuration;
  19. import pd.pixiv;
  20. import pd.pixiv_downloader;
  21. import std.datetime.systime;
  22. import std.experimental.logger;
  23. import mlib.term;
  24. import app.util;
  25. import std.typecons: nullable, Nullable;
  26. public int dailyHandle(string[] args, in Config config)
  27. {
  28. import core.time : TimeException;
  29. import std.algorithm.searching : canFind;
  30. import std.datetime.date : Date;
  31. import std.getopt : getopt, GetOptException, GetOptOption = config;
  32. import std.stdio : stderr;
  33. bool restrict = false;
  34. string rawBeginDate;
  35. string rawEndDate;
  36. SysTime beginDate;
  37. SysTime endDate;
  38. bool force;
  39. try {
  40. getopt(args,
  41. "begin|b", &rawBeginDate,
  42. "end|e", &rawEndDate,
  43. "force|f", &force,
  44. "sfw-only|s", &restrict);
  45. } catch (GetOptException e) {
  46. stderr.writefln("pixiv_down daily: %s", e.msg);
  47. stderr.writefln("Run 'pixiv_down help daily' for more information");
  48. return 1;
  49. }
  50. tracef("current date = %s", (cast(Date)Clock.currTime).toISOExtString);
  51. tracef(" begin date = %s", rawBeginDate);
  52. tracef(" end date = %s", rawEndDate);
  53. beginDate = Clock.currTime;
  54. if (string.init != rawBeginDate) {
  55. try {
  56. beginDate = SysTime.fromISOExtString(rawBeginDate ~ "T00:00:00+00:00");
  57. } catch (TimeException te) {
  58. stderr.writefln("pixiv_down daily: Failed to parse the begin DATE: %s", rawBeginDate);
  59. stderr.writeln("Make sure it's in the format YYYY-MM-DD, for example: 2023-06-20");
  60. return 1;
  61. }
  62. }
  63. /* Automatically determine the end date, based off of the last run
  64. * of this command, but allow people to specify a custom end date. */
  65. if (string.init != rawEndDate) {
  66. try {
  67. endDate = SysTime.fromISOExtString(rawEndDate ~ "T00:00:00+00:00");
  68. } catch (TimeException te) {
  69. stderr.writefln("pixiv_down daily: Failed to parse the end DATE: %s", rawEndDate);
  70. stderr.writeln("Make sure it's in the format YYYY-MM-DD, for example: 2023-06-20");
  71. return 1;
  72. }
  73. } else if (string.init != rawBeginDate) {
  74. stderr.writeln("pixiv_down daily: Cannot determine END date when BEGIN date has been provided.");
  75. stderr.writeln(" Please manually provide and END date with the -e option.");
  76. return 1;
  77. } else {
  78. auto lastRunTime = fetchLastStartTime();
  79. if (lastRunTime.isNull()) {
  80. stderr.writeln("pixiv_down daily: No end date could be determined, please provide date.");
  81. stderr.writeln(" For example `pixiv_down daily -e 2023-06-20`");
  82. return 1;
  83. }
  84. endDate = lastRunTime.get();
  85. }
  86. /*
  87. * If beginDate and endDate are the same, then we want to download
  88. * all images for that day.
  89. */
  90. if (endDate.stdTime == beginDate.stdTime) {
  91. beginDate.hour = 23;
  92. beginDate.minute = 59;
  93. beginDate.second = 59;
  94. }
  95. if (endDate.stdTime > beginDate.stdTime) {
  96. stderr.writeln("pixiv_down daily: Cannot download. Provided END date is after BEGIN date.");
  97. stderr.writeln(" pixiv_down downloads from newer date to older date.");
  98. return 1;
  99. }
  100. stderr.writeln("Begin = ", (cast(Date)beginDate).toISOExtString());
  101. stderr.writeln("End = ", (cast(Date)endDate).toISOExtString());
  102. downloadDaily(DailyOptions(beginDate, endDate, restrict, force), config);
  103. saveLastStartTime(beginDate);
  104. return 0;
  105. }
  106. void displayDailyHelp()
  107. {
  108. import std.stdio : stderr;
  109. stderr.writefln(
  110. "pixiv_down daily - Download new content from followed artists.\n" ~
  111. "\nUsage:\tpixiv_down daily [options]\n" ~
  112. "\nThis command will, by default, download all the recently uploaded\n" ~
  113. "from the people you follow up to (and including) the end date.\n" ~
  114. "This will also download both the \"safe\" and \"r18\" works. Use\n" ~
  115. "the --sfw-only flag to change this.\n" ~
  116. "\nBoth the BEGIN and END dates must follow the format YYYY-MM-DD,\n" ~
  117. "for example: 2023-06-20. The END date is only required for the\n" ~
  118. "first run of the daily command. Afterwards, the end date will be\n" ~
  119. "determined automatically based on the last run's BEGIN date,\n" ~
  120. "regardless of whether a BEGIN date was provided or not.\n" ~
  121. "\nOptions:\n" ~
  122. " -b, --begin BEGIN \tThe date the begin downloading from.\n" ~
  123. " -e, --end END \tThe date to finish downloading at.\n" ~
  124. " -f, --force \tForce download all artworks (overwrite).\n" ~
  125. " -s, --sfw-only \tOnly download SFW content (not R-18).\n" ~
  126. " -h, --help \tDisplay this help message and exit.\n" ~
  127. "\nExamples:\n" ~
  128. "\n Download all content from the current time until 2023-06-19:\n" ~
  129. " pixiv_down daily --end 2023-06-19\n" ~
  130. "\n Download all content from the 20th June, 2023:\n" ~
  131. " pixiv_down daily --begin 2021-06-20 --end 2021-06-20\n" ~
  132. "\nI wouldn't recommend downloading all content from a specific day\n" ~
  133. "which is over a month or two ago, since this command isn't really\n" ~
  134. "designed for that. It'll require a lot of requests to the pixiv\n" ~
  135. "server and will take a long while. You'd be better using either\n" ~
  136. "the 'artist' or 'following' command.");
  137. }
  138. private:
  139. struct DailyOptions
  140. {
  141. const SysTime begin;
  142. const SysTime end;
  143. const bool sfwOnly;
  144. const bool force;
  145. }
  146. void downloadDaily(in DailyOptions options, in Config config)
  147. {
  148. trace("downloadDaily -- begin");
  149. downloadLatestArtworks(options, config);
  150. trace("downloadDaily ---- finished artworks");
  151. sleep(5, 10);
  152. downloadLatestNovels(options, config);
  153. trace("downloadDaily ---- finished novels");
  154. trace("downloadDaily -- end");
  155. }
  156. void downloadLatestArtworks(in DailyOptions options, in Config config)
  157. {
  158. import std.algorithm.iteration : map;
  159. import app.util : sleep;
  160. bool pastEndDate = false;
  161. int page = 1;
  162. do {
  163. page > 1 && sleep(10, 15);
  164. const ids = fetchFollowLatest(page, config);
  165. foreach(id; ids) {
  166. const artwork = fetchArtworkInfo(id, config);
  167. const createDate = SysTime.fromISOExtString(artwork.createDate);
  168. // Only download artworks uploaded on or after `begin`.
  169. if (createDate > options.begin) {
  170. continue;
  171. }
  172. // Only download posts uploaded on or before `end`.
  173. if (createDate < options.end) {
  174. pastEndDate = true;
  175. info("daily downloads has past `endDate`");
  176. break;
  177. }
  178. if (options.sfwOnly && artwork.isR18) {
  179. continue;
  180. }
  181. downloadArtwork(artwork, config, options.force);
  182. sleep(1, 5);
  183. Term.goUpAndClearLine(1, Yes.useStderr);
  184. }
  185. if (pastEndDate == false) {
  186. tracef("finished downloading page %d", page);
  187. page += 1;
  188. tracef("preparing to download page %d", page);
  189. }
  190. } while (pastEndDate == false);
  191. }
  192. void downloadLatestNovels(in DailyOptions options, in Config config)
  193. {
  194. import std.stdio : stdout;
  195. int page = 1;
  196. bool pastEndDate = false;
  197. do {
  198. page > 1 && sleep(10, 15);
  199. const ids = fetchNovelLatest(page, config);
  200. foreach(id; ids) {
  201. const novel = fetchNovelInfo(id, config);
  202. const createDate = SysTime.fromISOExtString(novel.createDate);
  203. if (createDate > options.begin) {
  204. continue;
  205. }
  206. if (createDate < options.end) {
  207. pastEndDate = true;
  208. info("daily downloads has past `endDate`");
  209. break;
  210. }
  211. if (options.sfwOnly && novel.isR18) {
  212. continue;
  213. }
  214. downloadNovel(novel, config, options.force);
  215. }
  216. if (!pastEndDate) {
  217. tracef("finished downloading page %d", page);
  218. page += 1;
  219. tracef("preparing to download page %d", page);
  220. }
  221. } while (!pastEndDate);
  222. }
  223. Nullable!SysTime fetchLastStartTime() nothrow
  224. {
  225. import mlib.directories: getProjectDirectories;
  226. import std.datetime.date: Date;
  227. import std.path: buildPath;
  228. import std.stdio: File;
  229. Nullable!SysTime sysTime;
  230. auto dirs = getProjectDirectories(null, "YumeNeru Software", "pixiv_down");
  231. try {
  232. auto file = File(buildPath(dirs.stateDir, "last_daily_run"), "r");
  233. auto date = Date.fromISOExtString(file.readln());
  234. sysTime = SysTime(date);
  235. } catch (Exception) {
  236. return sysTime;
  237. }
  238. return sysTime;
  239. }
  240. void saveLastStartTime(const ref SysTime startDate)
  241. {
  242. import mlib.directories: getProjectDirectories;
  243. import std.datetime.date: Date;
  244. import std.path: buildPath;
  245. import std.stdio: File;
  246. auto dirs = getProjectDirectories(null, "YumeNeru Software", "pixiv_down");
  247. auto file = File(buildPath(dirs.stateDir, "last_daily_run"), "w+");
  248. file.writeln((cast(Date)startDate).toISOExtString());
  249. }