Tutorial

Here we will document the creation of an “add-on”. In this instance, “plugin.video.metalvideo”. This will be a simplified version of the full add-on that can be found over at: https://github.com/willforde/plugin.video.metalvideo

First of all, import the required “Codequick” components.

  • Route will be used to list folder items.
  • Resolver will be used to resolve video URLs.
  • Listitem is used to create “items” within Kodi.
  • utils is a module, containing some useful functions.
  • run is the function that controls the execution of the add-on.
from codequick import Route, Resolver, Listitem, utils, run

Next we will import “urlquick”, which is a “light-weight” HTTP client with a “requests” like interface, featuring caching support.

import urlquick

Now, we will use utils.urljoin_partial to create a URL constructor with the “base” URL of the site. This is use to convert relative URLs to absolute URLs. Normally HTML is full of relative URLs and this makes it easier to work with them, guaranteeing that you will always have an absolute URL to use.

# Base url constructor
url_constructor = utils.urljoin_partial("https://metalvideo.com")

Next we will create the “Root” function which will be the starting point for the add-on. It is very important that the “Root” function is called “root”. This function will first have to be registered as a “callback” function. Since this is a function that will return “listitems”, this will be registered as a Route callback. It is expected that a Route callback should return a “generator” or “list”, of codequick.Listitem objects. The first argument that will be passed to a Route callback, will always be the Route instance.

This “callback” will parse the list of “Music Video Categories” available on: http://metalvideo.com, This will return a “generator” of “listitems” linking to a sub-directory of videos within that category. Parsing of the HTML source will be done using “HTMLement” which is integrated into the “urlquick” request response.

@Route.register
def root(plugin):
    # Request the online resource
    url = url_constructor("/browse.html")
    resp = urlquick.get(url)

    # Filter source down to required section by giving the name and
    # attributes of the element containing the required data.
    # It's a lot faster to limit the parser to required section.
    root_elem = resp.parse("div", attrs={"id": "primary"})

    # Parse each category
    for elem in root_elem.iterfind("ul/li"):
        item = Listitem()

        # The image tag contains both the image url and title
        img = elem.find(".//img")

        # Set the thumbnail image
        item.art["thumb"] = img.get("src")

        # Set the title
        item.label = img.get("alt")

        # Fetch the url
        url = elem.find("div/a").get("href")

        # This will set the callback that will be called when listitem is activated.
        # 'video_list' is the route callback function that we will create later.
        # The 'url' argument is the url of the category that will be passed
        # to the 'video_list' callback.
        item.set_callback(video_list, url=url)

        # Return the listitem as a generator.
        yield item

Now, we can create the “video parser” callback that will return “playable” listitems. Since this is another function that will return listitems, it will be registered as a Route callback.

@Route.register
def video_list(plugin, url):
    # Request the online resource.
    url = url_constructor(url)
    resp = urlquick.get(url)

    # Parse the html source
    root_elem = resp.parse("div", attrs={"class": "primary-content"})

    # Parse each video
    for elem in root_elem.find("ul").iterfind("./li/div"):
        item = Listitem()

        # Set the thumbnail image of the video.
        item.art["thumb"] = elem.find(".//img").get("src")

        # Set the duration of the video
        item.info["duration"] = elem.find("span/span/span").text.strip()

        # Set thel plot info
        item.info["plot"] = elem.find("p").text.strip()

        # Set view count
        views = elem.find("./div/span[@class='pm-video-attr-numbers']/small").text
        item.info["count"] = views.split(" ", 1)[0].strip()

        # Set the date that the video was published
        date = elem.find(".//time[@datetime]").get("datetime")
        date = date.split("T", 1)[0]
        item.info.date(date, "%Y-%m-%d")  # 2018-10-19

        # Url to video & name
        a_tag = elem.find("h3/a")
        url = a_tag.get("href")
        item.label = a_tag.text

        # Extract the artist name from the title
        item.info["artist"] = [a_tag.text.split("-", 1)[0].strip()]

        # 'play_video' is the resolver callback function that we will create later.
        # The 'url' argument is the url of the video that will be passed
        # to the 'play_video' resolver callback.
        item.set_callback(play_video, url=url)

        # Return the listitem as a generator.
        yield item

    # Extract the next page url if one exists.
    next_tag = root_elem.find("./div[@class='pagination pagination-centered']/ul")
    if next_tag is not None:
        # Find all page links
        next_tag = next_tag.findall("li/a")
        # Reverse list of links so the next page link should be the first item
        next_tag.reverse()
        # Attempt to find the next page link with the text of '>>'
        for node in next_tag:
            if node.text == u"\xbb":
                yield Listitem.next_page(url=node.get("href"), callback=video_list)
                # We found the link so we can now break from the loop
                break

Finally we need to create the Resolver “callback”, and register it as so. This callback is expected to return a playable video URL. The first argument that will be passed to a Resolver callback, will always be a Resolver instance.

@Resolver.register
def play_video(plugin, url):
    # Sence https://metalvideo.com uses enbeaded youtube videos,
    # we can use 'plugin.extract_source' to extract the video url.
    url = url_constructor(url)
    return plugin.extract_source(url)

plugin.extract_source uses “YouTube.DL” to extract the video URL. Since it uses YouTube.DL, it will work with way-more than just youtube.

So to finish, we need to initiate the “codequick” startup process. This will call the “callback functions” automatically for you.

if __name__ == "__main__":
    run()