Auxiliary tools for users of pixiv_down
Revision | 3ad89fdb643116df3c85c9acf28f884287f504ee (tree) |
---|---|
Time | 2023-01-29 12:34:57 |
Author | nemophila <stigma@disr...> |
Commiter | nemophila |
Add pixiv_combine_users program
@@ -7,6 +7,7 @@ | ||
7 | 7 | |
8 | 8 | # Executables |
9 | 9 | pixiv_remove_unfollowed |
10 | +pixiv_combine_users | |
10 | 11 | |
11 | 12 | # Logs |
12 | 13 | .log |
@@ -13,3 +13,9 @@ | ||
13 | 13 | targetType "executable" |
14 | 14 | targetName "pixiv_remove_unfollowed" |
15 | 15 | } |
16 | + | |
17 | +configuration "combine_users" { | |
18 | + sourceFiles "pixiv_combine_users.d" | |
19 | + targetType "executable" | |
20 | + targetName "pixiv_combine_users" | |
21 | +} |
@@ -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 | +} |