From f56dc0d9658359e191329a6e17a75f49a37c3360 Mon Sep 17 00:00:00 2001 From: Packit Service Date: Dec 09 2020 18:08:32 +0000 Subject: Changes after running %prep ignore: true --- diff --git a/apps/lensfun-add-adapter b/apps/lensfun-add-adapter index 7f84730..c4bc65b 100755 --- a/apps/lensfun-add-adapter +++ b/apps/lensfun-add-adapter @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/libexec/platform-python # -*- coding: utf-8 -*- """This program allows the user to add further mount compatibilities to his diff --git a/apps/lensfun-add-adapter.shbang b/apps/lensfun-add-adapter.shbang new file mode 100755 index 0000000..7f84730 --- /dev/null +++ b/apps/lensfun-add-adapter.shbang @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This program allows the user to add further mount compatibilities to his +Lensfun configuration. This way, the number of default mount compatibilities +can be kept low, while users who own adapters can add those to their local +installation. Especially mirrorless systems can use all SLR lenses, so the +lens lists to choose from would become very long. + +The program can be used non-interactively (passing arguments on the command +line) and interactively, where the program asks you questions and takes your +input. + +For example, you own a Sony NEX-7 and have bought an adapter to use A-mount +lenses with it. Thus, you start this program. The program asks for a camera +model name. You enter "NEX-7". Then, you get a list of possible mounts, +together with numbers. In this list, it says:: + + 32) Sony Alpha + +So, you enter "32 ". That's it. Lensfun now offers also A-mount +lenses for E-mount cameras (all, by the way, not only for the NEX-7). + +Diagnostics: + +Status code 0 -- successful + 1 -- invalid command line arguments + 2 -- invalid input in interactive mode + +The program writes its output to ``~/.local/share/lensfun/_mounts.xml``. Any +former mount configuration in that file is preserved. Since it can be assumed +that this file will not change its format often (in particular, not with every +new Lensfun DB version), this file is deliberately not put in a versioned +sub-directory. As a positive side-effect of this, the user is not forced to +move this file once a new Lensfun DB version comes out. +""" + +import os, sys, argparse, glob +from xml.etree import ElementTree +import lensfun + + +local_xml_filepath = os.path.expanduser("~/.local/share/lensfun/_mounts.xml") +os.makedirs(os.path.dirname(local_xml_filepath), exist_ok=True) + +parser = argparse.ArgumentParser(description="Add mount compatibilities (adapters) to Lensfun's database.") +parser.add_argument("--maker", help="Exact camera manufacturer name.") +parser.add_argument("--model", help="Exact camera model name.") +parser.add_argument("--mount", help="Exact mount name to add as an adapter for camera mount.") +parser.add_argument("--remove-local-mount-config", action="store_true", + help="Remove all local changes to the mount compatibility configuration made through this program.") +args = parser.parse_args() +if args.maker and not args.model or args.model and not args.maker: + print("ERROR: The options --maker and --camera must be passed together.") + sys.exit(1) +if args.remove_local_mount_config: + if args.maker or args.model or args.mount: + print("ERROR: If the option --remove-local-mount-config is given, it must be the only one.") + sys.exit(1) + +def default_name(element, tag_name): + """Returns the untranslated string in the tag `tag_name`, which is a + sub-element of `element`. This is useful if you need the e.g. definitive + model name of a camera in order to use it as a key. + """ + for child in element.findall(tag_name): + if "lang" not in child.attrib: + return child.text + +def fancy_name(element, tag_name): + """Returns the English version of the string in the tag `tag_name`, which is a + sub-element of `element`. If not available, take the untranslated version. + """ + for child in element.findall(tag_name): + if child.attrib.get("lang") == "en": + return child.text + return default_name(element, tag_name) + +def normalize_camera_name(name): + return name.lower().translate(str.maketrans("", "", " ,.:-+*/()[]")) + +def indent(root, level=0): + """Pretty-print an XML tree by indentation.""" + i = "\n" + level * " " + if len(root): + if not root.text or not root.text.strip(): + root.text = i + " " + if not root.tail or not root.tail.strip(): + root.tail = i + for root in root: + indent(root, level + 1) + if not root.tail or not root.tail.strip(): + root.tail = i + else: + if level and (not root.tail or not root.tail.strip()): + root.tail = i + + +class Mount: + """Normally, all data structures in this program are just strings or mappings + or lists of strings. Even compatibility mounts are only represented by + their name. However, for the main list of mounts, we have to store more of + them. Therefore, this is a class. + """ + def __init__(self, name, fixed_lens=False): + self.name, self.fixed_lens = name, fixed_lens + self.compatible_mounts = set() + def __str__(self): + return self.name + +def read_database(): + """Read the database of Lensfun. Note that also the local configuration – + including the ``_mounts.xml`` file – is read. + + It returns the cameras as a mapping from the (maker, model) tuple to the + mount object, the `makers_and_models` mapping which maps so-called “loose” + camera names to (maker, model) tuples, and the mounts as a mapping from + mount names to `Mount` objects. + + “Loose” camera names are brutally normalised to that partial string + matching can be easily done against user input. In particular, everything + is lowercase and all punctuation and spacing is removed. + """ + cameras = {} + makers_and_models = {} + mounts = {} + paths = lensfun.get_database_directories() + for path in paths: + for filepath in glob.glob(os.path.join(path, "*.xml")): + filename = os.path.basename(filepath) + tree = ElementTree.parse(filepath) + for mount in tree.findall("mount"): + name = default_name(mount, "name") + try: + fixed_lens = mounts[name].fixed_lens + except KeyError: + fixed_lens = filename.startswith("compact") or name in ["olympusE10"] + mounts[name] = Mount(name, fixed_lens) + for compatible_mount in mount.findall("compat"): + mounts[name].compatible_mounts.add(compatible_mount.text) + for camera in tree.findall("camera"): + loose_name = normalize_camera_name(fancy_name(camera, "maker") + fancy_name(camera, "model")) + maker_and_model = default_name(camera, "maker"), default_name(camera, "model") + makers_and_models[loose_name] = maker_and_model + mount = default_name(camera, "mount") + cameras[maker_and_model] = mount + mounts.setdefault(mount, Mount(mount, filename.startswith("compact") or mount in ["olympusE10"])) + for maker_and_model, mount_name in cameras.items(): + cameras[maker_and_model] = mounts[mount_name] + return mounts, makers_and_models, cameras + +def find_mount_groups(mounts): + """A “mount group” is a set of mount names that are fully compatible to + every other one in the group. This way, we can offer the user to add all + Nikon lenses in one go to their camera mount, no matter whether the Nikon + lens is Nikon F, Nikon F AI-S, etc. Remember that mount compatibilities + are not transitive in Lensfun. + """ + groups = set() + for mount in mounts.values(): + if mount.name != "Generic": + mutually_compatibles = {mount.name} + for compatible_mount in mount.compatible_mounts: + try: + reverse_compatibles = mounts[compatible_mount].compatible_mounts + except KeyError: + continue + if compatible_mount != "Generic" and mount.name in reverse_compatibles: + mutually_compatibles.add(compatible_mount) + if len(mutually_compatibles) > 1: + groups.add(frozenset(mutually_compatibles)) + return groups + +def find_to_mount(cameras, makers_and_models): + """The `to_mount` is the camera mount, i.e. the mount *to* which we want to + adapt. + """ + if args.model: + try: + return cameras[args.maker, args.model] + except KeyError: + print("ERROR: Camera model not found in database.") + sys.exit(1) + else: + camera_name = normalize_camera_name(input("Enter camera model name (or part of it): ")) + hits = [] + for loose_name, maker_and_model in makers_and_models.items(): + if camera_name in loose_name: + hits.append(maker_and_model) + if not hits: + print("ERROR: No cameras with this name found.") + sys.exit(2) + elif len(hits) > 1: + hits.sort() + for i, maker_and_model in enumerate(hits, 1): + print("{:3d}) {} {}".format(i, *maker_and_model)) + try: + number = int(input("Enter number: ")) + maker_and_model = hits[number - 1] + except (ValueError, IndexError, EOFError): + print("ERROR: Invalid input.") + sys.exit(2) + else: + maker_and_model = hits[0] + return cameras[maker_and_model] + +def find_from_mounts(mounts, groups, to_mount): + """The `from_mount` is the lenses' mount, i.e. the mount *from* which we want + to adapt. It may be a set of mounts in case of a mount group. + """ + if args.mount: + if args.mount not in mounts: + print("ERROR: Mount name not found in database.") + sys.exit(1) + return {args.mount} + else: + from_candidates = [] + for from_candidate in [(mount.name, {mount.name}) for mount in mounts.values() if not mount.fixed_lens] + \ + [(", ".join(sorted(group)) + " (all together)", group) for group in groups]: + for mount in from_candidate[1]: + if mount not in to_mount.compatible_mounts | {to_mount.name}: + break + else: + continue + from_candidates.append(from_candidate) + + from_candidates.sort() + for i, mount_data in enumerate(from_candidates, 1): + print("{:3d}) {}".format(i, mount_data[0])) + try: + number = int(input("Enter number: ")) + from_mounts = from_candidates[number - 1][1] + except (ValueError, IndexError, EOFError): + print("ERROR: Invalid input.") + sys.exit(2) + return from_mounts + +def write_xml_file(to_mount, from_mounts, mounts): + """We write the ``_mounts.xml`` file rather non-invasive, i.e. we touch only + the mount we want to change. Either we change it, or we add it. + """ + try: + tree = ElementTree.parse(local_xml_filepath) + except (OSError, ElementTree.ParseError): + tree = ElementTree.ElementTree(ElementTree.Element("lensdatabase")) + for mount in tree.findall("mount"): + if default_name(mount, "name") == to_mount.name: + for compatible_mount in mount.findall("compat"): + mount.remove(compatible_mount) + break + else: + mount = ElementTree.SubElement(tree.getroot(), "mount") + ElementTree.SubElement(mount, "name").text = to_mount.name + for compatible_mount in sorted(to_mount.compatible_mounts): + ElementTree.SubElement(mount, "compat").text = compatible_mount + indent(tree.getroot()) + tree.write(local_xml_filepath) + + +if args.remove_local_mount_config: + try: + os.remove(local_xml_filepath) + except OSError: + pass +else: + mounts, makers_and_models, cameras = read_database() + mount_groups = find_mount_groups(mounts) + to_mount = find_to_mount(cameras, makers_and_models) + if to_mount.fixed_lens: + from_mounts = {"Generic"} + else: + from_mounts = find_from_mounts(mounts, mount_groups, to_mount) + to_mount.compatible_mounts.update(from_mounts) + write_xml_file(to_mount, from_mounts, mounts) + print("Made {} lenses mountable to {}.".format(" / ".join(from_mounts), to_mount)) diff --git a/apps/lensfun-update-data b/apps/lensfun-update-data index 2fa0e88..031cfa7 100755 --- a/apps/lensfun-update-data +++ b/apps/lensfun-update-data @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/libexec/platform-python # -*- coding: utf-8 -*- """This program fetches the latest version of the Lensfun database from the diff --git a/apps/lensfun-update-data.shbang b/apps/lensfun-update-data.shbang new file mode 100755 index 0000000..2fa0e88 --- /dev/null +++ b/apps/lensfun-update-data.shbang @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This program fetches the latest version of the Lensfun database from the +Internet and places it on the local system. This way, the user can update the +database conveniently. Unfortunately, we have to take into account that the +Git database may have a too new format for the locally installed Lensfun. +Then, it may fetch backports of the database from other URLs. + +This program must be called with root privileges. It stores the new database +in `/var/lib/lensfun-updates`. + +The repository of databases resides at a base URL. Below that URL, there is +the file versions.json. It contains a list with three elements. The first is +the database timestamp, the second is a list of available version numbers, and +the third is a list of strings which represents further alternative base URLs +to look at. So, the file may contain the following:: + + [1386797501, [1, 2, 3], ["http://wilson.bronger.org/"]] + +All URLs must end with a slash. For every version number, there must be a file +called version_.tar.bz2. So in our case, there must be the +files + +:: + + version_1.tar.bz2 + version_2.tar.bz2 + version_3.tar.bz2 + +in the same directory as versions.json. These tar balls contain the Lensfun +database with the given timestamp and version. + +The timestamps are the number of seconds since the Epoch as an int. + + +Diagnostics: + +Status code 0 -- successfully installed updates + 1 -- no newer upstream database found for last installed Lensfun + 3 -- no location was responsive; maybe network problems + +""" + +import urllib.request, shutil, sys, os, time, calendar, tarfile, json, glob +import lensfun + + +database_version = lensfun.get_database_version() + +lensfun_updates_dir = "/var/lib/lensfun-updates" +if os.name == "posix" and os.geteuid() != 0: + lensfun_updates_dir = os.path.join(os.path.expanduser("~"), + ".local", "share", "lensfun", "updates") + print("Info: root privileges needed for updating the system database.") + print("Info: updating user DB in '%s'" % lensfun_updates_dir) + + +def seconds_since_epoch(): + return calendar.timegm(time.gmtime()) + + +def detect_local_timestamp(version): + if version == database_version: + return lensfun.get_core_database()[0] + + def detect_single_timestamp(path): + try: + return int(open(os.path.join(path, "version_{}".format(version), "timestamp.txt")).read()) + except (FileNotFoundError, ValueError): + return 0 + + directory_candidates = {"/usr/share/lensfun", "/usr/local/share/lensfun", + "/var/lib/lensfun-updates", os.path.expanduser("~/.local/share/lensfun/updates")} + return max(map(detect_single_timestamp, directory_candidates)) + + +class Location: + + def __init__(self, base_url, version, timestamp): + self.base_url, self.version, self.timestamp = base_url, version, timestamp + + def __lt__(self, other): + return self.timestamp < other.timestamp + + def extract(self, directory): + tar = tarfile.open(fileobj=urllib.request.urlopen(self.base_url + "version_{}.tar.bz2".format(self.version)), + mode="r|*") + tar.extractall(directory) + tar.close() + + +def detect_local_database_versions(): + versions = {database_version} + for directory in glob.glob("/usr/share/lensfun/version_*") + glob.glob("/usr/local/share/lensfun/version_*"): + if os.path.isdir(directory): + try: + versions.add(int(directory.partition("version_")[2])) + except ValueError: + pass + return versions +local_database_versions = detect_local_database_versions() + + +locations = {version: set() for version in local_database_versions} +local_timestamps = {version: detect_local_timestamp(version) for version in local_database_versions} +seen_urls = set() +at_least_one_responsive_location = False + +def read_location(base_url): + global at_least_one_responsive_location + if base_url not in seen_urls and len(seen_urls) < 50: + seen_urls.add(base_url) + print("Reading {} …".format(base_url + "versions.json")) + try: + response = urllib.request.urlopen(base_url + "versions.json") + except (urllib.error.HTTPError, ValueError): + print(" Error: URL could not be opened.") + else: + try: + timestamp, versions, alternatives = json.loads(response.read().decode("utf-8")) + except ValueError: + print(" Error: Invalid data received.") + else: + at_least_one_responsive_location = True + versions = set(versions) & set(locations) + for version in versions: + if timestamp > local_timestamps[version]: + locations[version].add(Location(base_url, version, timestamp)) + for base_url in alternatives: + read_location(base_url) + +read_location("http://lensfun.sourceforge.net/db/") +read_location("http://wilson.bronger.org/lensfun-db/") + +if not at_least_one_responsive_location: + print("Fatal: No location was responsive. Network down?") + sys.exit(3) +elif not locations[database_version]: + print("Info: No newer database was found for last installed Lensfun.") + sys.exit(1) + + +for version, location_list in locations.items(): + try: + best_location = max(location_list) + except ValueError: + continue + updates_dir = os.path.join(lensfun_updates_dir, "version_{}".format(version)) + shutil.rmtree(updates_dir, ignore_errors=True) + os.makedirs(updates_dir) + best_location.extract(updates_dir) + open(os.path.join(updates_dir, "timestamp.txt"), "w").write(str(best_location.timestamp)) + print("Successfully updated the database in " + updates_dir + ".")