• R/O
  • SSH

Commit

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

Auxiliary tools for users of pixiv_down


Commit MetaInfo

Revision3ad89fdb643116df3c85c9acf28f884287f504ee (tree)
Time2023-01-29 12:34:57
Authornemophila <stigma@disr...>
Commiternemophila

Log Message

Add pixiv_combine_users program

Change Summary

Incremental Difference

diff -r 42f2bc4f33b5 -r 3ad89fdb6431 .hgignore
--- a/.hgignore Sun Jan 29 13:27:49 2023 +1000
+++ b/.hgignore Sun Jan 29 13:34:57 2023 +1000
@@ -7,6 +7,7 @@
77
88 # Executables
99 pixiv_remove_unfollowed
10+pixiv_combine_users
1011
1112 # Logs
1213 .log
diff -r 42f2bc4f33b5 -r 3ad89fdb6431 dub.sdl
--- a/dub.sdl Sun Jan 29 13:27:49 2023 +1000
+++ b/dub.sdl Sun Jan 29 13:34:57 2023 +1000
@@ -13,3 +13,9 @@
1313 targetType "executable"
1414 targetName "pixiv_remove_unfollowed"
1515 }
16+
17+configuration "combine_users" {
18+ sourceFiles "pixiv_combine_users.d"
19+ targetType "executable"
20+ targetName "pixiv_combine_users"
21+}
diff -r 42f2bc4f33b5 -r 3ad89fdb6431 pixiv_combine_users.d
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pixiv_combine_users.d Sun Jan 29 13:34:57 2023 +1000
@@ -0,0 +1,218 @@
1+/**
2+ * A program which combines all the directories for each pixiv user ID.
3+ *
4+ * When pixiv_down downloads an item, it checks if a directory exists in the
5+ * form <userID>_<displayName>. The display name is used to make navigation
6+ * familiar -- so you know who you're looking for. However, this value can
7+ * change. As such, it's possible to end up with duplicates of the same
8+ * content.
9+ *
10+ * This program will let you know of all the options available (i.e. each
11+ * <displayName> that has been used) along with the current <displayName>.
12+ * It'll then move each file over in to the chosen directory (keeping
13+ * modification and create dates). It will not (by default) overwrite any
14+ * existing files.
15+ */
16+module pixiv_combine_users;
17+
18+import std.file;
19+import std.path : buildPath;
20+import std.stdio;
21+
22+import std.experimental.logger;
23+
24+import mlib.configparser;
25+import mlib.directories;
26+import mlib.trash;
27+
28+import pixivd;
29+import pixivd.types;
30+
31+struct Config
32+{
33+ string sessid;
34+ string baseFolder;
35+}
36+
37+FileLogger logger;
38+
39+int main(string[] args)
40+{
41+ Config config;
42+ logger = new FileLogger("logs/pixiv_combine_users.log");
43+
44+ try {
45+ loadConfig(config);
46+ } catch (FileException fe) {
47+ logger.critical(fe.msg);
48+ stderr.writeln(fe.msg);
49+ stderr.writeln("Have you used both pixivd and pixiv_down before?");
50+ return 1;
51+ } catch (NoSectionException nse) {
52+ stderr.writeln("Couldn't find 'output' section in the pixiv_down config.");
53+ stderr.writeln("Please see pixiv_down documentation on setting this.");
54+ return 1;
55+ } catch (NoOptionException noe) {
56+ stderr.writeln("Couldn't find the 'base_folder' option in the pixiv_down config.");
57+ stderr.writeln("Please see pixiv_down documentation on setting this.");
58+ return 1;
59+ }
60+
61+ string[][string] dupes = findDupes(config);
62+ removeDupes(dupes, config);
63+ return 0;
64+}
65+
66+/**
67+ * Initialize a Config structure.
68+ *
69+ * This attempts to read both the pixivd PHPSESSID and pixiv_down's settings.
70+ * pixivd is used to check the current user's <displayName>, while pixiv_down's
71+ * settings are used to see where the pictures are stored.
72+ */
73+void loadConfig(ref Config config)
74+{
75+ import std.string : strip;
76+
77+ DirEntry configDir = open(Directory.config);
78+ string pdPath = buildPath(configDir.name, "pixivd", "phpsessid.txt");
79+ string pdownPath = buildPath(configDir.name, "pixiv_down", "settings.conf");
80+
81+ if (false == exists(pdPath)) {
82+ throw new FileException(pdPath, "file does not exist");
83+ }
84+ if (false == exists(pdownPath)) {
85+ throw new FileException(pdownPath, "file does not exist");
86+ }
87+
88+ config.sessid = File(pdPath, "r").readln().strip();
89+
90+ scope ConfigParser parser = new ConfigParser();
91+ parser.read(pdownPath);
92+ config.baseFolder = parser.get("output", "base_folder");
93+}
94+
95+/**
96+ * Find all directories where the pixiv ID is duplicated.
97+ *
98+ * The returned associative array is in the form:
99+ * "id": [
100+ * "displayName1",
101+ * "displayName2",
102+ * ....
103+ * "displayNameN"
104+ * ]
105+ *
106+ * Each returned ID will have more than one <displayName>.
107+ */
108+string[][string] findDupes(in ref Config config)
109+{
110+ import std.array : join, split;
111+ import std.path : baseName;
112+
113+ const cwd = getcwd();
114+ scope(exit) chdir(cwd);
115+
116+ chdir(config.baseFolder);
117+
118+ string[][string] ids;
119+
120+ foreach(string name; dirEntries(config.baseFolder, SpanMode.shallow)) {
121+ string bname = baseName(name);
122+ string[] spl = bname.split("_");
123+ string id = spl[0];
124+
125+ /* Don't include any non-numerical "ids" (other directories) */
126+ if (id[0] < '0' && id[0] > '9')
127+ continue;
128+
129+ ids[id] ~= spl[1..$].join("_");
130+ }
131+
132+ foreach(id, names; ids) {
133+ if (names.length == 1) {
134+ ids.remove(id);
135+ }
136+ }
137+
138+ return ids;
139+}
140+
141+void prettyPrint(string[] arr)
142+{
143+ foreach(num, elem; arr) {
144+ writefln(" %d) %s", num, elem);
145+ }
146+}
147+
148+void removeDupes(string[][string] dupes, in ref Config config)
149+{
150+ import core.thread : Thread;
151+ import core.time : dur;
152+ import std.string : empty;
153+
154+ Client client = new Client(config.sessid);
155+ foreach(id, names; dupes) {
156+ try {
157+ FullUser user = client.fetchUser(id);
158+ writefln("All names for ID %s (current: %s):", id, user.userName);
159+ prettyPrint(names);
160+ write("Which number should be used? ");
161+ int choice;
162+ readf!" %d"(choice);
163+ removeDupe(id, names, choice, config);
164+ } catch (PixivJSONException pje) {
165+ logger.criticalf("Fetching ID '%s': %s", id, pje.message);
166+ stderr.writeln(pje.msg);
167+ continue;
168+ }
169+
170+ writeln("Sleeping for 3 seconds");
171+ Thread.sleep(dur!"seconds"(3));
172+ }
173+ writeln("Done removing duplicates! (if any!)");
174+}
175+
176+void removeDupe(string id, string[] names, int choice, in ref Config config)
177+{
178+ import std.algorithm.mutation : remove;
179+ import std.array : array, split;
180+ import std.path : baseName, dirSeparator;
181+
182+ const newPath = buildPath(config.baseFolder, id ~ "_" ~ names[choice]);
183+ writefln("Moving all files for %s to %s", names[choice], newPath);
184+ names = names.remove(choice);
185+
186+ /* eww */
187+ foreach(name; names) {
188+ const baseDir = buildPath(config.baseFolder, id ~ "_" ~ name);
189+ foreach(entry; dirEntries(baseDir, SpanMode.breadth)) {
190+
191+ // We need to account for Manga and multi-page illustrations.
192+ string maybeIllustId = entry.name.split(dirSeparator)[$-2];
193+ string destPath = newPath;
194+ if (maybeIllustId != (id ~ "_" ~ name)) {
195+ // Is Manga or multi-paged illustration.
196+ destPath = buildPath(destPath, maybeIllustId);
197+ logger.tracef("Found Manga or multi-paged illust--output: %s", destPath);
198+ }
199+
200+ if (false == exists(buildPath(destPath, baseName(entry.name)))) {
201+ writefln("%s does not exist in %s... Copying.", entry.name, destPath);
202+ string fname = baseName(entry.name);
203+ destPath = buildPath(destPath, fname);
204+ // FIXME: Why not just rename() the directory?
205+ if (isDir(entry.name)) {
206+ logger.infof("Making directory: %s", destPath);
207+ mkdir(destPath);
208+ continue;
209+ }
210+ logger.infof("Copying %s to %s", entry.name, destPath);
211+ copy(entry.name, destPath);
212+ }
213+ }
214+ writeln("Moved everything from ", baseDir, "... Deleting.");
215+ logger.infof("Trashing '%s'", baseDir);
216+ trash(baseDir);
217+ }
218+}