Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/gallery_dl/config.py
5457 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2015-2025 Mike Fährmann
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 version 2 as
7
# published by the Free Software Foundation.
8
9
"""Global configuration module"""
10
11
import sys
12
import os.path
13
import logging
14
from . import util
15
16
log = logging.getLogger("config")
17
18
19
# --------------------------------------------------------------------
20
# internals
21
22
_config = {}
23
_files = []
24
25
if util.WINDOWS:
26
_default_configs = [
27
r"%APPDATA%\gallery-dl\config.json",
28
r"%USERPROFILE%\gallery-dl\config.json",
29
r"%USERPROFILE%\gallery-dl.conf",
30
]
31
else:
32
_default_configs = [
33
"/etc/gallery-dl.conf",
34
"${XDG_CONFIG_HOME}/gallery-dl/config.json"
35
if os.environ.get("XDG_CONFIG_HOME") else
36
"${HOME}/.config/gallery-dl/config.json",
37
"${HOME}/.gallery-dl.conf",
38
]
39
40
41
if util.EXECUTABLE:
42
# look for config file in PyInstaller executable directory (#682)
43
_default_configs.append(os.path.join(
44
os.path.dirname(sys.executable),
45
"gallery-dl.conf",
46
))
47
48
49
# --------------------------------------------------------------------
50
# public interface
51
52
53
def initialize():
54
paths = list(map(util.expand_path, _default_configs))
55
56
for path in paths:
57
if os.access(path, os.R_OK | os.W_OK):
58
log.error("There is already a configuration file at '%s'", path)
59
return 1
60
61
for path in paths:
62
try:
63
os.makedirs(os.path.dirname(path), exist_ok=True)
64
with open(path, "x", encoding="utf-8") as fp:
65
fp.write("""\
66
{
67
"extractor": {
68
69
},
70
"downloader": {
71
72
},
73
"output": {
74
75
},
76
"postprocessor": {
77
78
}
79
}
80
""")
81
break
82
except OSError as exc:
83
log.debug("%s: %s", exc.__class__.__name__, exc)
84
else:
85
log.error("Unable to create a new configuration file "
86
"at any of the default paths")
87
return 1
88
89
log.info("Created a basic configuration file at '%s'", path)
90
return 0
91
92
93
def open_extern():
94
for path in _default_configs:
95
path = util.expand_path(path)
96
if os.access(path, os.R_OK | os.W_OK):
97
break
98
else:
99
log.warning("Unable to find any writable configuration file")
100
return 1
101
102
if util.WINDOWS:
103
openers = ("explorer", "notepad")
104
else:
105
openers = ("xdg-open", "open")
106
if editor := os.environ.get("EDITOR"):
107
openers = (editor,) + openers
108
109
import shutil
110
for opener in openers:
111
if opener := shutil.which(opener):
112
break
113
else:
114
log.warning("Unable to find a program to open '%s' with", path)
115
return 1
116
117
log.info("Running '%s %s'", opener, path)
118
retcode = util.Popen((opener, path)).wait()
119
120
if not retcode:
121
try:
122
with open(path, encoding="utf-8") as fp:
123
util.json_loads(fp.read())
124
except Exception as exc:
125
log.warning("%s when parsing '%s': %s",
126
exc.__class__.__name__, path, exc)
127
return 2
128
129
return retcode
130
131
132
def status():
133
from .output import stdout_write
134
135
paths = []
136
for path in _default_configs:
137
path = util.expand_path(path)
138
139
try:
140
with open(path, encoding="utf-8") as fp:
141
util.json_loads(fp.read())
142
except FileNotFoundError:
143
status = "Not Present"
144
except OSError:
145
status = "Inaccessible"
146
except ValueError:
147
status = "Invalid JSON"
148
except Exception as exc:
149
log.debug(exc)
150
status = "Unknown"
151
else:
152
status = "OK"
153
154
paths.append((path, status))
155
156
fmt = f"{{:<{max(len(p[0]) for p in paths)}}} : {{}}\n".format
157
158
for path, status in paths:
159
stdout_write(fmt(path, status))
160
161
162
def remap_categories():
163
opts = _config.get("extractor")
164
if not opts:
165
return
166
167
cmap = opts.get("config-map")
168
if cmap is None:
169
cmap = (
170
("coomerparty" , "coomer"),
171
("kemonoparty" , "kemono"),
172
("giantessbooru", "sizebooru"),
173
("koharu" , "schalenetwork"),
174
("naver" , "naver-blog"),
175
("chzzk" , "naver-chzzk"),
176
("naverwebtoon", "naver-webtoon"),
177
("pixiv" , "pixiv-novel"),
178
)
179
elif not cmap:
180
return
181
elif isinstance(cmap, dict):
182
cmap = cmap.items()
183
184
for old, new in cmap:
185
if old in opts and new not in opts:
186
opts[new] = opts[old]
187
188
189
def load(files=None, strict=False, loads=util.json_loads, conf=_config):
190
"""Load JSON configuration files"""
191
for pathfmt in files or _default_configs:
192
path = util.expand_path(pathfmt)
193
try:
194
with open(path, encoding="utf-8") as fp:
195
config = loads(fp.read())
196
except OSError as exc:
197
if strict:
198
log.error(exc)
199
raise SystemExit(1)
200
except Exception as exc:
201
log.error("%s when loading '%s': %s",
202
exc.__class__.__name__, path, exc)
203
if strict:
204
raise SystemExit(2)
205
else:
206
if not conf:
207
conf.update(config)
208
else:
209
util.combine_dict(conf, config)
210
_files.append(pathfmt)
211
212
if "subconfigs" in config:
213
if subconfigs := config["subconfigs"]:
214
if isinstance(subconfigs, str):
215
subconfigs = (subconfigs,)
216
load(subconfigs, strict, loads, conf)
217
218
219
def clear():
220
"""Reset configuration to an empty state"""
221
_config.clear()
222
223
224
def get(path, key, default=None, conf=_config):
225
"""Get the value of property 'key' or a default value"""
226
try:
227
for p in path:
228
conf = conf[p]
229
return conf[key]
230
except Exception:
231
return default
232
233
234
def interpolate(path, key, default=None, conf=_config):
235
"""Interpolate the value of 'key'"""
236
if key in conf:
237
return conf[key]
238
try:
239
for p in path:
240
conf = conf[p]
241
if key in conf:
242
default = conf[key]
243
except Exception:
244
pass
245
return default
246
247
248
def interpolate_common(common, paths, key, default=None, conf=_config):
249
"""Interpolate the value of 'key'
250
using multiple 'paths' along a 'common' ancestor
251
"""
252
if key in conf:
253
return conf[key]
254
255
# follow the common path
256
try:
257
for p in common:
258
conf = conf[p]
259
if key in conf:
260
default = conf[key]
261
except Exception:
262
return default
263
264
# try all paths until a value is found
265
value = util.SENTINEL
266
for path in paths:
267
c = conf
268
try:
269
for p in path:
270
c = c[p]
271
if key in c:
272
value = c[key]
273
except Exception:
274
pass
275
if value is not util.SENTINEL:
276
return value
277
return default
278
279
280
def accumulate(path, key, conf=_config):
281
"""Accumulate the values of 'key' along 'path'"""
282
result = []
283
try:
284
if key in conf:
285
if value := conf[key]:
286
if isinstance(value, list):
287
result.extend(value)
288
else:
289
result.append(value)
290
for p in path:
291
conf = conf[p]
292
if key in conf:
293
if value := conf[key]:
294
if isinstance(value, list):
295
result[:0] = value
296
else:
297
result.insert(0, value)
298
except Exception:
299
pass
300
return result
301
302
303
def set(path, key, value, conf=_config):
304
"""Set the value of property 'key' for this session"""
305
for p in path:
306
try:
307
conf = conf[p]
308
except KeyError:
309
conf[p] = conf = {}
310
conf[key] = value
311
312
313
def setdefault(path, key, value, conf=_config):
314
"""Set the value of property 'key' if it doesn't exist"""
315
for p in path:
316
try:
317
conf = conf[p]
318
except KeyError:
319
conf[p] = conf = {}
320
return conf.setdefault(key, value)
321
322
323
def unset(path, key, conf=_config):
324
"""Unset the value of property 'key'"""
325
try:
326
for p in path:
327
conf = conf[p]
328
del conf[key]
329
except Exception:
330
pass
331
332
333
class apply():
334
"""Context Manager: apply a collection of key-value pairs"""
335
336
def __init__(self, kvlist):
337
self.original = []
338
self.kvlist = kvlist
339
340
def __enter__(self):
341
for path, key, value in self.kvlist:
342
self.original.append((path, key, get(path, key, util.SENTINEL)))
343
set(path, key, value)
344
345
def __exit__(self, exc_type, exc_value, traceback):
346
self.original.reverse()
347
for path, key, value in self.original:
348
if value is util.SENTINEL:
349
unset(path, key)
350
else:
351
set(path, key, value)
352
353