|
|
@ -8,9 +8,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
"""Global configuration module"""
|
|
|
|
"""Global configuration module"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import sys
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
import os.path
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
|
from . import util
|
|
|
|
from . import util
|
|
|
|
|
|
|
|
|
|
|
@ -21,6 +21,7 @@ log = logging.getLogger("config")
|
|
|
|
# internals
|
|
|
|
# internals
|
|
|
|
|
|
|
|
|
|
|
|
_config = {"__global__": {}}
|
|
|
|
_config = {"__global__": {}}
|
|
|
|
|
|
|
|
_warn_legacy = True
|
|
|
|
|
|
|
|
|
|
|
|
if util.WINDOWS:
|
|
|
|
if util.WINDOWS:
|
|
|
|
_default_configs = [
|
|
|
|
_default_configs = [
|
|
|
@ -49,23 +50,84 @@ if getattr(sys, "frozen", False):
|
|
|
|
# --------------------------------------------------------------------
|
|
|
|
# --------------------------------------------------------------------
|
|
|
|
# public interface
|
|
|
|
# public interface
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get(section, key, default=None, *, conf=_config):
|
|
|
|
|
|
|
|
"""Get the value of property 'key' or a default value"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
return conf[section][key]
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set(section, key, value, *, conf=_config):
|
|
|
|
|
|
|
|
"""Set the value of property 'key' for this session"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
conf[section][key] = value
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
|
|
conf[section] = {key: value}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setdefault(section, key, value, *, conf=_config):
|
|
|
|
|
|
|
|
"""Set the value of property 'key' if it doesn't exist"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
conf[section].setdefault(key, value)
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
|
|
conf[section] = {key: value}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unset(section, key, *, conf=_config):
|
|
|
|
|
|
|
|
"""Unset the value of property 'key'"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
del conf[section][key]
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def interpolate(sections, key, default=None, *, conf=_config):
|
|
|
|
|
|
|
|
if key in conf["__global__"]:
|
|
|
|
|
|
|
|
return conf["__global__"][key]
|
|
|
|
|
|
|
|
for section in sections:
|
|
|
|
|
|
|
|
if section in conf and key in conf[section]:
|
|
|
|
|
|
|
|
default = conf[section][key]
|
|
|
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class apply():
|
|
|
|
|
|
|
|
"""Context Manager: apply a collection of key-value pairs"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, kvlist):
|
|
|
|
|
|
|
|
self.original = []
|
|
|
|
|
|
|
|
self.kvlist = kvlist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
|
|
|
for path, key, value in self.kvlist:
|
|
|
|
|
|
|
|
self.original.append((path, key, get(path, key, util.SENTINEL)))
|
|
|
|
|
|
|
|
set(path, key, value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __exit__(self, etype, value, traceback):
|
|
|
|
|
|
|
|
for path, key, value in self.original:
|
|
|
|
|
|
|
|
if value is util.SENTINEL:
|
|
|
|
|
|
|
|
unset(path, key)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
set(path, key, value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load(files=None, strict=False, fmt="json"):
|
|
|
|
def load(files=None, strict=False, fmt="json"):
|
|
|
|
"""Load JSON configuration files"""
|
|
|
|
"""Load JSON configuration files"""
|
|
|
|
if fmt == "yaml":
|
|
|
|
if fmt == "yaml":
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
import yaml
|
|
|
|
import yaml
|
|
|
|
parsefunc = yaml.safe_load
|
|
|
|
load = yaml.safe_load
|
|
|
|
except ImportError:
|
|
|
|
except ImportError:
|
|
|
|
log.error("Could not import 'yaml' module")
|
|
|
|
log.error("Could not import 'yaml' module")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
parsefunc = json.load
|
|
|
|
load = json.load
|
|
|
|
|
|
|
|
|
|
|
|
for path in files or _default_configs:
|
|
|
|
for path in files or _default_configs:
|
|
|
|
path = util.expand_path(path)
|
|
|
|
path = util.expand_path(path)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
with open(path, encoding="utf-8") as fp:
|
|
|
|
with open(path, encoding="utf-8") as fp:
|
|
|
|
confdict = parsefunc(fp)
|
|
|
|
config_dict = load(fp)
|
|
|
|
except OSError as exc:
|
|
|
|
except OSError as exc:
|
|
|
|
if strict:
|
|
|
|
if strict:
|
|
|
|
log.error(exc)
|
|
|
|
log.error(exc)
|
|
|
@ -75,10 +137,17 @@ def load(files=None, strict=False, fmt="json"):
|
|
|
|
if strict:
|
|
|
|
if strict:
|
|
|
|
sys.exit(2)
|
|
|
|
sys.exit(2)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
|
|
|
|
if "extractor" in config_dict:
|
|
|
|
|
|
|
|
if _warn_legacy:
|
|
|
|
|
|
|
|
log.warning("Legacy config file found at %s", path)
|
|
|
|
|
|
|
|
log.warning("Run 'gallery-dl --config-update' or follow "
|
|
|
|
|
|
|
|
"<link> to update to the new format")
|
|
|
|
|
|
|
|
config_dict = update_config_dict(config_dict)
|
|
|
|
|
|
|
|
|
|
|
|
if not _config:
|
|
|
|
if not _config:
|
|
|
|
_config.update(confdict)
|
|
|
|
_config.update(config_dict)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
util.combine_dict(_config, confdict)
|
|
|
|
util.combine_dict(_config, config_dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear():
|
|
|
|
def clear():
|
|
|
@ -153,62 +222,166 @@ def build_module_options_dict(extr, package, module, conf=_config):
|
|
|
|
return build_options_dict(keys)
|
|
|
|
return build_options_dict(keys)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get(section, key, default=None, *, conf=_config):
|
|
|
|
def config_init():
|
|
|
|
"""Get the value of property 'key' or a default value"""
|
|
|
|
paths = [
|
|
|
|
try:
|
|
|
|
util.expand_path(path)
|
|
|
|
return conf[section][key]
|
|
|
|
for path in _default_configs
|
|
|
|
except Exception:
|
|
|
|
]
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for path in paths:
|
|
|
|
|
|
|
|
if os.access(path, os.R_OK):
|
|
|
|
|
|
|
|
log.error("There is already a configuration file at %s", path)
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
def set(section, key, value, *, conf=_config):
|
|
|
|
for path in paths:
|
|
|
|
"""Set the value of property 'key' for this session"""
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
with open(path, "w", encoding="utf-8") as fp:
|
|
|
|
conf[section][key] = value
|
|
|
|
fp.write("""\
|
|
|
|
except KeyError:
|
|
|
|
{
|
|
|
|
conf[section] = {key: value}
|
|
|
|
"general": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"downloader": {
|
|
|
|
|
|
|
|
|
|
|
|
def setdefault(section, key, value, *, conf=_config):
|
|
|
|
},
|
|
|
|
"""Set the value of property 'key' if it doesn't exist"""
|
|
|
|
"output": {
|
|
|
|
try:
|
|
|
|
|
|
|
|
conf[section].setdefault(key, value)
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
|
|
conf[section] = {key: value}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
|
|
log.debug(exc)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
log.error("Unable to create a new configuration file "
|
|
|
|
|
|
|
|
"at any of the default paths")
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
def unset(section, key, *, conf=_config):
|
|
|
|
log.info("Created a basic configuration file at %s", path)
|
|
|
|
"""Unset the value of property 'key'"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
del conf[section][key]
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def interpolate(sections, key, default=None, *, conf=_config):
|
|
|
|
def config_open():
|
|
|
|
if key in conf["__global__"]:
|
|
|
|
for path in _default_configs:
|
|
|
|
return conf["__global__"][key]
|
|
|
|
path = util.expand_path(path)
|
|
|
|
for section in sections:
|
|
|
|
if os.access(path, os.R_OK | os.W_OK):
|
|
|
|
if section in conf and key in conf[section]:
|
|
|
|
import subprocess
|
|
|
|
default = conf[section][key]
|
|
|
|
import shutil
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
openers = (("explorer", "notepad")
|
|
|
|
|
|
|
|
if util.WINDOWS else
|
|
|
|
|
|
|
|
("xdg-open", "open", os.environ.get("EDITOR", "")))
|
|
|
|
|
|
|
|
for opener in openers:
|
|
|
|
|
|
|
|
if opener := shutil.which(opener):
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
log.warning("Unable to find a program to open '%s' with", path)
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log.info("Running '%s %s'", opener, path)
|
|
|
|
|
|
|
|
return subprocess.Popen((opener, path)).wait()
|
|
|
|
|
|
|
|
|
|
|
|
class apply():
|
|
|
|
log.warning("Unable to find any writable configuration file")
|
|
|
|
"""Context Manager: apply a collection of key-value pairs"""
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, kvlist):
|
|
|
|
|
|
|
|
self.original = []
|
|
|
|
|
|
|
|
self.kvlist = kvlist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
def config_status():
|
|
|
|
for path, key, value in self.kvlist:
|
|
|
|
for path in _default_configs:
|
|
|
|
self.original.append((path, key, get(path, key, util.SENTINEL)))
|
|
|
|
path = util.expand_path(path)
|
|
|
|
set(path, key, value)
|
|
|
|
try:
|
|
|
|
|
|
|
|
with open(path, encoding="utf-8") as fp:
|
|
|
|
|
|
|
|
config_dict = json.load(fp)
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
|
|
status = "NOT PRESENT"
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
|
|
status = "INVALID JSON"
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
|
|
log.debug(exc)
|
|
|
|
|
|
|
|
status = "UNKNOWN"
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
status = "OK"
|
|
|
|
|
|
|
|
if "extractor" in config_dict:
|
|
|
|
|
|
|
|
status += " (legacy)"
|
|
|
|
|
|
|
|
print(f"{path}: {status}")
|
|
|
|
|
|
|
|
|
|
|
|
def __exit__(self, etype, value, traceback):
|
|
|
|
|
|
|
|
for path, key, value in self.original:
|
|
|
|
def config_update():
|
|
|
|
if value is util.SENTINEL:
|
|
|
|
for path in _default_configs:
|
|
|
|
unset(path, key)
|
|
|
|
path = util.expand_path(path)
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
with open(path, encoding="utf-8") as fp:
|
|
|
|
|
|
|
|
config_content = fp.read()
|
|
|
|
|
|
|
|
config_dict = json.loads(config_content)
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
|
|
log.debug(exc)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
if "extractor" in config_dict:
|
|
|
|
|
|
|
|
config_dict = update_config_dict(config_dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# write backup
|
|
|
|
|
|
|
|
with open(path + ".bak", "w", encoding="utf-8") as fp:
|
|
|
|
|
|
|
|
fp.write(config_content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# overwrite with updated JSON
|
|
|
|
|
|
|
|
with open(path, "w", encoding="utf-8") as fp:
|
|
|
|
|
|
|
|
json.dump(
|
|
|
|
|
|
|
|
config_dict, fp,
|
|
|
|
|
|
|
|
indent=4,
|
|
|
|
|
|
|
|
ensure_ascii=get("output", "ascii"),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log.info("Updated %s", path)
|
|
|
|
|
|
|
|
log.info("Backup at %s", path + ".bak")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_config_dict(config_legacy):
|
|
|
|
|
|
|
|
# option names that could be a dict
|
|
|
|
|
|
|
|
optnames = {"filename", "directory", "path-restrict", "cookies",
|
|
|
|
|
|
|
|
"extension-map", "keywords", "keywords-default", "proxy"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config = {"general": {}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if extractor := config_legacy.pop("extractor", None):
|
|
|
|
|
|
|
|
for key, value in extractor.items():
|
|
|
|
|
|
|
|
if isinstance(value, dict) and key not in optnames:
|
|
|
|
|
|
|
|
config[key] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
delete = []
|
|
|
|
|
|
|
|
instances = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for skey, sval in value.items():
|
|
|
|
|
|
|
|
if isinstance(sval, dict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# basecategory instance
|
|
|
|
|
|
|
|
if "root" in sval:
|
|
|
|
|
|
|
|
if instances is None:
|
|
|
|
|
|
|
|
config[key + ":instances"] = instances = {}
|
|
|
|
|
|
|
|
instances[skey] = sval
|
|
|
|
|
|
|
|
delete.append(skey)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# subcategory options
|
|
|
|
|
|
|
|
elif skey not in optnames:
|
|
|
|
|
|
|
|
config[f"{key}:{skey}"] = value[skey]
|
|
|
|
|
|
|
|
delete.append(skey)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for skey in delete:
|
|
|
|
|
|
|
|
del value[skey]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not value:
|
|
|
|
|
|
|
|
del config[key]
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
set(path, key, value)
|
|
|
|
config["general"][key] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if downloader := config_legacy.pop("downloader", None):
|
|
|
|
|
|
|
|
config["downloader"] = downloader
|
|
|
|
|
|
|
|
for module in ("http", "ytdl", "text"):
|
|
|
|
|
|
|
|
if opts := downloader.pop(module, None):
|
|
|
|
|
|
|
|
config["downloader:" + module] = opts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for section_name in ("output", "postprocessor", "cache"):
|
|
|
|
|
|
|
|
if section := config_legacy.pop(section_name, None):
|
|
|
|
|
|
|
|
config[section_name] = section
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for key, value in config_legacy.items():
|
|
|
|
|
|
|
|
config["general"][key] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return config
|
|
|
|