123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- /*
- * pixiv_down - CLI-based downloading tool for https://www.pixiv.net.
- * Copyright (C) 2023, 2024 Mio
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
- module app.cmds.compact;
- import std.experimental.logger;
- import pd.configuration: Config;
- import pd.pixiv;
- public void displayCompactHelp()
- {
- import std.stdio : stderr;
- stderr.writefln(
- "pixiv_down compact - Compact an account's directories in to one\n" ~
- "\nUsage:\tpixiv_down compact [options]\n" ~
- "\nThis command will determine all pixiv account's that have\n" ~
- "multiple directories, and compact them in to one. By default\n" ~
- "the current account display name is used. You can use the\n" ~
- "\"--interactive\" option to select a name to use.\n" ~
- "\nOptions:\n" ~
- " -h, --help \tDisplay this help message and exit.\n" ~
- " -i, --interactive\tSelect the name for each account.\n" ~
- " -n, --dry-run \tPrint the directories that would be moved,\n" ~
- " \tbut do not actually move them.\n" ~
- "\nNOTE: Multiple directories can occur as people change their display\n" ~
- "name on pixiv.");
- }
- public int compactHandle(string[] args, in Config config)
- {
- import std.getopt : getopt, GetOptException, GetOptOption = config;
- import std.stdio : stderr;
- Options options;
- try {
- auto helpInformation = getopt(args,
- GetOptOption.bundling,
- "interactive|i", &options.interactive,
- "dry-run|n", &options.dryRun);
- if (helpInformation.helpWanted) {
- displayCompactHelp();
- return 0;
- }
- } catch (GetOptException e) {
- stderr.writefln("pixiv_down compact: %s", e.msg);
- stderr.writefln("Run 'pixiv_down help compact' for more information");
- return 1;
- }
- compactAccounts(config, options);
- return 0;
- }
- private:
- import std.typecons : Tuple;
- alias PairType = Tuple!(string, "key", string[], "value");
- struct Options
- {
- bool interactive = false;
- bool dryRun = false;
- }
- void compactAccounts(in Config config, const ref Options options)
- {
- import core.thread : Thread;
- import core.time : seconds;
- import std.array : byPair;
- import std.stdio : stdout, stderr;
- import std.random : Random, uniform, unpredictableSeed;
- import app.util : makeSafe;
- string[][string] duplicatedAccounts = findDuplicateDirectories(config.outputDirectory);
- foreach(PairType pair; duplicatedAccounts.byPair) {
- string newName = makeSafe(pair[1][0]);
- try {
- newName = checkAndGetChoice(pair, config, options);
- } catch (PixivJSONException pje) {
- errorf("checkAndRemove for ID %s: %s", pair[0], pje.msg);
- if (options.interactive) {
- stderr.writefln("WARNING: Account ID %s is not valid", pair[0]);
- stderr.writefln(" %s", pje.msg);
- stderr.writefln("All names for ID %s", pair[0]);
- newName = getChoice(newName, pair[1]);
- } else {
- stderr.writefln("ERROR: Failed to compact account for ID %s", pair[0]);
- stderr.writefln(" %s", pje.msg);
- newName = null;
- }
- }
- if (newName !is null) {
- compact(newName, pair, config, options);
- }
- scope rnd = Random(unpredictableSeed);
- auto sleepDuration = uniform(3, 10, rnd);
- stderr.writefln("Sleeping for %d seconds...", sleepDuration);
- Thread.sleep(sleepDuration.seconds);
- }
- if (options.dryRun)
- {
- import std.stdio : writeln;
- writeln(" No files or directories were moved.");
- }
- }
- auto findDuplicateDirectories(string outputDirectory)
- {
- import std.algorithm.iteration : each, filter;
- import std.algorithm.searching : countUntil;
- import std.array : byPair;
- import std.file : SpanMode, dirEntries;
- import std.path : baseName;
- // ["uid": ["name1", "name2", ...], "uid2": ...]
- string[][string] uids;
- dirEntries(outputDirectory, SpanMode.shallow).each!((dir) {
- const bname = baseName(dir);
- const splitIndex = countUntil!"a < '0' || a > '9'"(bname);
- if (splitIndex > 0) {
- const uid = bname[0..splitIndex];
- uids[uid] ~= bname[splitIndex+1..$];
- }
- });
- // D 2.076 compat: filter doesn't seem to like this predicate.
- // return uids.byPair.filter!(pair => pair.value.length > 1)
- foreach(uid, names; uids) {
- if (names.length <= 1) {
- uids.remove(uid);
- }
- }
- return uids;
- }
- string checkAndGetChoice(PairType pair, in Config config, const ref Options options)
- {
- import std.stdio : writefln;
- import app.util : makeSafe;
- // Default to the current name
- const user = fetchUser(pair[0], config);
- string newName = makeSafe(user.userName);
- if (options.interactive) {
- writefln("All names for ID %s", user.id);
- newName = getChoice(newName, pair[1]);
- }
- infof("newName = %s", newName);
- return newName;
- }
- void compact(string newName, PairType pair, in Config config, const ref Options options)
- {
- import std.algorithm.iteration : each;
- import std.file : SpanMode, dirEntries, exists, mkdirRecurse;
- import std.path : buildPath;
- import std.stdio : stdout;
- const newDirName = buildPath(config.outputDirectory, pair[0] ~ "_" ~ newName);
- if (options.dryRun)
- {
- if (false == exists(newDirName))
- {
- stdout.writefln("Would create directory: %s", newDirName);
- }
- }
- else
- {
- mkdirRecurse(newDirName);
- }
- // copy old files to new directory
- foreach(oldDirUname; pair[1]) {
- const oldDirName = buildPath(config.outputDirectory, pair[0] ~ "_" ~ oldDirUname);
- if (oldDirName == newDirName) {
- continue;
- }
- dirEntries(oldDirName, SpanMode.shallow).each!((ent) {
- if (options.dryRun) {
- fakeMove(ent.name, newDirName);
- } else {
- move(ent.name, newDirName);
- }
- });
- if (false == options.dryRun) {
- import mlib.trash : trash;
- trash(oldDirName);
- infof("trashed %s", oldDirName);
- stdout.writefln("Moved all files and directories:\n\tFrom: %s\n\tTo: %s", oldDirName,
- newDirName);
- } else {
- import std.range : repeat;
- import mlib.term;
- stdout.writefln(" %s", '-'.repeat(Term.getColumnCount() - 4));
- }
- }
- }
- /// Prompt to choose which directory name to use.
- ///
- /// Params:
- /// safeWord = should be the result of makeSafe on the current
- /// pixiv account name.
- /// validNames = the array of existing names used for directories.
- ///
- /// Returns: The chosen name.
- string getChoice(string safeWord, string[] validNames)
- {
- import std.algorithm.searching : countUntil;
- import std.conv : to;
- import std.stdio : readln, writef;
- import std.string : strip;
- bool appendedSafeWord = false;
- // Default choice should be 'safeWord'
- const indexOfSafeWord = countUntil(validNames, safeWord);
- long choice = (indexOfSafeWord == -1) ? validNames.length + 1 : indexOfSafeWord;
- foreach(i, name; validNames) {
- writef("%2d: %s\n", i + 1, name);
- }
- if (indexOfSafeWord == -1) {
- writef("%2d: %s\n", validNames.length +1, safeWord);
- appendedSafeWord = true;
- }
- lPromptForChoice:
- writef("Enter the number to use: ");
- const answer = readln.strip;
- if (answer == "") {
- return safeWord;
- }
- try {
- choice = to!long(answer);
- } catch (Exception) {
- goto lPromptForChoice;
- }
- const maxChoice = appendedSafeWord ? validNames.length + 1 : validNames.length;
- if (choice <= 0 || choice > maxChoice) {
- goto lPromptForChoice;
- }
- if (appendedSafeWord && choice == maxChoice) {
- return safeWord;
- }
- return validNames[choice - 1];
- }
- ///
- /// move *from* (absolute path) to *to* (absolute path).
- ///
- /// *to* should not contain the new filename/dirname, but
- /// be the absolute path to the new parent directory.
- void move(string from, string to)
- {
- import std.file : exists, rename;
- import std.path : baseName, buildPath;
- const newFilename = buildPath(to, baseName(from));
- if (false == exists(newFilename))
- {
- // TODO: Access to logging functions
- rename(from, newFilename);
- }
- }
- void fakeMove(string from, string to)
- {
- import std.path : baseName, buildPath;
- import std.stdio : writefln;
- const newFilename = buildPath(to, baseName(from));
- writefln(" FROM: %s", from);
- writefln(" TO: %s", newFilename);
- }
|