diff --git a/addons.xml b/addons.xml
index 0fd9968..41944b3 100644
--- a/addons.xml
+++ b/addons.xml
@@ -1,15 +1,17 @@
-
+
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository.midarr/addons.xml
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository.midarr/addons.xml.md5
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository.midarr/
+ false
-
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/
+
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/plugin.video.midarr/addons.xml
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/plugin.video.midarr/addons.xml.md5
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/plugin.video.midarr/
+ false
@@ -20,4 +22,21 @@
icon.png
+
+
+
+
+
+ video
+
+
+ Midarr
+ Official Midarr for Kodi add-on
+ https://github.com/midarrlabs/repository.midarr
+
+
+ icon.png
+ fanart.png
+
+
\ No newline at end of file
diff --git a/addons.xml.md5 b/addons.xml.md5
index 5e7e8eb..815f3c4 100644
--- a/addons.xml.md5
+++ b/addons.xml.md5
@@ -1 +1 @@
-dd4e4b3079c8b4a87bc32dfa422f19bd
\ No newline at end of file
+7324f571bd6df1ad4b886a5f1fd67d73 addons.xml
diff --git a/nexus/zips/addons.xml b/nexus/zips/addons.xml
deleted file mode 100644
index 29192f7..0000000
--- a/nexus/zips/addons.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
- video
-
-
- Midarr
- Official Midarr for Kodi add-on
- https://github.com/midarrlabs/repository.midarr
-
-
- icon.png
-
-
-
\ No newline at end of file
diff --git a/nexus/zips/addons.xml.md5 b/nexus/zips/addons.xml.md5
deleted file mode 100644
index c7ac981..0000000
--- a/nexus/zips/addons.xml.md5
+++ /dev/null
@@ -1 +0,0 @@
-2e5773f5f179b703e8336768e6bcad99
\ No newline at end of file
diff --git a/nexus/zips/plugin.video.midarr/addon.xml b/nexus/zips/plugin.video.midarr/addon.xml
deleted file mode 100644
index 644bd83..0000000
--- a/nexus/zips/plugin.video.midarr/addon.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
- video
-
-
- Midarr
- Official Midarr for Kodi add-on
- https://github.com/midarrlabs/repository.midarr
-
-
- icon.png
-
-
-
diff --git a/nexus/plugin.video.midarr/addon.py b/plugin.video.midarr/addon.py
similarity index 100%
rename from nexus/plugin.video.midarr/addon.py
rename to plugin.video.midarr/addon.py
diff --git a/nexus/plugin.video.midarr/addon.xml b/plugin.video.midarr/addon.xml
similarity index 95%
rename from nexus/plugin.video.midarr/addon.xml
rename to plugin.video.midarr/addon.xml
index 644bd83..2447713 100644
--- a/nexus/plugin.video.midarr/addon.xml
+++ b/plugin.video.midarr/addon.xml
@@ -13,6 +13,7 @@
icon.png
+ fanart.png
diff --git a/plugin.video.midarr/fanart.jpg b/plugin.video.midarr/fanart.jpg
new file mode 100644
index 0000000..c3fadbd
Binary files /dev/null and b/plugin.video.midarr/fanart.jpg differ
diff --git a/nexus/plugin.video.midarr/logo.png b/plugin.video.midarr/icon.png
similarity index 100%
rename from nexus/plugin.video.midarr/logo.png
rename to plugin.video.midarr/icon.png
diff --git a/nexus/zips/plugin.video.midarr/plugin.video.midarr-1.0.0.zip b/plugin.video.midarr/plugin.video.midarr-1.0.0.zip
similarity index 57%
rename from nexus/zips/plugin.video.midarr/plugin.video.midarr-1.0.0.zip
rename to plugin.video.midarr/plugin.video.midarr-1.0.0.zip
index c8a7a89..362da90 100644
Binary files a/nexus/zips/plugin.video.midarr/plugin.video.midarr-1.0.0.zip and b/plugin.video.midarr/plugin.video.midarr-1.0.0.zip differ
diff --git a/plugin.video.midarr/plugin.video.midarr-1.0.0.zip.md5 b/plugin.video.midarr/plugin.video.midarr-1.0.0.zip.md5
new file mode 100644
index 0000000..0bcbfdd
--- /dev/null
+++ b/plugin.video.midarr/plugin.video.midarr-1.0.0.zip.md5
@@ -0,0 +1 @@
+d06c112b4738338a4e507deb9519a1cb *plugin.video.midarr-1.0.0.zip
diff --git a/nexus/plugin.video.midarr/resources/settings.xml b/plugin.video.midarr/resources/settings.xml
similarity index 100%
rename from nexus/plugin.video.midarr/resources/settings.xml
rename to plugin.video.midarr/resources/settings.xml
diff --git a/repo_generator.py b/repo_generator.py
index b25857b..9028933 100644
--- a/repo_generator.py
+++ b/repo_generator.py
@@ -1,378 +1,465 @@
+#!/usr/bin/env python
+r"""
+Create a Kodi add-on repository from add-on sources
+
+This tool extracts Kodi add-ons from their respective locations and
+copies the appropriate files into a Kodi add-on repository. Each add-on
+is placed in its own directory. Each contains the add-on metadata files
+and a zip archive. In addition, the repository catalog "addons.xml" is
+placed in the repository folder.
+
+Each add-on location is either a local path or a URL. If it is a local
+path, it can be to either an add-on folder or an add-on ZIP archive. If
+it is a URL, it should be to a Git repository and it should use the
+format:
+ REPOSITORY_URL#BRANCH:PATH
+The first segment is the Git URL that would be used to clone the
+repository, (e.g.,
+"https://github.com/chadparry/kodi-repository.chad.parry.org.git").
+That is followed by an optional "#" sign and a branch or tag name,
+(e.g. "release-1.0"). If no branch name is specified, then the default
+is the repository's currently active branch, which is the same default
+as git-clone. Next comes an optional ":" sign and path. The path
+denotes the location of the add-on within the repository. If no path is
+specified, then the default is ".".
+
+For example, if you are in the directory that should contain addons.xml
+and you just copied a new version of the only add-on
+"repository.chad.parry.org" to a subdirectory, then you can create or
+update the addons.xml file with this command:
+
+ create_repository.py repository.chad.parry.org
+
+As another example, here is the command that generates Chad Parry's
+Repository:
+
+ create_repository.py \
+ --datadir=~/html/software/kodi \
+ --compressed \
+ https://github.com/chadparry\
+/kodi-repository.chad.parry.org.git:repository.chad.parry.org \
+ https://github.com/chadparry\
+/kodi-plugin.program.remote.control.browser.git\
+:plugin.program.remote.control.browser
+
+This script has been tested with Python 2.7.10 and Python 3.6.5. It
+depends on the GitPython module.
"""
- Put this script in the root folder of your repo and it will
- zip up all addon folders, create a new zip in your zips folder
- and then update the md5 and addons.xml file
-"""
+__author__ = "Chad Parry"
+__contact__ = "github@chad.parry.org"
+__copyright__ = "Copyright 2016-2022 Chad Parry"
+__license__ = "GNU GENERAL PUBLIC LICENSE. Version 2, June 1991"
+__version__ = "2.3.8"
+
+
+import argparse
+import collections
+import errno
+import gzip
import hashlib
+import io
import os
+import re
import shutil
+import stat
import sys
+import tempfile
+import threading
+import xml.etree.ElementTree
import zipfile
-from xml.etree import ElementTree
-
-SCRIPT_VERSION = 5
-KODI_VERSIONS = ["nexus", "repository"]
-IGNORE = [
- ".git",
- ".github",
- ".gitignore",
- ".DS_Store",
- "thumbs.db",
- ".idea",
- "venv",
-]
-_COLOR_ESCAPE = "\x1b[{}m"
-_COLORS = {
- "black": "30",
- "red": "31",
- "green": "4;32",
- "yellow": "3;33",
- "blue": "34",
- "magenta": "35",
- "cyan": "1;36",
- "grey": "37",
- "endc": "0",
-}
-
-
-def _setup_colors():
- """
- Return True if the running system's terminal supports color,
- and False otherwise.
- """
-
- def vt_codes_enabled_in_windows_registry():
- """
- Check the Windows registry to see if VT code handling has been enabled by default.
- """
- try:
- import winreg
- except:
- return False
- else:
- reg_key = winreg.OpenKey(
- winreg.HKEY_CURRENT_USER, "Console", access=winreg.KEY_ALL_ACCESS
- )
- try:
- reg_key_value, _ = winreg.QueryValueEx(reg_key, "VirtualTerminalLevel")
- except FileNotFoundError:
- try:
- winreg.SetValueEx(
- reg_key, "VirtualTerminalLevel", 0, winreg.KEY_DWORD, 1
- )
- except:
- return False
- else:
- reg_key_value, _ = winreg.QueryValueEx(
- reg_key, "VirtualTerminalLevel"
- )
- else:
- return reg_key_value == 1
-
- def is_a_tty():
- return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
-
- def legacy_support():
- console = 0
- color = 0
- if sys.platform in ["linux", "linux2", "darwin"]:
- pass
- elif sys.platform == "win32":
- color = os.system("color")
-
- from ctypes import windll
-
- k = windll.kernel32
- console = k.SetConsoleMode(k.GetStdHandle(-11), 7)
-
- return any([color == 1, console == 1])
-
- return any(
- [
- is_a_tty(),
- sys.platform != "win32",
- "ANSICON" in os.environ,
- "WT_SESSION" in os.environ,
- os.environ.get("TERM_PROGRAM") == "vscode",
- vt_codes_enabled_in_windows_registry(),
- legacy_support(),
- ]
- )
-
-
-_SUPPORTS_COLOR = _setup_colors()
-
-
-def color_text(text, color):
- """
- Return an ANSI-colored string, if supported.
- """
-
- return (
- '{}{}{}'.format(
- _COLOR_ESCAPE.format(_COLORS[color]),
- text,
- _COLOR_ESCAPE.format(_COLORS["endc"]),
- )
- if _SUPPORTS_COLOR
- else text
- )
-
-
-def convert_bytes(num):
- """
- this function will convert bytes to MB.... GB... etc
- """
- for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
- if num < 1024.0:
- return "%3.1f %s" % (num, x)
- num /= 1024.0
-
-
-class Generator:
- """
- Generates a new addons.xml file from each addons addon.xml file
- and a new addons.xml.md5 hash file. Must be run from the root of
- the checked-out repo.
- """
-
- def __init__(self, release):
- self.release_path = release
- self.zips_path = os.path.join(self.release_path, "zips")
- addons_xml_path = os.path.join(self.zips_path, "addons.xml")
- md5_path = os.path.join(self.zips_path, "addons.xml.md5")
-
- if not os.path.exists(self.zips_path):
- os.makedirs(self.zips_path)
-
- self._remove_binaries()
-
- if self._generate_addons_file(addons_xml_path):
- print(
- "Successfully updated {}".format(color_text(addons_xml_path, 'yellow'))
- )
-
- if self._generate_md5_file(addons_xml_path, md5_path):
- print("Successfully updated {}".format(color_text(md5_path, 'yellow')))
-
- def _remove_binaries(self):
- """
- Removes any and all compiled Python files before operations.
- """
-
- for parent, dirnames, filenames in os.walk(self.release_path):
- for fn in filenames:
- if fn.lower().endswith("pyo") or fn.lower().endswith("pyc"):
- compiled = os.path.join(parent, fn)
- try:
- os.remove(compiled)
- print(
- "Removed compiled python file: {}".format(
- color_text(compiled, 'green')
- )
- )
- except:
- print(
- "Failed to remove compiled python file: {}".format(
- color_text(compiled, 'red')
- )
- )
- for dir in dirnames:
- if "pycache" in dir.lower():
- compiled = os.path.join(parent, dir)
- try:
- shutil.rmtree(compiled)
- print(
- "Removed __pycache__ cache folder: {}".format(
- color_text(compiled, 'green')
- )
- )
- except:
- print(
- "Failed to remove __pycache__ cache folder: {}".format(
- color_text(compiled, 'red')
- )
- )
-
- def _create_zip(self, folder, addon_id, version):
- """
- Creates a zip file in the zips directory for the given addon.
- """
- addon_folder = os.path.join(self.release_path, folder)
- zip_folder = os.path.join(self.zips_path, addon_id)
- if not os.path.exists(zip_folder):
- os.makedirs(zip_folder)
-
- final_zip = os.path.join(zip_folder, "{0}-{1}.zip".format(addon_id, version))
- if not os.path.exists(final_zip):
- zip = zipfile.ZipFile(final_zip, "w", compression=zipfile.ZIP_DEFLATED)
- root_len = len(os.path.dirname(os.path.abspath(addon_folder)))
-
- for root, dirs, files in os.walk(addon_folder):
- # remove any unneeded artifacts
- for i in IGNORE:
- if i in dirs:
- try:
- dirs.remove(i)
- except:
- pass
- for f in files:
- if f.startswith(i):
- try:
- files.remove(f)
- except:
- pass
-
- archive_root = os.path.abspath(root)[root_len:]
-
- for f in files:
- fullpath = os.path.join(root, f)
- archive_name = os.path.join(archive_root, f)
- zip.write(fullpath, archive_name, zipfile.ZIP_DEFLATED)
-
- zip.close()
- size = convert_bytes(os.path.getsize(final_zip))
- print(
- "Zip created for {} ({}) - {}".format(
- color_text(addon_id, 'cyan'),
- color_text(version, 'green'),
- color_text(size, 'yellow'),
- )
- )
-
- def _copy_meta_files(self, addon_id, addon_folder):
- """
- Copy the addon.xml and relevant art files into the relevant folders in the repository.
- """
-
- tree = ElementTree.parse(os.path.join(self.release_path, addon_id, "addon.xml"))
- root = tree.getroot()
-
- copyfiles = ["addon.xml"]
- for ext in root.findall("extension"):
- if ext.get("point") in ["xbmc.addon.metadata", "kodi.addon.metadata"]:
- assets = ext.find("assets")
- if not assets:
- continue
- for art in [a for a in assets if a.text]:
- copyfiles.append(os.path.normpath(art.text))
-
- src_folder = os.path.join(self.release_path, addon_id)
- for file in copyfiles:
- addon_path = os.path.join(src_folder, file)
- if not os.path.exists(addon_path):
- continue
-
- zips_path = os.path.join(addon_folder, file)
- asset_path = os.path.split(zips_path)[0]
- if not os.path.exists(asset_path):
- os.makedirs(asset_path)
-
- shutil.copy(addon_path, zips_path)
- def _generate_addons_file(self, addons_xml_path):
- """
- Generates a zip for each found addon, and updates the addons.xml file accordingly.
- """
- if not os.path.exists(addons_xml_path):
- addons_root = ElementTree.Element('addons')
- addons_xml = ElementTree.ElementTree(addons_root)
- else:
- addons_xml = ElementTree.parse(addons_xml_path)
- addons_root = addons_xml.getroot()
-
- folders = [
- i
- for i in os.listdir(self.release_path)
- if os.path.isdir(os.path.join(self.release_path, i))
- and i != "zips"
- and not i.startswith(".")
- and os.path.exists(os.path.join(self.release_path, i, "addon.xml"))
- ]
-
- addon_xpath = "addon[@id='{}']"
- changed = False
- for addon in folders:
- try:
- addon_xml_path = os.path.join(self.release_path, addon, "addon.xml")
- addon_xml = ElementTree.parse(addon_xml_path)
- addon_root = addon_xml.getroot()
- id = addon_root.get('id')
- version = addon_root.get('version')
-
- updated = False
- addon_entry = addons_root.find(addon_xpath.format(id))
- if addon_entry is not None and addon_entry.get('version') != version:
- index = addons_root.findall('addon').index(addon_entry)
- addons_root.remove(addon_entry)
- addons_root.insert(index, addon_root)
- updated = True
- changed = True
- elif addon_entry is None:
- addons_root.append(addon_root)
- updated = True
- changed = True
-
- if updated:
- # Create the zip files
- self._create_zip(addon, id, version)
- self._copy_meta_files(addon, os.path.join(self.zips_path, id))
- except Exception as e:
- print(
- "Excluding {}: {}".format(
- color_text(addon, 'yellow'), color_text(e, 'red')
- )
- )
-
- if changed:
- addons_root[:] = sorted(addons_root, key=lambda addon: addon.get('id'))
+AddonMetadata = collections.namedtuple(
+ 'AddonMetadata', ('id', 'version', 'root'))
+WorkerResult = collections.namedtuple(
+ 'WorkerResult', ('addon_metadata', 'exc_info'))
+AddonWorker = collections.namedtuple('AddonWorker', ('thread', 'result_slot'))
+
+
+INFO_BASENAME = 'addon.xml'
+METADATA_BASENAMES = (
+ INFO_BASENAME,
+ 'icon.png',
+ 'fanart.jpg',
+ 'LICENSE.txt')
+
+
+# The specification for version numbers is at http://semver.org/.
+# The Kodi documentation at
+# http://kodi.wiki/index.php?title=Addon.xml&oldid=128873#How_versioning_works
+# adds a twist by recommending a tilde instead of a hyphen.
+VERSION_PATTERN = (r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
+ r'(?:[-~]((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)'
+ r'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?'
+ r'(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$')
+
+
+def get_archive_basename(addon_metadata):
+ return '{}-{}.zip'.format(addon_metadata.id, addon_metadata.version)
+
+
+def get_metadata_basenames(addon_metadata):
+ return ([(basename, basename) for basename in METADATA_BASENAMES] +
+ [(
+ 'changelog.txt',
+ 'changelog-{}.txt'.format(addon_metadata.version))])
+
+
+def is_url(addon_location):
+ return bool(re.match(r'[A-Za-z0-9+.-]+://.', addon_location))
+
+
+def get_posix_path(path):
+ return path.replace(os.path.sep, '/')
+
+
+def samefile(a, b):
+ try:
+ return os.path.samefile(a, b)
+ except AttributeError:
+ return (os.path.normcase(os.path.normpath(a)) ==
+ os.path.normcase(os.path.normpath(b)))
+
+
+def parse_metadata(metadata_file):
+ # Parse the addon.xml metadata.
+ try:
+ tree = xml.etree.ElementTree.parse(metadata_file)
+ except IOError:
+ raise RuntimeError(
+ 'Cannot open add-on metadata: {}'.format(metadata_file))
+ root = tree.getroot()
+ addon_metadata = AddonMetadata(
+ root.get('id'),
+ root.get('version'),
+ root)
+ # Validate the add-on ID.
+ if (addon_metadata.id is None or
+ re.search(r'[^a-z0-9._-]', addon_metadata.id)):
+ raise RuntimeError('Invalid add-on ID: {}'.format(addon_metadata.id))
+ if (addon_metadata.version is None or
+ not re.match(VERSION_PATTERN, addon_metadata.version)):
+ raise RuntimeError(
+ 'Invalid add-on verson: {}'.format(addon_metadata.version))
+ return addon_metadata
+
+
+def generate_checksum(archive_path, is_binary=True, checksum_path_opt=None):
+ checksum_path = ('{}.md5'.format(archive_path)
+ if checksum_path_opt is None else checksum_path_opt)
+ checksum_dirname = os.path.dirname(checksum_path)
+ archive_relpath = os.path.relpath(archive_path, checksum_dirname)
+
+ checksum = hashlib.md5()
+ with open(archive_path, 'rb') as archive_contents:
+ for chunk in iter(lambda: archive_contents.read(2**12), b''):
+ checksum.update(chunk)
+ digest = checksum.hexdigest()
+
+ binary_marker = '*' if is_binary else ' '
+ # Force a UNIX line ending, like the md5sum utility.
+ with io.open(checksum_path, 'w', newline='\n') as sig:
+ sig.write(u'{} {}{}\n'.format(digest, binary_marker, archive_relpath))
+
+
+def copy_metadata_files(source_folder, addon_target_folder, addon_metadata):
+ for (source_basename, target_basename) in get_metadata_basenames(
+ addon_metadata):
+ source_path = os.path.join(source_folder, source_basename)
+ if os.path.isfile(source_path):
+ shutil.copyfile(
+ source_path,
+ os.path.join(addon_target_folder, target_basename))
+
+
+def on_remove_error(function, path, excinfo):
+ exc_info_value = excinfo[1]
+ if (hasattr(exc_info_value, 'errno') and
+ exc_info_value.errno == errno.EACCES):
+ os.chmod(path, stat.S_IWUSR)
+ function(path)
+ else:
+ raise
+
+
+def fetch_addon_from_git(addon_location, target_folder):
+ # Parse the format "REPOSITORY_URL#BRANCH:PATH". The colon is a delimiter
+ # unless it looks more like a scheme, (e.g., "http://").
+ match = re.match(
+ r'((?:[A-Za-z0-9+.-]+://)?.*?)(?:#([^#]*?))?(?::([^:]*))?$',
+ addon_location)
+ (clone_repo, clone_branch, clone_path_option) = match.groups()
+ clone_path = (get_posix_path(os.path.join('.', ''))
+ if clone_path_option is None else clone_path_option)
+
+ # Create a temporary folder for the git clone.
+ clone_folder = tempfile.mkdtemp('-repo')
+ try:
+ # Check out the sources.
+ cloned = git.Repo.clone_from(clone_repo, clone_folder)
+ if clone_branch is not None:
+ cloned.git.checkout(clone_branch)
+ clone_source_folder = os.path.join(clone_folder, clone_path)
+
+ metadata_path = os.path.join(clone_source_folder, INFO_BASENAME)
+ addon_metadata = parse_metadata(metadata_path)
+ addon_target_folder = os.path.join(target_folder, addon_metadata.id)
+
+ # Create the compressed add-on archive.
+ if not os.path.isdir(addon_target_folder):
+ os.mkdir(addon_target_folder)
+ archive_path = os.path.join(
+ addon_target_folder, get_archive_basename(addon_metadata))
+ with open(archive_path, 'wb') as archive:
+ cloned.archive(
+ archive,
+ treeish='HEAD:{}'.format(clone_path),
+ prefix=get_posix_path(os.path.join(addon_metadata.id, '')),
+ format='zip')
+ generate_checksum(archive_path)
+
+ copy_metadata_files(
+ clone_source_folder, addon_target_folder, addon_metadata)
+
+ return addon_metadata
+ finally:
+ shutil.rmtree(
+ clone_folder,
+ ignore_errors=False,
+ onerror=on_remove_error)
+
+
+def fetch_addon_from_folder(raw_addon_location, target_folder):
+ addon_location = os.path.expanduser(raw_addon_location)
+ metadata_path = os.path.join(addon_location, INFO_BASENAME)
+ addon_metadata = parse_metadata(metadata_path)
+ addon_target_folder = os.path.join(target_folder, addon_metadata.id)
+
+ # Create the compressed add-on archive.
+ if not os.path.isdir(addon_target_folder):
+ os.mkdir(addon_target_folder)
+ archive_path = os.path.join(
+ addon_target_folder, get_archive_basename(addon_metadata))
+ with zipfile.ZipFile(
+ archive_path,
+ 'w',
+ compression=zipfile.ZIP_DEFLATED,
+ ) as archive:
+ for (root, _, files) in os.walk(addon_location):
+ relative_root = os.path.join(
+ addon_metadata.id, os.path.relpath(root, addon_location))
+ archive.write(root, relative_root)
+ for relative_path in files:
+ source_path = os.path.realpath(
+ os.path.join(root, relative_path))
+ if source_path == archive_path:
+ # Don't include the archive within itself.
+ continue
+ archive.write(
+ source_path,
+ os.path.join(relative_root, relative_path))
+ generate_checksum(archive_path)
+
+ if not samefile(addon_location, addon_target_folder):
+ copy_metadata_files(
+ addon_location, addon_target_folder, addon_metadata)
+
+ return addon_metadata
+
+
+def fetch_addon_from_zip(raw_addon_location, target_folder):
+ addon_location = os.path.expanduser(raw_addon_location)
+ with zipfile.ZipFile(
+ addon_location,
+ compression=zipfile.ZIP_DEFLATED,
+ ) as archive:
+ # Find out the name of the archive's root folder.
+ roots = frozenset(
+ next(iter(path.split('/')), '')
+ for path in archive.namelist())
+ if len(roots) != 1:
+ raise RuntimeError('Archive should contain one directory')
+ root = next(iter(roots))
+
+ metadata_file = archive.open(
+ get_posix_path(os.path.join(root, INFO_BASENAME)))
+ addon_metadata = parse_metadata(metadata_file)
+ addon_target_folder = os.path.join(target_folder, addon_metadata.id)
+
+ # Copy the metadata files.
+ if not os.path.isdir(addon_target_folder):
+ os.mkdir(addon_target_folder)
+ for (source_basename, target_basename) in get_metadata_basenames(
+ addon_metadata):
try:
- addons_xml.write(
- addons_xml_path, encoding="utf-8", xml_declaration=True
- )
-
- return changed
- except Exception as e:
- print(
- "An error occurred updating {}!\n{}".format(
- color_text(addons_xml_path, 'yellow'), color_text(e, 'red')
- )
- )
-
- def _generate_md5_file(self, addons_xml_path, md5_path):
- """
- Generates a new addons.xml.md5 file.
- """
- try:
- with open(addons_xml_path, "r", encoding="utf-8") as f:
- m = hashlib.md5(f.read().encode("utf-8")).hexdigest()
- self._save_file(m, file=md5_path)
-
- return True
- except Exception as e:
- print(
- "An error occurred updating {}!\n{}".format(
- color_text(md5_path, 'yellow'), color_text(e, 'red')
- )
- )
-
- def _save_file(self, data, file):
- """
- Saves a file.
- """
+ source_file = archive.open(
+ get_posix_path(os.path.join(root, source_basename)))
+ except KeyError:
+ continue
+ with open(
+ os.path.join(addon_target_folder, target_basename),
+ 'wb',
+ ) as target_file:
+ shutil.copyfileobj(source_file, target_file)
+
+ # Copy the archive.
+ archive_basename = get_archive_basename(addon_metadata)
+ archive_path = os.path.join(addon_target_folder, archive_basename)
+ addon_source_folder = os.path.dirname(addon_location) or '.'
+ if (not samefile(addon_source_folder, addon_target_folder) or
+ os.path.basename(addon_location) != archive_basename):
+ shutil.copyfile(addon_location, archive_path)
+ generate_checksum(archive_path)
+
+ return addon_metadata
+
+
+def fetch_addon(addon_location, target_folder):
+ if is_url(addon_location):
+ addon_metadata = fetch_addon_from_git(addon_location, target_folder)
+ elif os.path.isdir(addon_location):
+ addon_metadata = fetch_addon_from_folder(addon_location, target_folder)
+ elif os.path.isfile(addon_location):
+ addon_metadata = fetch_addon_from_zip(addon_location, target_folder)
+ else:
+ raise RuntimeError('Path not found: {}'.format(addon_location))
+ return addon_metadata
+
+
+def fetch_addon_to_result_slot(addon_location, target_folder, result_slot):
+ try:
+ addon_metadata = fetch_addon(addon_location, target_folder)
+ result_slot.append(WorkerResult(addon_metadata, None))
+ except Exception:
+ result_slot.append(WorkerResult(None, sys.exc_info()))
+
+
+def get_addon_worker(addon_location, target_folder):
+ result_slot = []
+ thread = threading.Thread(target=lambda: fetch_addon_to_result_slot(
+ addon_location, target_folder, result_slot))
+ return AddonWorker(thread, result_slot)
+
+
+def create_repository(
+ addon_locations,
+ data_path,
+ info_path,
+ checksum_path,
+ is_compressed,
+ no_parallel):
+ # Import git lazily.
+ if any(is_url(addon_location) for addon_location in addon_locations):
try:
- with open(file, "w") as f:
- f.write(data)
- except Exception as e:
- print(
- "An error occurred saving {}!\n{}".format(
- color_text(file, 'yellow'), color_text(e, 'red')
- )
- )
+ global git
+ import git
+ except ImportError:
+ raise RuntimeError(
+ 'Please install GitPython: pip install gitpython')
+
+ # Create the target folder.
+ target_folder = os.path.realpath(data_path)
+ if not os.path.isdir(target_folder):
+ os.mkdir(target_folder)
+
+ if no_parallel or len(addon_locations) <= 1:
+ metadata = [
+ fetch_addon(addon_location, target_folder)
+ for addon_location in addon_locations]
+ else:
+ # Fetch all the add-on sources in parallel.
+ workers = [
+ get_addon_worker(addon_location, target_folder)
+ for addon_location in addon_locations]
+ for worker in workers:
+ worker.thread.start()
+ for worker in workers:
+ worker.thread.join()
+
+ # Collect the results from all the threads.
+ metadata = []
+ for worker in workers:
+ try:
+ result = next(iter(worker.result_slot))
+ except StopIteration:
+ raise RuntimeError('Add-on worker did not report result')
+ if result.exc_info is not None:
+ raise result.exc_info[1]
+ metadata.append(result.addon_metadata)
+
+ # Generate the addons.xml file.
+ root = xml.etree.ElementTree.Element('addons')
+ for addon_metadata in metadata:
+ root.append(addon_metadata.root)
+ tree = xml.etree.ElementTree.ElementTree(root)
+ if is_compressed:
+ info_file = gzip.open(info_path, 'wb')
+ else:
+ info_file = open(info_path, 'wb')
+ with info_file:
+ tree.write(info_file, encoding='UTF-8', xml_declaration=True)
+ is_binary = is_compressed
+ generate_checksum(info_path, is_binary, checksum_path)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Create a Kodi add-on repository from add-on sources')
+ parser.add_argument(
+ '--datadir',
+ '-d',
+ default='.',
+ help='Path to place the add-ons [current directory]')
+ parser.add_argument(
+ '--info',
+ '-i',
+ help='''Path for the addons.xml file [DATADIR/addons.xml or
+ DATADIR/addons.xml.gz if compressed]''')
+ parser.add_argument(
+ '--checksum',
+ '-c',
+ help='Path for the addons.xml.md5 file [INFO.md5]')
+ parser.add_argument(
+ '--compressed',
+ '-z',
+ action='store_true',
+ help='Compress addons.xml with gzip')
+ parser.add_argument(
+ '--no-parallel',
+ '-n',
+ action='store_true',
+ help='''Build add-on sources serially,
+ which also makes error diagnosis easier''')
+ parser.add_argument(
+ 'addon',
+ nargs='*',
+ metavar='ADDON',
+ help='''Location of the add-on: either a path to a local folder or
+ to a zip archive or a URL for a Git repository with the
+ format REPOSITORY_URL#BRANCH:PATH''')
+ args = parser.parse_args()
+
+ data_path = os.path.expanduser(args.datadir)
+ if args.info is None:
+ if args.compressed:
+ info_basename = 'addons.xml.gz'
+ else:
+ info_basename = 'addons.xml'
+ info_path = os.path.join(data_path, info_basename)
+ else:
+ info_path = os.path.expanduser(args.info)
+ checksum_path = (
+ os.path.expanduser(args.checksum) if args.checksum is not None
+ else '{}.md5'.format(info_path))
+ create_repository(
+ args.addon,
+ data_path,
+ info_path,
+ checksum_path,
+ args.compressed,
+ args.no_parallel)
if __name__ == "__main__":
- for release in [r for r in KODI_VERSIONS if os.path.exists(r)]:
- Generator(release)
\ No newline at end of file
+ main()
\ No newline at end of file
diff --git a/repository.midarr-1.0.0.zip b/repository.midarr-1.0.0.zip
index e36afcb..a11fb9b 100644
Binary files a/repository.midarr-1.0.0.zip and b/repository.midarr-1.0.0.zip differ
diff --git a/repository/zips/repository.midarr/addon.xml b/repository.midarr/addon.xml
similarity index 58%
rename from repository/zips/repository.midarr/addon.xml
rename to repository.midarr/addon.xml
index c586569..6330a21 100644
--- a/repository/zips/repository.midarr/addon.xml
+++ b/repository.midarr/addon.xml
@@ -2,14 +2,16 @@
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository.midarr/addons.xml
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository.midarr/addons.xml.md5
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository.midarr/
+ false
-
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/
+
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/plugin.video.midarr/addons.xml
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/plugin.video.midarr/addons.xml.md5
+ https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/plugin.video.midarr/
+ false
diff --git a/repository.midarr/fanart.jpg b/repository.midarr/fanart.jpg
new file mode 100644
index 0000000..c3fadbd
Binary files /dev/null and b/repository.midarr/fanart.jpg differ
diff --git a/repository/repository.midarr/logo.png b/repository.midarr/icon.png
similarity index 100%
rename from repository/repository.midarr/logo.png
rename to repository.midarr/icon.png
diff --git a/repository/zips/repository.midarr/repository.midarr-1.0.0.zip b/repository.midarr/repository.midarr-1.0.0.zip
similarity index 53%
rename from repository/zips/repository.midarr/repository.midarr-1.0.0.zip
rename to repository.midarr/repository.midarr-1.0.0.zip
index 33f4f13..a11fb9b 100644
Binary files a/repository/zips/repository.midarr/repository.midarr-1.0.0.zip and b/repository.midarr/repository.midarr-1.0.0.zip differ
diff --git a/repository.midarr/repository.midarr-1.0.0.zip.md5 b/repository.midarr/repository.midarr-1.0.0.zip.md5
new file mode 100644
index 0000000..d6669cf
--- /dev/null
+++ b/repository.midarr/repository.midarr-1.0.0.zip.md5
@@ -0,0 +1 @@
+2cac180d3eb81cbe7ed1b9b1063e59c3 *repository.midarr-1.0.0.zip
diff --git a/repository/repository.midarr/addon.xml b/repository/repository.midarr/addon.xml
deleted file mode 100644
index c586569..0000000
--- a/repository/repository.midarr/addon.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/
-
-
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/
-
-
-
- Kodi repository for Midarr addons
- Kodi repository for Midarr addons
- all
-
- icon.png
-
-
-
\ No newline at end of file
diff --git a/repository/zips/addons.xml b/repository/zips/addons.xml
deleted file mode 100644
index 0fd9968..0000000
--- a/repository/zips/addons.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/repository/zips/
-
-
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/addons.xml.md5
- https://raw.githubusercontent.com/midarrlabs/repository.midarr/main/nexus/zips/
-
-
-
- Kodi repository for Midarr addons
- Kodi repository for Midarr addons
- all
-
- icon.png
-
-
-
\ No newline at end of file
diff --git a/repository/zips/addons.xml.md5 b/repository/zips/addons.xml.md5
deleted file mode 100644
index 5e7e8eb..0000000
--- a/repository/zips/addons.xml.md5
+++ /dev/null
@@ -1 +0,0 @@
-dd4e4b3079c8b4a87bc32dfa422f19bd
\ No newline at end of file