compact.d 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /*
  2. * pixiv_down - CLI-based downloading tool for https://www.pixiv.net.
  3. * Copyright (C) 2023, 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.compact;
  18. import std.experimental.logger;
  19. import pd.configuration: Config;
  20. import pd.pixiv;
  21. public void displayCompactHelp()
  22. {
  23. import std.stdio : stderr;
  24. stderr.writefln(
  25. "pixiv_down compact - Compact an account's directories in to one\n" ~
  26. "\nUsage:\tpixiv_down compact [options]\n" ~
  27. "\nThis command will determine all pixiv account's that have\n" ~
  28. "multiple directories, and compact them in to one. By default\n" ~
  29. "the current account display name is used. You can use the\n" ~
  30. "\"--interactive\" option to select a name to use.\n" ~
  31. "\nOptions:\n" ~
  32. " -h, --help \tDisplay this help message and exit.\n" ~
  33. " -i, --interactive\tSelect the name for each account.\n" ~
  34. " -n, --dry-run \tPrint the directories that would be moved,\n" ~
  35. " \tbut do not actually move them.\n" ~
  36. "\nNOTE: Multiple directories can occur as people change their display\n" ~
  37. "name on pixiv.");
  38. }
  39. public int compactHandle(string[] args, in Config config)
  40. {
  41. import std.getopt : getopt, GetOptException, GetOptOption = config;
  42. import std.stdio : stderr;
  43. Options options;
  44. try {
  45. auto helpInformation = getopt(args,
  46. GetOptOption.bundling,
  47. "interactive|i", &options.interactive,
  48. "dry-run|n", &options.dryRun);
  49. if (helpInformation.helpWanted) {
  50. displayCompactHelp();
  51. return 0;
  52. }
  53. } catch (GetOptException e) {
  54. stderr.writefln("pixiv_down compact: %s", e.msg);
  55. stderr.writefln("Run 'pixiv_down help compact' for more information");
  56. return 1;
  57. }
  58. compactAccounts(config, options);
  59. return 0;
  60. }
  61. private:
  62. import std.typecons : Tuple;
  63. alias PairType = Tuple!(string, "key", string[], "value");
  64. struct Options
  65. {
  66. bool interactive = false;
  67. bool dryRun = false;
  68. }
  69. void compactAccounts(in Config config, const ref Options options)
  70. {
  71. import core.thread : Thread;
  72. import core.time : seconds;
  73. import std.array : byPair;
  74. import std.stdio : stdout, stderr;
  75. import std.random : Random, uniform, unpredictableSeed;
  76. import app.util : makeSafe;
  77. string[][string] duplicatedAccounts = findDuplicateDirectories(config.outputDirectory);
  78. foreach(PairType pair; duplicatedAccounts.byPair) {
  79. string newName = makeSafe(pair[1][0]);
  80. try {
  81. newName = checkAndGetChoice(pair, config, options);
  82. } catch (PixivJSONException pje) {
  83. errorf("checkAndRemove for ID %s: %s", pair[0], pje.msg);
  84. if (options.interactive) {
  85. stderr.writefln("WARNING: Account ID %s is not valid", pair[0]);
  86. stderr.writefln(" %s", pje.msg);
  87. stderr.writefln("All names for ID %s", pair[0]);
  88. newName = getChoice(newName, pair[1]);
  89. } else {
  90. stderr.writefln("ERROR: Failed to compact account for ID %s", pair[0]);
  91. stderr.writefln(" %s", pje.msg);
  92. newName = null;
  93. }
  94. }
  95. if (newName !is null) {
  96. compact(newName, pair, config, options);
  97. }
  98. scope rnd = Random(unpredictableSeed);
  99. auto sleepDuration = uniform(3, 10, rnd);
  100. stderr.writefln("Sleeping for %d seconds...", sleepDuration);
  101. Thread.sleep(sleepDuration.seconds);
  102. }
  103. if (options.dryRun)
  104. {
  105. import std.stdio : writeln;
  106. writeln(" No files or directories were moved.");
  107. }
  108. }
  109. auto findDuplicateDirectories(string outputDirectory)
  110. {
  111. import std.algorithm.iteration : each, filter;
  112. import std.algorithm.searching : countUntil;
  113. import std.array : byPair;
  114. import std.file : SpanMode, dirEntries;
  115. import std.path : baseName;
  116. // ["uid": ["name1", "name2", ...], "uid2": ...]
  117. string[][string] uids;
  118. dirEntries(outputDirectory, SpanMode.shallow).each!((dir) {
  119. const bname = baseName(dir);
  120. const splitIndex = countUntil!"a < '0' || a > '9'"(bname);
  121. if (splitIndex > 0) {
  122. const uid = bname[0..splitIndex];
  123. uids[uid] ~= bname[splitIndex+1..$];
  124. }
  125. });
  126. // D 2.076 compat: filter doesn't seem to like this predicate.
  127. // return uids.byPair.filter!(pair => pair.value.length > 1)
  128. foreach(uid, names; uids) {
  129. if (names.length <= 1) {
  130. uids.remove(uid);
  131. }
  132. }
  133. return uids;
  134. }
  135. string checkAndGetChoice(PairType pair, in Config config, const ref Options options)
  136. {
  137. import std.stdio : writefln;
  138. import app.util : makeSafe;
  139. // Default to the current name
  140. const user = fetchUser(pair[0], config);
  141. string newName = makeSafe(user.userName);
  142. if (options.interactive) {
  143. writefln("All names for ID %s", user.id);
  144. newName = getChoice(newName, pair[1]);
  145. }
  146. infof("newName = %s", newName);
  147. return newName;
  148. }
  149. void compact(string newName, PairType pair, in Config config, const ref Options options)
  150. {
  151. import std.algorithm.iteration : each;
  152. import std.file : SpanMode, dirEntries, exists, mkdirRecurse;
  153. import std.path : buildPath;
  154. import std.stdio : stdout;
  155. const newDirName = buildPath(config.outputDirectory, pair[0] ~ "_" ~ newName);
  156. if (options.dryRun)
  157. {
  158. if (false == exists(newDirName))
  159. {
  160. stdout.writefln("Would create directory: %s", newDirName);
  161. }
  162. }
  163. else
  164. {
  165. mkdirRecurse(newDirName);
  166. }
  167. // copy old files to new directory
  168. foreach(oldDirUname; pair[1]) {
  169. const oldDirName = buildPath(config.outputDirectory, pair[0] ~ "_" ~ oldDirUname);
  170. if (oldDirName == newDirName) {
  171. continue;
  172. }
  173. dirEntries(oldDirName, SpanMode.shallow).each!((ent) {
  174. if (options.dryRun) {
  175. fakeMove(ent.name, newDirName);
  176. } else {
  177. move(ent.name, newDirName);
  178. }
  179. });
  180. if (false == options.dryRun) {
  181. import mlib.trash : trash;
  182. trash(oldDirName);
  183. infof("trashed %s", oldDirName);
  184. stdout.writefln("Moved all files and directories:\n\tFrom: %s\n\tTo: %s", oldDirName,
  185. newDirName);
  186. } else {
  187. import std.range : repeat;
  188. import mlib.term;
  189. stdout.writefln(" %s", '-'.repeat(Term.getColumnCount() - 4));
  190. }
  191. }
  192. }
  193. /// Prompt to choose which directory name to use.
  194. ///
  195. /// Params:
  196. /// safeWord = should be the result of makeSafe on the current
  197. /// pixiv account name.
  198. /// validNames = the array of existing names used for directories.
  199. ///
  200. /// Returns: The chosen name.
  201. string getChoice(string safeWord, string[] validNames)
  202. {
  203. import std.algorithm.searching : countUntil;
  204. import std.conv : to;
  205. import std.stdio : readln, writef;
  206. import std.string : strip;
  207. bool appendedSafeWord = false;
  208. // Default choice should be 'safeWord'
  209. const indexOfSafeWord = countUntil(validNames, safeWord);
  210. long choice = (indexOfSafeWord == -1) ? validNames.length + 1 : indexOfSafeWord;
  211. foreach(i, name; validNames) {
  212. writef("%2d: %s\n", i + 1, name);
  213. }
  214. if (indexOfSafeWord == -1) {
  215. writef("%2d: %s\n", validNames.length +1, safeWord);
  216. appendedSafeWord = true;
  217. }
  218. lPromptForChoice:
  219. writef("Enter the number to use: ");
  220. const answer = readln.strip;
  221. if (answer == "") {
  222. return safeWord;
  223. }
  224. try {
  225. choice = to!long(answer);
  226. } catch (Exception) {
  227. goto lPromptForChoice;
  228. }
  229. const maxChoice = appendedSafeWord ? validNames.length + 1 : validNames.length;
  230. if (choice <= 0 || choice > maxChoice) {
  231. goto lPromptForChoice;
  232. }
  233. if (appendedSafeWord && choice == maxChoice) {
  234. return safeWord;
  235. }
  236. return validNames[choice - 1];
  237. }
  238. ///
  239. /// move *from* (absolute path) to *to* (absolute path).
  240. ///
  241. /// *to* should not contain the new filename/dirname, but
  242. /// be the absolute path to the new parent directory.
  243. void move(string from, string to)
  244. {
  245. import std.file : exists, rename;
  246. import std.path : baseName, buildPath;
  247. const newFilename = buildPath(to, baseName(from));
  248. if (false == exists(newFilename))
  249. {
  250. // TODO: Access to logging functions
  251. rename(from, newFilename);
  252. }
  253. }
  254. void fakeMove(string from, string to)
  255. {
  256. import std.path : baseName, buildPath;
  257. import std.stdio : writefln;
  258. const newFilename = buildPath(to, baseName(from));
  259. writefln(" FROM: %s", from);
  260. writefln(" TO: %s", newFilename);
  261. }