• 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

Revision019d96c167ce1ca2e26231d1818442eb269ae86e (tree)
Time2023-01-06 19:48:18
Authornemophila <stigma@disr...>
Commiternemophila

Log Message

initial commit

Change Summary

Incremental Difference

diff -r 000000000000 -r 019d96c167ce .hgignore
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,11 @@
1+# dub
2+.dub/
3+
4+# mlib
5+.a
6+
7+# Executables
8+pixiv_remove_unfollowed
9+
10+# Logs
11+.log
diff -r 000000000000 -r 019d96c167ce LICENSE
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,26 @@
1+Copyright 2023 mio <stigma@disroot.org>
2+
3+Redistribution and use in source and binary forms, with or without
4+modification, are permitted provided that the following conditions
5+are met:
6+
7+ 1. Redistributions of source code must retain the above copyright
8+ notice, this list of conditions and the following disclaimer.
9+
10+ 2. Redistributions in binary form must reproduce the above
11+ copyright notice, this list of conditions and the following
12+ disclaimer in the documentation and/or other materials
13+ provided with the distribution.
14+
15+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
18+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
19+COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
20+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
25+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+POSSIBILITY OF SUCH DAMAGE.
diff -r 000000000000 -r 019d96c167ce README
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/README Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,24 @@
1+- pixivd_tools -
2+
3+Auxiliary tools for [pixiv_down].
4+
5+[pixiv_down]: https://yume-neru.neocities.org/p/pixiv_down.html
6+
7+
8+= Requirements =
9+
10+* A D compiler
11+* dub
12+* [pixivd]
13+
14+You'll need pixivd downloaded. By defaut these tools will look for the
15+directory at "../pixivd" (i.e. they'll look for "pixivd" in the parent
16+directory). You can change this in the "dub.sdl" file.
17+
18+[pixivd]: https://yume-neru.neocities.org/p/pixivd.html
19+
20+
21+= License =
22+
23+Each tool is licensed under the BSD 2-Clause License, which
24+you can read in the LICENSE file.
diff -r 000000000000 -r 019d96c167ce dub.sdl
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dub.sdl Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,14 @@
1+name "pixivd-tools"
2+description "Collection of extra utility programs utilising pixivd"
3+license "BSD 2-clause"
4+
5+# Change the path here for pixivd if required.
6+dependency "pixivd" path="../pixivd"
7+
8+dependency "mlib" path="mlib"
9+
10+configuration "remove_unfollowed" {
11+ sourceFiles "pixiv_remove_unfollowed.d"
12+ targetType "executable"
13+ targetName "pixiv_remove_unfollowed"
14+}
diff -r 000000000000 -r 019d96c167ce dub.selections.json
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dub.selections.json Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,6 @@
1+{
2+ "fileVersion": 1,
3+ "versions": {
4+ "mlib": {"path":"mlib"}
5+ }
6+}
diff -r 000000000000 -r 019d96c167ce mlib/configparser.d
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mlib/configparser.d Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,644 @@
1+/*
2+ * Copyright (C) 2021, 2022 mio <stigma@disroot.org>
3+ *
4+ * Permission to use, copy, modify, and/or distribute this software for any
5+ * purpose with or without fee is herby granted.
6+ *
7+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
10+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
12+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
13+ * CONNECTION WITH THE USE OR PEFORMANCE OF THIS SOFTWARE.
14+ */
15+
16+
17+/**
18+ * An incomplete single-file INI parser for D.
19+ *
20+ * The API should be similar to python's configparse module. Internally it
21+ * uses the standard D associative array.
22+ *
23+ * Example:
24+ * ---
25+ * import configparser;
26+ *
27+ * auto config = new ConfigParser();
28+ * // no sections initially
29+ * assert(config.sections.length == 0);
30+ * // Section names are case-sensitive
31+ * conf.addSection("Default");
32+ * // option names (pythonpath) are case-insensitive (converted to lowercase)
33+ * conf.set("Default", "pythonpath", "/usr/bin/python3");
34+ * ---
35+ *
36+ * License: 0BSD
37+ * Version: 0.2
38+ * History:
39+ * 0.2 Add .getBool()
40+ * 0.1 Initial release
41+ */
42+module mlib.configparser;
43+
44+private
45+{
46+ import std.conv : ConvException;
47+ import std.stdio : File;
48+}
49+
50+public class DuplicateSectionException : Exception
51+{
52+ private string m_section;
53+
54+ this(string section)
55+ {
56+ string msg = "Section " ~ section ~ " already exists.";
57+ m_section = section;
58+ super(msg);
59+ }
60+
61+ string section()
62+ {
63+ return m_section;
64+ }
65+}
66+
67+public class NoSectionException : Exception
68+{
69+ private string m_section;
70+
71+ this(string section)
72+ {
73+ string msg = "Section '" ~ section ~ "' does not exist.";
74+ m_section = section;
75+ super(msg);
76+ }
77+
78+ string section()
79+ {
80+ return m_section;
81+ }
82+}
83+
84+public class NoOptionException : Exception
85+{
86+ private string m_section;
87+ private string m_option;
88+
89+ this(string section, string option)
90+ {
91+ string msg = "Section '" ~ section ~ "' does not have option '" ~
92+ option ~ "'.";
93+ m_section = section;
94+ m_option = option;
95+ super(msg);
96+ }
97+
98+ string section() { return m_section; }
99+ string option() { return m_option; }
100+}
101+
102+/**
103+ * The main configuration parser.
104+ */
105+public class ConfigParser
106+{
107+ private char[] m_delimiters;
108+ private char[] m_commentPrefixes;
109+
110+ /** current section for parsing */
111+ private string m_currentSection;
112+ private string[string][string] m_sections;
113+
114+ /**
115+ * Creates a new instance of ConfigParser.
116+ */
117+ this(char[] delimiters = ['=', ':'], char[] commentPrefixes = ['#', ';'])
118+ {
119+ m_delimiters = delimiters;
120+ m_commentPrefixes = commentPrefixes;
121+ }
122+
123+ /**
124+ * Return an array containing the available sections.
125+ */
126+ string[] sections()
127+ {
128+ return m_sections.keys();
129+ }
130+
131+ ///
132+ unittest
133+ {
134+ auto conf = new ConfigParser();
135+
136+ assert(0 == conf.sections().length);
137+
138+ conf.addSection("Section");
139+
140+ assert(1 == conf.sections().length);
141+ }
142+
143+ /**
144+ * Add a section named `section` to the instance.
145+ *
146+ * Throws:
147+ * - DuplicateSectionError if a section by the given name already
148+ * exists.
149+ */
150+ void addSection(string section)
151+ {
152+ if (section in m_sections)
153+ throw new DuplicateSectionException(section);
154+ m_sections[section] = null;
155+ }
156+
157+ ///
158+ unittest
159+ {
160+ import std.exception : assertNotThrown, assertThrown;
161+
162+ auto conf = new ConfigParser();
163+
164+ /* doesn't yet exist */
165+ assertNotThrown!DuplicateSectionException(conf.addSection("sample"));
166+ /* already exists */
167+ assertThrown!DuplicateSectionException(conf.addSection("sample"));
168+ }
169+
170+ /**
171+ * Indicates whether the named `section` is present in the configuration.
172+ *
173+ * Params:
174+ * section = The section to check for in the configuration.
175+ *
176+ * Returns: `true` if the section exists, `false` otherwise.
177+ */
178+ bool hasSection(string section)
179+ {
180+ auto exists = (section in m_sections);
181+ return (exists !is null);
182+ }
183+
184+ ///
185+ unittest
186+ {
187+ auto conf = new ConfigParser();
188+ conf.addSection("nExt");
189+ assert(true == conf.hasSection("nExt"), "Close the world.");
190+ assert(false == conf.hasSection("world"), "Open the nExt.");
191+ }
192+
193+ string[] options(string section)
194+ {
195+ if (false == this.hasSection(section))
196+ throw new NoSectionException(section);
197+ return m_sections[section].keys();
198+ }
199+
200+ ///
201+ unittest
202+ {
203+ import std.exception : assertNotThrown, assertThrown;
204+
205+ auto conf = new ConfigParser();
206+
207+ conf.addSection("Settings");
208+
209+ assertNotThrown!NoSectionException(conf.options("Settings"));
210+ assertThrown!NoSectionException(conf.options("void"));
211+
212+ string[] options = conf.options("Settings");
213+ assert(0 == options.length, "More keys than we need");
214+ }
215+
216+ bool hasOption(string section, string option)
217+ {
218+ if (false == this.hasSection(section))
219+ return false;
220+
221+ auto exists = (option in m_sections[section]);
222+ return (exists !is null);
223+ }
224+ /*
225+ string[] read(string[] filenames)
226+ {
227+ return null;
228+ }*/
229+
230+ void read(string filename)
231+ {
232+ import std.file : FileException, exists, isFile;
233+
234+ if (false == exists(filename)) {
235+ throw new FileException(filename);
236+ }
237+
238+ if (false == isFile(filename)) {
239+ throw new FileException(filename);
240+ }
241+
242+ File file = File(filename, "r");
243+ scope(exit) { file.close(); }
244+ read(file, false);
245+ }
246+
247+ ///
248+ unittest
249+ {
250+ import std.file : remove;
251+ import std.stdio : File;
252+
253+ auto configFile = File("test.conf", "w+");
254+ configFile.writeln("[Section 1]");
255+ configFile.writeln("key=value");
256+ configFile.writeln("\n[Section 2]");
257+ configFile.writeln("key2 = value");
258+ configFile.close();
259+
260+ auto conf = new ConfigParser();
261+ conf.read("test.conf");
262+
263+ assert(2 == conf.sections.length, "Incorrect Sections length");
264+ assert(true == conf.hasSection("Section 1"),
265+ "Config file doesn't have Section 1");
266+ assert(true == conf.hasOption("Section 1", "key"),
267+ "Config file doesn't have 'key' in 'Section 1'");
268+
269+ remove("test.conf");
270+ }
271+
272+ /**
273+ * Parse a config file.
274+ *
275+ * Params:
276+ * file = Reference to the file from which to read.
277+ * close = Close the file when finished parsing.
278+ */
279+ void read(ref File file, bool close = true)
280+ {
281+ import std.array : array;
282+ import std.algorithm.searching : canFind;
283+ import std.string : strip;
284+
285+ scope(exit) { if (close) file.close(); }
286+
287+ string[] lines = file.byLineCopy.array;
288+
289+ for (auto i = 0; i < lines.length; i++) {
290+ string line = lines[i].strip();
291+
292+ if (line == "")
293+ continue;
294+
295+ if ('[' == lines[i][0]) {
296+ parseSectionHeader(lines[i]);
297+ } else if (false == canFind(m_commentPrefixes, lines[i][0])) {
298+ parseLine(lines[i]);
299+ }
300+ /* ignore comments */
301+ }
302+ }
303+
304+ // void readString(string str)
305+ // {
306+ // }
307+
308+ /**
309+ * Get an `option` value for the named `section`.
310+ *
311+ * Params:
312+ * section = The section to look for the given `option`.
313+ * option = The option to return the value of
314+ * fallback = Fallback value if the `option` is not found. Can be null.
315+ *
316+ * Returns:
317+ * - The value for `option` if it is found.
318+ * - `null` if the `option` is not found and `fallback` is not provided.
319+ * - `fallback` if the `option` is not found and `fallback` is provided.
320+ *
321+ * Throws:
322+ * - NoSectionException if the `section` does not exist and no fallback is provided.
323+ * - NoOptionException if the `option` does not exist and no fallback is provided.
324+ */
325+ string get(string section, string option)
326+ {
327+ if (false == this.hasSection(section))
328+ throw new NoSectionException(section);
329+
330+ if (false == this.hasOption(section, option))
331+ throw new NoOptionException(section, option);
332+
333+ return m_sections[section][option];
334+ }
335+
336+ ///
337+ unittest
338+ {
339+ import std.exception : assertThrown;
340+
341+ auto conf = new ConfigParser();
342+ conf.addSection("Section");
343+ conf.set("Section", "option", "value");
344+
345+ assert(conf.get("Section", "option") == "value");
346+ assertThrown!NoSectionException(conf.get("section", "option"));
347+ assertThrown!NoOptionException(conf.get("Section", "void"));
348+ }
349+
350+ /// Ditto
351+ string get(string section, string option, string fallback)
352+ {
353+ string res = fallback;
354+
355+ try {
356+ res = get(section, option);
357+ } catch (NoSectionException e) {
358+ return res;
359+ } catch (NoOptionException e) {
360+ return res;
361+ }
362+
363+ return res;
364+ }
365+
366+ ///
367+ unittest
368+ {
369+ import std.exception : assertThrown;
370+
371+ auto conf = new ConfigParser();
372+ conf.addSection("Section");
373+ conf.set("Section", "option", "value");
374+
375+ assert("value" == conf.get("Section", "option"));
376+ assert("fallback" == conf.get("section", "option", "fallback"));
377+ assert("fallback" == conf.get("Section", "void", "fallback"));
378+
379+ /* can use null for fallback */
380+ assert(null == conf.get("section", "option", null));
381+ assert(null == conf.get("Section", "void", null));
382+ }
383+
384+ /**
385+ * A convenience method which casts the value of `option` in `section`
386+ * to an integer.
387+ *
388+ * Params:
389+ * section = The section to look for the given `option`.
390+ * option = The option to return the value for.
391+ * fallback = The fallback value to use if `option` isn't found.
392+ *
393+ * Returns:
394+ *
395+ *
396+ * Throws:
397+ * - NoSectionFoundException if `section` doesn't exist.
398+ * - NoOptionFoundException if the `section` doesn't contain `option`.
399+ * - ConvException if it failed to parse the value to an int.
400+ * - ConvOverflowException if the value would overflow an int.
401+ *
402+ * See_Also: get()
403+ */
404+ int getInt(string section, string option)
405+ {
406+ import std.conv : parse;
407+
408+ string res;
409+
410+ res = get(section, option);
411+
412+ return parse!int(res);
413+ }
414+
415+ /// Ditto
416+ int getInt(string section, string option, int fallback)
417+ {
418+ int res = fallback;
419+
420+ try {
421+ res = getInt(section, option);
422+ } catch (Exception e) {
423+ return res;
424+ }
425+
426+ return res;
427+ }
428+
429+ /*
430+ double getDouble(string section, string option)
431+ {
432+ }
433+
434+ double getDouble(string section, string option, double fallback)
435+ {
436+ }
437+
438+ float getFloat(string section, string option)
439+ {
440+ }
441+
442+ float getFloat(string section, string option, float fallback)
443+ {
444+ }*/
445+
446+ /**
447+ * A convenience method which coerces the $(I option) in the
448+ * specified $(I section) to a boolean value.
449+ *
450+ * Note that the accepted values for the option are "1", "yes",
451+ * "true", and "on", which cause this method to return `true`, and
452+ * "0", "no", "false", and "off", which cause it to return `false`.
453+ *
454+ * These string values are checked in a case-insensitive manner.
455+ *
456+ * Params:
457+ * section = The section to look for the given option.
458+ * option = The option to return the value for.
459+ * fallback = The fallback value to use if the option was not found.
460+ *
461+ * Throws:
462+ * - NoSectionFoundException if `section` doesn't exist.
463+ * - NoOptionFoundException if the `section` doesn't contain `option`.
464+ * - ConvException if any other value was found.
465+ */
466+ bool getBool(string section, string option)
467+ {
468+ import std.string : toLower;
469+
470+ string value = get(section, option);
471+
472+ switch (value.toLower)
473+ {
474+ case "1":
475+ case "yes":
476+ case "true":
477+ case "on":
478+ return true;
479+ case "0":
480+ case "no":
481+ case "false":
482+ case "off":
483+ return false;
484+ default:
485+ throw new ConvException("No valid boolean value found");
486+ }
487+ }
488+
489+ /// Ditto
490+ bool getBool(string section, string option, bool fallback)
491+ {
492+ try {
493+ return getBool(section, option);
494+ } catch (Exception e) {
495+ return fallback;
496+ }
497+ }
498+
499+ /*
500+ string[string] items(string section)
501+ {
502+ }*/
503+
504+ /**
505+ * Remove the specified `option` from the specified `section`.
506+ *
507+ * Params:
508+ * section = The section to remove from.
509+ * option = The option to remove from section.
510+ *
511+ * Retruns:
512+ * `true` if option existed, false otherwise.
513+ *
514+ * Throws:
515+ * - NoSectionException if the specified section doesn't exist.
516+ */
517+ bool removeOption(string section, string option)
518+ {
519+ if ((section in m_sections) is null) {
520+ throw new NoSectionException(section);
521+ }
522+
523+ if (option in m_sections[section]) {
524+ m_sections[section].remove(option);
525+ return true;
526+ }
527+
528+ return false;
529+ }
530+
531+ ///
532+ unittest
533+ {
534+ import std.exception : assertThrown;
535+
536+ auto conf = new ConfigParser();
537+ conf.addSection("Default");
538+ conf.set("Default", "exists", "true");
539+
540+ assertThrown!NoSectionException(conf.removeOption("void", "false"));
541+ assert(false == conf.removeOption("Default", "void"));
542+ assert(true == conf.removeOption("Default", "exists"));
543+ }
544+
545+ /**
546+ * Remove the specified `section` from the config.
547+ *
548+ * Params:
549+ * section = The section to remove.
550+ *
551+ * Returns:
552+ * `true` if the section existed, `false` otherwise.
553+ */
554+ bool removeSection(string section)
555+ {
556+ if (section in m_sections) {
557+ m_sections.remove(section);
558+ return true;
559+ }
560+ return false;
561+ }
562+
563+ ///
564+ unittest
565+ {
566+ auto conf = new ConfigParser();
567+ conf.addSection("Exists");
568+ assert(false == conf.removeSection("DoesNotExist"));
569+ assert(true == conf.removeSection("Exists"));
570+ }
571+
572+ void set(string section, string option, string value)
573+ {
574+ if (false == this.hasSection(section))
575+ throw new NoSectionException(section);
576+
577+ m_sections[section][option] = value;
578+ }
579+
580+ ///
581+ unittest
582+ {
583+ import std.exception : assertThrown;
584+
585+ auto conf = new ConfigParser();
586+
587+ assertThrown!NoSectionException(conf.set("Section", "option",
588+ "value"));
589+
590+ conf.addSection("Section");
591+ conf.set("Section", "option", "value");
592+ assert(conf.get("Section", "option") == "value");
593+ }
594+
595+ // void write(ref File file)
596+ // {
597+ // }
598+
599+ private:
600+
601+ void parseSectionHeader(ref string line)
602+ {
603+ import std.array : appender, assocArray;
604+
605+ auto sectionHeader = appender!string;
606+ /* presume that the last character is ] */
607+ sectionHeader.reserve(line.length - 1);
608+ string popped = line[1 .. $];
609+
610+ foreach(c; popped) {
611+ if (c != ']')
612+ sectionHeader.put(c);
613+ else
614+ break;
615+ }
616+
617+ version (DigitalMars) {
618+ m_currentSection = sectionHeader[];
619+ } else {
620+ /* LDC / GNU */
621+ m_currentSection = sectionHeader.data;
622+ }
623+
624+ try {
625+ this.addSection(m_currentSection);
626+ } catch (DuplicateSectionException e) {
627+ /* haven't checked what python would do */
628+ /* perhaps we should just merge the settings? */
629+ return;
630+ }
631+ }
632+
633+ void parseLine(ref string line)
634+ {
635+ import std.string : indexOfAny, strip;
636+
637+ ptrdiff_t idx = line.indexOfAny(m_delimiters);
638+ if (-1 == idx) return;
639+ string option = line[0 .. idx].dup.strip;
640+ string value = line[idx + 1 .. $].dup.strip;
641+
642+ m_sections[m_currentSection][option] = value;
643+ }
644+}
diff -r 000000000000 -r 019d96c167ce mlib/directories.d
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mlib/directories.d Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,317 @@
1+/*
2+ * This is free and unencumbered software released into the public domain.
3+ *
4+ * Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
5+ * software, either in source code form or as a compiled binary, for any purpose,
6+ * commercial or non-commercial, and by any means.
7+ *
8+ * In jurisdictions that recognize copyright laws, the author or authors of this
9+ * software dedicate any and all copyright interest in the software to the public
10+ * domain. We make this dedication for the benefit of the public at large and to
11+ * the detriment of our heirs and successors. We intend this dedication to be an
12+ * overt act of relinquishment in perpetuity of all present and future rights to
13+ * this software under copyright law.
14+ *
15+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+ * AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+ */
22+
23+/**
24+ * This module provides quick & easy access to the XDG folder locations on GNU/Linux. Currently,
25+ * only POSIX (XDG Base Directory Specification) is supported. OS X (Standard Directories) and
26+ * Windows (Known Folder) will be supported at a later date.
27+ *
28+ * The main goal of this module is to provide a minimal and simple API.
29+ *
30+ * API
31+ * ---
32+ * enum Directory
33+ * {
34+ * home,
35+ * config,
36+ * data,
37+ * cache,
38+ * }
39+ *
40+ * DirEntry open(Directory);
41+ * ---
42+ *
43+ * This module supports D version greater than or equal to 2.068.0.
44+ *
45+ * Authors: mio <stigma@disroot.org>
46+ * Bugs: Doesn't check if the environment variable value is empty.
47+ * Date: June 12, 2021
48+ * License: public domain
49+ * Standards: XDG Base Directory Specification 0.8
50+ * Version: 0.X.X
51+ */
52+module mlib.directories;
53+
54+import std.file : DirEntry;
55+
56+public enum Directory
57+ {
58+ home,
59+ data,
60+ config,
61+ state,
62+ // dataDirs, /* XDG */
63+ // configDirs, /* XDG */
64+ cache,
65+ runtime,
66+ }
67+
68+/++
69+ Return a DirEntry pointing to `dt`.
70+
71+ If the folder couldn't be found, an empty DirEntry is returned.
72+
73+ Examples:
74+ ----
75+ import directories;
76+ import std.stdio;
77+
78+ int main()
79+ {
80+ DirEntry userConfDir = open(Directory.config);
81+ /* Handle error */
82+ if (0 == userConfDir.name.length)
83+ {
84+ stderr.writeln("Failed to find user config directory.");
85+ return 1;
86+ }
87+ writefln("User config directory is %s", userConfDir.name);
88+ return 0;
89+ }
90+ ----
91++/
92+public DirEntry open(Directory dt) nothrow// @safe
93+{
94+ /*
95+ * Posix covers a few operating systems, if yours has a alternate
96+ * structure that is preferred over XDG, let me know.
97+ */
98+ version (Posix) enum supported = true;
99+ else enum supported = false;
100+ /* More operating systems will be supported soon-ish. */
101+
102+ static if (false == supported)
103+ {
104+ import core.stdc.stdio : fprintf, stderr;
105+
106+ fprintf(stderr, "*** error: The operating system you're running isn't supported. ***\n");
107+ // TODO: webpage for general contribution guide reporting.
108+ assert(false, "Unsupported platform.");
109+ }
110+
111+ immutable string path = getPath(dt);
112+ if (path is null) return DirEntry();
113+ try {
114+ return DirEntry(path);
115+ } catch (Exception e) {
116+ return DirEntry();
117+ }
118+}
119+
120+///
121+unittest
122+{
123+ import std.process : environment;
124+ import std.path : buildPath;
125+
126+ /*
127+ * Environment Variables (for checking against)
128+ * Currently only tests on Posix since that's the only supported
129+ * platform.
130+ */
131+ immutable string homeE = environment.get("HOME");
132+ immutable string configE = environment.get("XDG_CONFIG_HOME",
133+ buildPath(homeE, ".config"));
134+ immutable string dataE = environment.get("XDG_DATA_HOME",
135+ buildPath(homeE, ".local", "share"));
136+ immutable string cacheE = environment.get("XDG_CACHE_HOME",
137+ buildPath(homeE, ".cache"));
138+
139+ // Compare against folders.d (note that folders is cross-platform, so
140+ // some errors may occur on non-POSIX)
141+ assert(homeE == open(Directory.home));
142+ assert(configE == open(Directory.config));
143+ assert(dataE == open(Directory.data));
144+ assert(cacheE == open(Directory.cache));
145+ import std.stdio;
146+ writeln(typeof(open(Directory.cache)).stringof);
147+}
148+
149+private:
150+
151+void errLog(string msg) nothrow @trusted
152+{
153+ import core.stdc.stdio : fprintf, stderr;
154+
155+ // TODO: webpage for general issue reporting.
156+ // fprintf(stderr, "** info: report bugs to https://yume-neru.neocities.org/bugs.html **\n");
157+ fprintf(stderr, "*** error: %s ***\n", msg.ptr);
158+}
159+
160+immutable(string) getPath(in Directory dt) nothrow @safe
161+{
162+ switch (dt)
163+ {
164+ case Directory.home:
165+ return home();
166+ case Directory.data:
167+ return data();
168+ case Directory.config:
169+ return config();
170+ case Directory.cache:
171+ return cache();
172+ default:
173+ assert(false, "Unsupported Directory");
174+ }
175+}
176+
177+immutable(string) home() nothrow @trusted
178+{
179+ import std.path : isAbsolute;
180+ import std.process : environment;
181+
182+ string homeE;
183+
184+ try {
185+ homeE = environment.get("HOME");
186+ } catch (Exception e) {
187+ homeE = null;
188+ }
189+
190+ if (homeE is null) {
191+ import std.string : fromStringz;
192+ const(char)* pwdHome = fallbackHome();
193+ if (pwdHome !is null)
194+ homeE = cast(string)(pwdHome.fromStringz).dup;
195+ if (false == homeE.isAbsolute)
196+ homeE = null;
197+ }
198+ return homeE;
199+}
200+
201+immutable(string) data() nothrow @safe
202+{
203+ import std.path : buildPath, isAbsolute;
204+ import std.process : environment;
205+
206+ string dataE;
207+
208+ try {
209+ dataE = environment.get("XDG_DATA_HOME");
210+ } catch (Exception e) {
211+ dataE = null;
212+ }
213+
214+ if (dataE is null || false == dataE.isAbsolute)
215+ dataE = buildPath(home(), ".local", "share");
216+
217+ return dataE;
218+}
219+
220+immutable(string) config() nothrow @safe
221+{
222+ import std.path : buildPath, isAbsolute;
223+ import std.process : environment;
224+
225+ string configE;
226+
227+ try {
228+ configE = environment.get("XDG_CONFIG_HOME");
229+ } catch (Exception e) {
230+ configE = null;
231+ }
232+
233+ if (configE is null || false == configE.isAbsolute)
234+ configE = buildPath(home(), ".config");
235+
236+ return configE;
237+}
238+
239+/* TODO: xdgState() nothrow @safe */
240+
241+immutable(string) cache() nothrow @safe
242+{
243+ import std.path : buildPath, isAbsolute;
244+ import std.process : environment;
245+
246+ string cacheE;
247+
248+ try {
249+ cacheE = environment.get("XDG_CACHE_HOME");
250+ } catch (Exception e) {
251+ cacheE = null;
252+ }
253+
254+ if (cacheE is null || false == cacheE.isAbsolute)
255+ cacheE = buildPath(home(), ".cache");
256+
257+ return cacheE;
258+}
259+
260+/* Helpers */
261+
262+const(char)* fallbackHome() nothrow @trusted
263+{
264+ import core.stdc.string : strdup;
265+
266+ passwd* pw;
267+ char* home;
268+
269+ setpwent();
270+ pw = getpwuid(getuid());
271+ endpwent();
272+
273+ if (pw is null || pw.pw_dir is null)
274+ return null;
275+
276+ home = strdup(pw.pw_dir);
277+ return home;
278+}
279+
280+@system @nogc extern(C) nothrow
281+{
282+ /* <bits/types.h> */
283+ alias gid_t = uint;
284+ alias uid_t = uint;
285+
286+ /* <pwd.h> */
287+ struct passwd
288+ {
289+ /// Username
290+ char* pw_name;
291+ /// Hashed passphrase, if shadow database is not in use
292+ char* pw_password;
293+ /// User ID
294+ uid_t pw_uid;
295+ /// Group ID
296+ gid_t pw_gid;
297+ /// "Real" name
298+ char* pw_gecos;
299+ /// Home directory
300+ char* pw_dir;
301+ /// Shell program
302+ char* pw_shell;
303+ }
304+
305+ /// Rewind the user database stream
306+ extern void setpwent();
307+
308+ /// Close the user database stream
309+ extern void endpwent();
310+
311+ /// Retrieve the user database entry for the given user ID
312+ extern passwd* getpwuid(uid_t uid);
313+
314+ /* <unistd.h> */
315+ /// Returns the real user ID of the calling process.
316+ extern uid_t getuid();
317+}
diff -r 000000000000 -r 019d96c167ce mlib/dub.sdl
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mlib/dub.sdl Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,5 @@
1+name "mlib"
2+description "A collection of miscellaneous D files."
3+license "public domain"
4+
5+sourcePaths "."
diff -r 000000000000 -r 019d96c167ce pixiv_remove_unfollowed.d
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pixiv_remove_unfollowed.d Fri Jan 06 20:48:18 2023 +1000
@@ -0,0 +1,202 @@
1+/**
2+ * A simple tool to help identify which pixiv accounts you've
3+ * downloaded media from, have since unfollowed, but haven't removed
4+ * the directory.
5+ *
6+ * Bugs:
7+ * If you encounter any errors, please let me know.
8+ * I don't include any form of automated error reporting,
9+ * so please consider attaching the "pixiv_remove_unfollowed.log"
10+ * file.
11+ */
12+module app;
13+
14+import core.stdc.stdlib : exit;
15+
16+import std.experimental.logger : FileLogger;
17+
18+import std.container : DList;
19+import std.file;
20+import std.path;
21+import std.stdio;
22+import std.string;
23+
24+import mlib.configparser;
25+import mlib.directories;
26+
27+import pixivd;
28+import pixivd.types;
29+
30+struct Context
31+{
32+ string phpsessid;
33+ string pixivBaseDir;
34+}
35+
36+private FileLogger logger;
37+
38+// Initialize a Context.
39+// Could exit the program if it failed to find the user configuration directory,
40+// or if the user doesn't enter a PHPSESSID value when prompted.
41+Context fetchContext()
42+{
43+ Context ctx;
44+
45+ DirEntry configDir = open(Directory.config);
46+ if (configDir.name.length == 0) {
47+ logger.critical("directories.d open(Directory.config) failed to return a valid DirEntry.");
48+ stderr.writeln("Failed to determine your configuration directory, please consider setting the" ~
49+ " XDG_CONFIG_HOME environment variable.");
50+ exit(1);
51+ }
52+
53+ auto phpsessidPath = buildPath(configDir.name, "pixivd", "phpsessid.txt");
54+ string phpsessid;
55+
56+ if (false == exists(phpsessidPath)) {
57+ mkdirRecurse(buildPath(configDir.name, "pixivd"));
58+ writeln("PHPSESSID not found, please enter:");
59+ phpsessid = readln().strip();
60+ if (phpsessid.empty) {
61+ stderr.writeln("No PHPSESSID entered, exiting.");
62+ exit(1);
63+ }
64+ auto f = File(phpsessidPath, "w");
65+ f.writeln(phpsessid);
66+ f.close();
67+
68+ ctx.phpsessid = phpsessid;
69+ return ctx;
70+ } else {
71+ auto phpsessidFile = File(phpsessidPath, "r");
72+ phpsessid = phpsessidFile.readln().strip();
73+
74+ if (phpsessid.empty) {
75+ writeln("PHPSESSID not found, please enter:");
76+ phpsessid = readln().strip();
77+ if (phpsessid.empty) {
78+ stderr.writeln("No PHPSESSID entered, exiting.");
79+ exit(1);
80+ }
81+ phpsessidFile.writeln(phpsessid);
82+ }
83+ phpsessidFile.close();
84+ }
85+
86+ ctx.phpsessid = phpsessid;
87+ return ctx;
88+}
89+
90+string getPixivPictureDir()
91+{
92+ auto configDirPath = open(Directory.config);
93+ auto configFilePath = buildPath(configDirPath.name, "pixiv_down", "settings.conf");
94+
95+ if (false == exists(configFilePath)) {
96+ logger.warningf("\"%s\" does not exist.", configFilePath);
97+ return "";
98+ }
99+
100+ scope configparser = new ConfigParser();
101+ configparser.read(configFilePath);
102+
103+ return configparser.get("output", "base_folder");
104+}
105+
106+DList!string processDirs(string pixivDirPath)
107+{
108+ import std.ascii : isDigit;
109+
110+ auto dirs = dirEntries(pixivDirPath, SpanMode.shallow);
111+ auto uids = DList!string();
112+
113+ foreach(dir; dirs) {
114+ auto bname = baseName(dir);
115+ if (bname.length == 0 || false == isDigit(bname[0])) {
116+ continue;
117+ }
118+ auto uid = bname.split('_')[0];
119+ uids.insert(uid);
120+ }
121+
122+ return uids;
123+}
124+
125+void removeUserId(in ref Context ctx, string id) {
126+ foreach(dir; dirEntries(ctx.pixivBaseDir, SpanMode.shallow)) {
127+ string dirname = baseName(dir.name);
128+ if (dirname.startsWith(id)) {
129+ rmdirRecurse(dir);
130+ writefln("Deleted directory: %s", dirname);
131+ }
132+ }
133+}
134+
135+void removeUnfollowed(in ref Context ctx, DList!string uids)
136+{
137+ import core.thread : Thread;
138+ import core.time : seconds;
139+
140+ auto client = new Client(ctx.phpsessid);
141+
142+ foreach (id; uids) {
143+ FullUser user;
144+ try {
145+ user = client.fetchUser(id);
146+ } catch (PixivJSONException pje) {
147+ logger.warningf("(PixivJSONException) %s", pje.msg);
148+ writefln("User ID %s has either left pixiv, or does not exist.", id);
149+ write("Do you want to remove the related directories? [y/N]: ");
150+ string input = readln().strip().toLower();
151+
152+ if (input == "y" || input == "yes") {
153+ removeUserId(ctx, id);
154+ }
155+ continue;
156+ } catch (Exception e) {
157+ logger.critical(e.msg);
158+
159+ stderr.writefln("Error encountered fetching user %s. No files were modified.", id);
160+ continue;
161+ }
162+
163+ if (false == user.following) {
164+ writefln("Found user not followed: %s (%s)", user.userName, user.userId);
165+ write("Do you want to remove the related directories? [y/N]: ");
166+ string input = readln().strip().toLower();
167+
168+ if (input == "y" || input == "yes") {
169+ writeln("Will remove user data.");
170+ removeUserId(ctx, id);
171+ }
172+ } else {
173+ writefln("Found followed user: %s", user.userName);
174+ }
175+ writeln("Sleeping 10 seconds");
176+ Thread.sleep(10.seconds);
177+ }
178+}
179+
180+
181+int main(string[] args)
182+{
183+ logger = new FileLogger("pixiv_remove_unfollowed.log");
184+
185+ Context ctx = fetchContext();
186+
187+ ctx.pixivBaseDir = getPixivPictureDir();
188+ if (0 == ctx.pixivBaseDir.length) {
189+ stderr.writeln("Error: Could not retrieve pixiv_down configuration file. " ~
190+ "Possibly because pixiv_down hasn't been used before.");
191+ return 1;
192+ }
193+
194+ DList!string uids = processDirs(ctx.pixivBaseDir);
195+ if (uids.empty) {
196+ writefln("No users found in %s!", ctx.pixivBaseDir);
197+ return 0;
198+ }
199+
200+ removeUnfollowed(ctx, uids);
201+ return 0;
202+}