diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c6c035
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/.idea
+/build
+/*.egg-info
+/.pytest_cache
+__pycache__
+/venv
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..185d421
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,34 @@
+
+SOURCES=$(shell find . -name *.py)
+OUTPUT_PATH=build/plugin.video.ipfs
+
+venv:
+ ( \
+ virtualenv --python=python3.7 venv
+ source venv/bin/activate; \
+ pip install -r requirements.txt; \
+ )
+
+test: install venv $(SOURCES)
+ venv/bin/py.test
+
+install:
+ venv/bin/python setup.py develop
+
+clean:
+ rm -rf build
+
+build/plugin_video_ipfs.zip: build
+ cd build && zip -r plugin_video_ipfs.zip plugin.video.ipfs
+
+build: $(SOURCES) fanart.jpg icon.png addon.xml
+ mkdir -p $(OUTPUT_PATH)/ipfs
+ cp -r src/*.py $(OUTPUT_PATH)
+ cp -r src/ipfs/*.py $(OUTPUT_PATH)/ipfs
+ cp -r resources $(OUTPUT_PATH)
+ cp icon.png $(OUTPUT_PATH)
+ cp addon.xml $(OUTPUT_PATH)
+ cp fanart.jpg $(OUTPUT_PATH)
+
+package: build/plugin_video_ipfs.zip
+ unzip -t build/plugin_video_ipfs.zip
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f8fc640
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# IPFS video plugin for Kodi
+
+Plugin to browse and play video media from IPFS.
+
+It has a configurable gateway and a configurable root CID that you can browse.
+
+# Build from source
+
+- `make package`
+- Copy the `build/plugin_video_ipfs.zip`
+
+# Viewing your own content
+
+- Install IPFS on your media player
+- Use `ipfs add -r -w yourdirectory` to insert your directory
+- Configure plugin to have gateway point to `http://localhost:8080`
+- Use the hash of the directory as the root CID in the plugin configuration
+
+# Develop
+
+- `make venv`
+- `make test`
+- `make build`
+
+# License information
+
+Source code license: [GPL v.3](http://www.gnu.org/copyleft/gpl.html)
+
+Fanart is from https://pixabay.com/nl/beaded-spinneweb-raagbol-web-dauw-1630493/
+
+Icon is from https://pixabay.com/nl/puzzel-spel-kubus-rubiks-kubus-1243091/
diff --git a/addon.xml b/addon.xml
new file mode 100644
index 0000000..a182440
--- /dev/null
+++ b/addon.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ video
+
+
+ IPFS video viewing plugin
+ A plugin allowing access to IPFS video media.
+ https://ipfs.video/
+
+ icon.png
+ fanart.jpg
+ resources\screenshot-01.jpg
+
+
+
diff --git a/fanart.jpg b/fanart.jpg
new file mode 100644
index 0000000..e609ead
Binary files /dev/null and b/fanart.jpg differ
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..5318c69
Binary files /dev/null and b/icon.png differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..9a55a62
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+
+requests
+
+pytest
diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
new file mode 100644
index 0000000..dbc5e9f
--- /dev/null
+++ b/resources/language/resource.language.en_gb/strings.po
@@ -0,0 +1,25 @@
+# Kodi Media Center language file
+# Addon Name: IPFS
+# Addon id: plugin.video.ipfs
+# Addon Provider: ipfs.video
+msgid ""
+msgstr ""
+"Project-Id-Version: Kodi Addons\n"
+"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Kodi Translation Team\n"
+"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "#32000"
+msgid "Root CID"
+msgstr ""
+
+msgctxt "#32001"
+msgid "IPFS Gateway"
+msgstr ""
diff --git a/resources/screenshot-01.jpg b/resources/screenshot-01.jpg
new file mode 100644
index 0000000..1bec755
Binary files /dev/null and b/resources/screenshot-01.jpg differ
diff --git a/resources/settings.xml b/resources/settings.xml
new file mode 100644
index 0000000..f4e0c76
--- /dev/null
+++ b/resources/settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..5818e32
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,11 @@
+from setuptools import setup
+
+setup(name='ipfs-video-kodi',
+ version='0.1',
+ description='The funniest joke in the world',
+ url='',
+ author='Flying Circus',
+ author_email='flyingcircus@example.com',
+ license='MIT',
+ packages=["src"],
+ zip_safe=False)
diff --git a/src/ipfs/__init__.py b/src/ipfs/__init__.py
new file mode 100644
index 0000000..88593b7
--- /dev/null
+++ b/src/ipfs/__init__.py
@@ -0,0 +1,37 @@
+
+import requests
+import random
+
+def via(gateway):
+ return IPFS(gateway)
+
+class IPFS:
+ def __init__(self, gateway):
+ assert len(gateway) > 0
+ self._gateway = gateway
+ self._cache = {}
+
+ def get(self, path, params):
+ r = requests.get(self._gateway + '/api/v0/dag/get', params=params)
+ r.raise_for_status()
+ return r
+
+ def list(self, hash):
+ """Get the directory content of the given hash"""
+ assert type(hash) == str
+ if hash in self._cache:
+ if len(self._cache) > 50:
+ #Drop 10 keys
+ for k in random.sample(self._cache.keys(), 10):
+ del self._cache[k]
+ return self._cache[hash]
+
+ r = self.get('/api/v0/dag/get', params={"arg": hash})
+ r.raise_for_status()
+
+ entries = list(filter(lambda link: len(link['Name']) > 0 and '/' in link['Cid'], r.json()["links"]))
+ self._cache[hash] = entries
+ return entries
+
+ def link(self, hash):
+ return self._gateway + "/ipfs/" + hash
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..afcfdd9
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+import sys
+
+try:
+ #Python 3
+ from urllib.parse import urlencode, parse_qsl
+except ImportError:
+ from urllib import urlencode
+ from urlparse import parse_qsl
+
+import xbmcgui
+import xbmcplugin
+import ipfs
+
+# Get the plugin url in plugin:// notation.
+_url = sys.argv[0]
+# Get the plugin handle as an integer number.
+_handle = int(sys.argv[1])
+_rootCid = xbmcplugin.getSetting(_handle, 'rootCid')
+_ipfs = ipfs.via(xbmcplugin.getSetting(_handle, 'ipfsGateway'))
+
+def self_url(**kwargs):
+ """
+ Create a URL for calling the plugin recursively from the given set of keyword arguments.
+
+ :param kwargs: "argument=value" pairs
+ :type kwargs: dict
+ :return: plugin call URL
+ :rtype: str
+ """
+ return '{0}?{1}'.format(_url, urlencode(kwargs))
+
+def list_node(cid):
+ """
+ Create a listing of the given ipfs cid
+
+ :param cid: content identifier
+ :type category: str
+ """
+ # Set plugin category. It is displayed in some skins as the name
+ # of the current section.
+ xbmcplugin.setPluginCategory(_handle, cid)
+ # Set plugin content. It allows Kodi to select appropriate views
+ # for this type of content.
+ xbmcplugin.setContent(_handle, 'videos')
+ # Get the list of videos in the category.
+ links = _ipfs.list(cid)
+
+ for link in links:
+ is_folder = len(_ipfs.list(link['Cid']['/'])) > 0
+
+ list_item = xbmcgui.ListItem(label=link['Name'])
+ # Set additional info for the list item.
+ # 'mediatype' is needed for skin to display info for this ListItem correctly.
+ list_item.setInfo('video', {'title': link['Name'],
+ 'mediatype': 'video'})
+ # TODO set thumbnails
+ # list_item.setArt({'thumb': video['thumb'], 'icon': video['thumb'], 'fanart': video['thumb']})
+
+ list_item.setProperty('IsPlayable', ('false' if is_folder else 'true'))
+
+ url = self_url(action=('list' if is_folder else 'play'), cid=link['Cid']['/'])
+ # Add our item to the Kodi virtual folder listing.
+ xbmcplugin.addDirectoryItem(_handle, url, list_item, is_folder)
+ # Add a sort method for the virtual folder items (alphabetically, ignore articles)
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_TITLE)
+ xbmcplugin.endOfDirectory(_handle)
+
+
+def play_node(cid):
+ """
+ Play a video by the provided cid.
+
+ :param cid: Content id
+ :type path: str
+ """
+ play_item = xbmcgui.ListItem(path=_ipfs.link(cid))
+ xbmcplugin.setResolvedUrl(_handle, True, listitem=play_item)
+
+
+def router(paramstring):
+ """
+ Router function that calls other functions
+ depending on the provided paramstring
+
+ :param paramstring: URL encoded plugin paramstring
+ :type paramstring: str
+ """
+ params = dict(parse_qsl(paramstring))
+
+ #Default action
+ if not params:
+ params['action'] = 'list'
+ params['cid'] = _rootCid
+
+ # Check the parameters passed to the plugin
+ if params['action'] == 'list':
+ list_node(params['cid'])
+ elif params['action'] == 'play':
+ play_node(params['cid'])
+ else:
+ raise ValueError('Invalid paramstring: {0}!'.format(paramstring))
+
+
+if __name__ == '__main__':
+ # Call the router function and pass the plugin call parameters to it.
+ # We use string slicing to trim the leading '?' from the plugin call paramstring
+ router(sys.argv[2][1:])
diff --git a/test/test_ipfs.py b/test/test_ipfs.py
new file mode 100644
index 0000000..b8ac98d
--- /dev/null
+++ b/test/test_ipfs.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+import unittest
+import src.ipfs as ipfs
+
+test_gateway = ipfs.via("http://51.15.122.1")
+
+
+class TestIpfsMethods(unittest.TestCase):
+
+ def test_list_file_should_be_empty(self):
+ a = test_gateway.list("QmTNdv6MBhCjcGY5tpabi7aCeLZL65tmDzW37J9ZrFbZfL")
+ self.assertEqual(a, [])
+
+ def test_list_directory_should_work(self):
+ a = test_gateway.list("QmVZV84e6nSwfA8LppiS4KXKiAhpbqqGYzofHtecQjd9js")
+ self.assertEqual(len(a), 1)
+ self.assertEqual(a[0]['Name'], "pexel")
+
+
+
+if __name__ == '__main__':
+ unittest.main()