226 lines
7.9 KiB
Python
226 lines
7.9 KiB
Python
|
#!/usr/bin/env python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
""" Publish a ZIP file to the Kodi repository. """
|
||
|
|
||
|
# Based on code from https://github.com/xbmc/kodi-addon-submitter
|
||
|
|
||
|
from __future__ import absolute_import, division, unicode_literals
|
||
|
|
||
|
import logging
|
||
|
import os
|
||
|
import shutil
|
||
|
import subprocess
|
||
|
import sys
|
||
|
import time
|
||
|
import xml.etree.ElementTree as ET
|
||
|
from pprint import pformat
|
||
|
from tempfile import TemporaryDirectory
|
||
|
from zipfile import ZipFile
|
||
|
|
||
|
import requests
|
||
|
|
||
|
_LOGGER = logging.getLogger(__name__)
|
||
|
|
||
|
GH_REPO = 'repo-plugins'
|
||
|
GH_USERNAME = os.getenv('GH_USERNAME')
|
||
|
GH_TOKEN = os.getenv('GH_TOKEN')
|
||
|
GH_EMAIL = os.getenv('EMAIL')
|
||
|
|
||
|
|
||
|
def get_addon_info(xml: str):
|
||
|
""" Parse the passed addon.xml file and extract some information. """
|
||
|
tree = ET.fromstring(xml)
|
||
|
return {
|
||
|
'id': tree.get('id'),
|
||
|
'name': tree.get('name'),
|
||
|
'version': tree.get('version'),
|
||
|
'description': tree.find("./extension[@point='xbmc.addon.metadata']/description").text,
|
||
|
'news': tree.find("./extension[@point='xbmc.addon.metadata']/news").text,
|
||
|
'python': tree.find("./requires/import[@addon='xbmc.python']").get('version'),
|
||
|
'source': tree.find("./extension[@point='xbmc.addon.metadata']/source").text,
|
||
|
}
|
||
|
|
||
|
|
||
|
def user_fork_exists(repo, gh_username, gh_token):
|
||
|
""" Check if the user has a fork of the repository on Github. """
|
||
|
resp = requests.get(
|
||
|
'https://api.github.com/repos/{}/{}'.format(
|
||
|
gh_username,
|
||
|
repo
|
||
|
),
|
||
|
headers={'Accept': 'application/vnd.github.v3+json'},
|
||
|
params={
|
||
|
'type': 'all'
|
||
|
},
|
||
|
auth=(gh_username, gh_token)
|
||
|
)
|
||
|
resp_json = resp.json()
|
||
|
return resp.ok and resp_json.get('fork')
|
||
|
|
||
|
|
||
|
def create_personal_fork(repo, gh_username, gh_token):
|
||
|
"""Create a personal fork for the official repo on GitHub. """
|
||
|
resp = requests.post(
|
||
|
'https://api.github.com/repos/xbmc/{}/forks'.format(
|
||
|
repo
|
||
|
),
|
||
|
headers={'Accept': 'application/vnd.github.v3+json'},
|
||
|
auth=(gh_username, gh_token)
|
||
|
)
|
||
|
if resp.ok:
|
||
|
elapsed_time = 0
|
||
|
while elapsed_time < 5 * 60:
|
||
|
if not user_fork_exists(repo, gh_username, gh_token):
|
||
|
time.sleep(20)
|
||
|
elapsed_time += 20
|
||
|
else:
|
||
|
return
|
||
|
raise Exception("Timeout waiting for fork creation exceeded")
|
||
|
raise Exception('GitHub API error: {}\n{}'.format(resp.status_code, resp.text))
|
||
|
|
||
|
|
||
|
def shell(*args):
|
||
|
""" Execute a shell command. """
|
||
|
subprocess.run(args, check=True)
|
||
|
|
||
|
|
||
|
def create_addon_branch(repo, branch, source, addon_info, gh_username, gh_token, gh_email):
|
||
|
""" Create and addon branch in your fork of the respective addon repo. """
|
||
|
cur_dir = os.getcwd()
|
||
|
os.chdir('dist')
|
||
|
|
||
|
local_branch_name = '{}@{}'.format(addon_info['id'], branch)
|
||
|
|
||
|
if os.path.isdir(repo):
|
||
|
# We already have a checked out repo locally, update this with upstream code
|
||
|
os.chdir(repo)
|
||
|
shell('git', 'reset', '--hard') # Remove all local changes
|
||
|
shell('git', 'remote', 'set-branches', '--add', 'upstream', branch) # Make sure the upstream branch exists
|
||
|
shell('git', 'fetch', '-f', 'upstream', branch) # Fetch upstream
|
||
|
else:
|
||
|
# Clone the upstream repo
|
||
|
shell('git', 'clone', '--branch', branch, '--origin', 'upstream', '--single-branch', 'git://github.com/xbmc/{}.git'.format(repo))
|
||
|
os.chdir(repo)
|
||
|
|
||
|
# Create local branch
|
||
|
shell('git', 'checkout', '-B', local_branch_name, 'upstream/{}'.format(branch))
|
||
|
|
||
|
# Remove current code
|
||
|
if os.path.isdir(addon_info['id']):
|
||
|
shutil.rmtree(addon_info['id'], ignore_errors=False)
|
||
|
|
||
|
# Add new code
|
||
|
shutil.copytree(source, addon_info['id'])
|
||
|
shell('git', 'add', '--', addon_info['id'])
|
||
|
shell('git', 'status')
|
||
|
|
||
|
# Create a commit with the new code
|
||
|
shell('git', 'config', 'user.name', gh_username)
|
||
|
shell('git', 'config', 'user.email', gh_email)
|
||
|
shell('git', 'commit', '-m', '[{}] {}'.format(addon_info['id'], addon_info['version']))
|
||
|
|
||
|
# Push branch to fork
|
||
|
shell('git', 'push', '-f', 'https://{}:{}@github.com/{}/{}.git'.format(gh_username, gh_token, gh_username, repo), local_branch_name)
|
||
|
|
||
|
# Restore working directory
|
||
|
os.chdir(cur_dir)
|
||
|
|
||
|
|
||
|
def create_pull_request(repo, branch, addon_info, gh_username, gh_token):
|
||
|
""" Create a pull request in the official repo on GitHub. """
|
||
|
|
||
|
local_branch_name = '{}@{}'.format(addon_info['id'], branch)
|
||
|
|
||
|
# Check if pull request already exists.
|
||
|
resp = requests.get(
|
||
|
'https://api.github.com/repos/xbmc/{}/pulls'.format(repo),
|
||
|
params={
|
||
|
'head': '{}:{}'.format(gh_username, local_branch_name),
|
||
|
'base': branch,
|
||
|
},
|
||
|
headers={'Accept': 'application/vnd.github.v3+json'},
|
||
|
auth=(gh_username, gh_token)
|
||
|
)
|
||
|
|
||
|
if resp.status_code == 200 and not resp.json():
|
||
|
# Create a new Pull Request
|
||
|
template = """### Description
|
||
|
|
||
|
- **General**
|
||
|
- Add-on name: {name}
|
||
|
- Add-on ID: {id}
|
||
|
- Version number: {version}
|
||
|
- Kodi/repository version: {kodi_repo_branch}
|
||
|
|
||
|
- **Code location**
|
||
|
- URL: {source}
|
||
|
|
||
|
{description}
|
||
|
|
||
|
### What's new
|
||
|
|
||
|
{news}
|
||
|
|
||
|
### Checklist:
|
||
|
- [X] My code follows the [add-on rules](http://kodi.wiki/view/Add-on_rules) and [piracy stance](http://kodi.wiki/view/Official:Forum_rules#Piracy_Policy) of this project.
|
||
|
- [X] I have read the [CONTRIBUTING](https://github.com/xbmc/repo-plugins/blob/master/CONTRIBUTING.md) document
|
||
|
- [X] Each add-on submission should be a single commit with using the following style: [plugin.video.foo] v1.0.0
|
||
|
"""
|
||
|
pr_body = template.format(
|
||
|
name=addon_info['name'],
|
||
|
id=addon_info['id'],
|
||
|
version=addon_info['version'],
|
||
|
kodi_repo_branch=branch,
|
||
|
source=addon_info['source'],
|
||
|
description=addon_info['description'],
|
||
|
news=addon_info['news']
|
||
|
)
|
||
|
resp = requests.post(
|
||
|
'https://api.github.com/repos/xbmc/{}/pulls'.format(repo),
|
||
|
json={
|
||
|
'title': '[{}] {}'.format(local_branch_name, addon_info['version']),
|
||
|
'head': '{}:{}'.format(gh_username, local_branch_name),
|
||
|
'base': branch,
|
||
|
'body': pr_body,
|
||
|
'maintainer_can_modify': True,
|
||
|
},
|
||
|
headers={'Accept': 'application/vnd.github.v3+json'},
|
||
|
auth=(gh_username, gh_token)
|
||
|
)
|
||
|
if resp.status_code != 201:
|
||
|
raise Exception('GitHub API error: {}\n{}'.format(resp.status_code, pformat(resp.json())))
|
||
|
|
||
|
elif resp.status_code == 200 and resp.json():
|
||
|
_LOGGER.info('Pull request in {} for {}:{} already exists.'.format(branch, gh_username, local_branch_name))
|
||
|
|
||
|
else:
|
||
|
raise Exception('Unexpected GitHub error: {}\n{}'.format(resp.status_code, pformat(resp.json())))
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
filenames = sys.argv[1:]
|
||
|
|
||
|
for filename in filenames:
|
||
|
# Fork the repo if the user does not have a personal repo fork
|
||
|
if not user_fork_exists(GH_REPO, GH_USERNAME, GH_TOKEN):
|
||
|
create_personal_fork(GH_REPO, GH_USERNAME, GH_TOKEN)
|
||
|
|
||
|
with TemporaryDirectory() as extract_dir:
|
||
|
with ZipFile(filename) as z:
|
||
|
# Look for addon.xml in zip and load the details
|
||
|
xmlfile = next(f.filename for f in z.filelist if f.filename.endswith('addon.xml'))
|
||
|
addon_info = get_addon_info(z.read(xmlfile).decode('utf-8'))
|
||
|
if addon_info['python'] != '3.0.0':
|
||
|
branch = 'leia'
|
||
|
else:
|
||
|
branch = 'matrix'
|
||
|
|
||
|
# Extract the ZIP file to the extract_dir
|
||
|
z.extractall(extract_dir)
|
||
|
|
||
|
# Checkout the fork locally and create a branch with our new code from the extract_dir
|
||
|
create_addon_branch(GH_REPO, branch, os.path.join(extract_dir, addon_info['id']), addon_info, GH_USERNAME, GH_TOKEN, GH_EMAIL)
|
||
|
|
||
|
# Create pull request
|
||
|
create_pull_request(GH_REPO, branch, addon_info, GH_USERNAME, GH_TOKEN)
|