# -*- coding: UTF-8 -*- """ Kan Video plugin """ # Copyright (c) 2017 Tzafrir Cohen # SPDX-License-Identifier: GPL-2.0+ # # License-Filename: LICENSE.txt import datetime import re import sys import urllib import urlparse from bs4 import BeautifulSoup import requests import simplecache import xbmc import xbmcgui import xbmcplugin import yaml USER_AGENT = 'Kodi/plugin.video.debconf/1.0' PLUGIN_NAME = "plugin.video.debconf" # FIXME: a better way to get this? URL_BASE = 'https://salsa.debian.org/debconf-video-team/archive-meta/' class DummyCache: """ Dummy replacement of SimpleCache """ def __init__(self): pass def get(self, cache_id, checksum=''): return None def set(self, cache_id, data, checksum='', expiration=None): pass #SiteCache = simplecache.SimpleCache() # FIXME: avoid this global SiteCache = DummyCache() # Avoid caching for now class Page: def __init__(self, argv): self.base_url = argv[0] self.addon_handle = int(argv[1]) self.args = urlparse.parse_qs(argv[2][1:]) self.mode = self.first_arg('mode') def first_arg(self, name): val = self.args.get(name, None) if val is not None: val = val[0] return val def build_url(self, query): return self.base_url + '?' + urllib.urlencode(query) def add_directory_item(self, **kwargs): xbmcplugin.addDirectoryItem(handle=self.addon_handle, **kwargs) def end_directory(self): xbmcplugin.endOfDirectory(self.addon_handle) def placeholder_folder(self, foldername): """ A folder with a single dummy item """ url = 'http://localhost/some_video.mkv' li = xbmcgui.ListItem(foldername + 'Not Implemented', iconImage='DefaultVideo.png') self.add_directory_item(url=url, listitem=li) self.end_directory() def __str__(self): return "[base_url: {}, addon_handle: {}, mode: {}]" \ .format(self.base_url, self.addon_handle, self.mode) def read_url(url, name): cache_id = PLUGIN_NAME + ".url." + name url_text = SiteCache.get(cache_id) if url_text: return url_text headers = {'user-agent': USER_AGENT} response = requests.get(url, headers=headers) if response.status_code != 200: raise IOError("Invalid URL {}".format(url)) url_text = response.text SiteCache.set(cache_id, url_text, expiration=datetime.timedelta(days=1)) return url_text def debconf_year_menu(page, year): """ Show a menu of all videos from debconf of a specific year """ year_short = str(int(year) - 2000) url = URL_BASE + 'raw/master/metadata/{}/debconf{}.yml'.format(year, year_short) metadata_text = read_url(url, year) metadata = yaml.load(metadata_text) base_url = metadata['conference']['video_base'] for video in metadata['videos']: description = '' if 'description' in video: description = video['description'] title = video['title'] url = base_url + video['video'] li = xbmcgui.ListItem(title) li.setProperty('IsPlayable', 'true') li.setInfo('video', { 'plot': description, 'title': title, 'year': year, }) page.add_directory_item(url=url, listitem=li) page.end_directory() def debconf_menu(page): """ Present a list of pages of debconfs for each year """ # Figure the list of years: index_url = URL_BASE + 'tree/master/metadata' index_text = read_url(index_url, 'index') parsed = BeautifulSoup(index_text, "html.parser") anchors = parsed.find_all('a', class_='str-truncated') years = [a.get('title') for a in anchors] # Create a menu: for year in years: url = page.build_url({'mode': 'debconf_year', 'year': year}) li = xbmcgui.ListItem(u'Debconf Year ' + str(year)) page.add_directory_item(url=url, listitem=li, isFolder=True) page.end_directory() VIDEO_SUFFIXES = ['avi', 'AVI', 'flv', 'mkv', 'mp4', 'mpeg', 'ogv', 'webm'] VDNET_BASE = 'https://meetings-archive.debian.net/pub/debian-meetings/' def parse_vdnet_list(text): """ Parses the full directory listing into a tree of videos """ data = {} for line in text.split('\n'): line = line.encode('utf-8') items = line.rsplit('.', 1) if len(items) < 2 or items[1] not in VIDEO_SUFFIXES: continue items = line.split('/') year = items[0] conference = items[1] # More standard names: for conf_name in ['debconf', 'dudesconf', 'fosdem', 'GPN']: if re.match('^{}\d*$'.format(conf_name), conference): conference = conf_name if conference == 'ducc-it': conference = 'ducc.it' # Right? conference = re.sub('mini(|-?deb)conf', 'mini-debconf', conference) name = items[-1].split('.')[0] quality = " ".join(items[2:-1]) if quality == '': quality = 'default' if 'year' not in data: data['year'] = {} if year not in data['year']: data['year'][year] = {} if conference not in data['year'][year]: data['year'][year][conference] = {} if name not in data['year'][year][conference]: data['year'][year][conference][name] = {} if quality not in data['year'][year][conference][name]: data['year'][year][conference][name][quality] = {} data['year'][year][conference][name][quality] = line if 'conf' not in data: data['conf'] = {} if conference not in data['conf']: data['conf'][conference] = {} if year not in data['conf'][conference]: data['conf'][conference][year] = {} if name not in data['conf'][conference][year]: data['conf'][conference][year][name] = {} if quality not in data['conf'][conference][year][name]: data['conf'][conference][year][name][quality] = {} data['conf'][conference][year][name][quality] = line return data def get_vdnet_data(): """ Download and parse the index data. There should be no issue calling it many times as its output is cases. """ index_url = URL_BASE + 'raw/master/data/video.debian.net.list' # FIXME: no point in caching this download: cache_id = PLUGIN_NAME + '.vdunet_data' vdnet_data = SiteCache.get(cache_id) if vdnet_data: return vdnet_data index_text = read_url(index_url, 'index') vdnet_data = parse_vdnet_list(index_text) SiteCache.set(cache_id, vdnet_data, expiration=datetime.timedelta(days=1)) return vdnet_data def vdnet_video_menu(page, conf, year, name): """ The actual v.d.net video page. Shows the video in various qualities """ vdnet_data = get_vdnet_data() for quality in sorted(vdnet_data['year'][year][conf][name].keys()): title = '{} ({})'.format(name, quality) url = VDNET_BASE + vdnet_data['year'][year][conf][name][quality] li = xbmcgui.ListItem(title) li.setProperty('IsPlayable', 'true') li.setInfo('video', { 'title': name, 'year': year, }) page.add_directory_item(url=url, listitem=li) page.end_directory() def vdnet_confyear_menu(page, conf, year): vdnet_data = get_vdnet_data() for name in sorted(vdnet_data['year'][year][conf].keys()): url = page.build_url({'mode': 'vdnet_video', 'year': year, 'conf': conf, 'name': name}) li = xbmcgui.ListItem(name) page.add_directory_item(url=url, listitem=li, isFolder=True) page.end_directory() def vdnet_year_menu(page, year): vdnet_data = get_vdnet_data() for conf in sorted(vdnet_data['year'][year].keys()): url = page.build_url({'mode': 'vdnet_confyear', 'year': year, 'conf': conf}) li = xbmcgui.ListItem(conf) page.add_directory_item(url=url, listitem=li, isFolder=True) page.end_directory() def vdnet_conf_menu(page, conf): vdnet_data = get_vdnet_data() for year in sorted(vdnet_data['conf'][conf].keys()): url = page.build_url({'mode': 'vdnet_confyear', 'year': year, 'conf': conf}) li = xbmcgui.ListItem(year) page.add_directory_item(url=url, listitem=li, isFolder=True) page.end_directory() def vdnet_menu(page): """ Videos from video.debian.net (https://meetings-archive.debian.net/pub/debian-meetings/) """ vdnet_data = get_vdnet_data() for conf in sorted(vdnet_data['conf'].keys()): url = page.build_url({'mode': 'vdnet_conf', 'conf': conf}) li = xbmcgui.ListItem(u'Conference ' + conf) page.add_directory_item(url=url, listitem=li, isFolder=True) for year in sorted(vdnet_data['year'].keys()): url = page.build_url({'mode': 'vdnet_year', 'year': year}) li = xbmcgui.ListItem(u'Conferences in ' + str(year)) page.add_directory_item(url=url, listitem=li, isFolder=True) page.end_directory() def main_menu(page): """ Top-level menu """ url = page.build_url({'mode': 'debconf'}) li = xbmcgui.ListItem(u'Debconf Talks (full information)') page.add_directory_item(url=url, listitem=li, isFolder=True) url = page.build_url({'mode': 'vdnet'}) li = xbmcgui.ListItem(u'All video.debian.net Videos (just names)') page.add_directory_item(url=url, listitem=li, isFolder=True) page.end_directory() def main(): """ Select handler for the page """ page = Page(sys.argv) if page.mode is None: main_menu(page) elif page.mode == 'debconf': debconf_menu(page) elif page.mode == 'debconf_year': year = page.args['year'][0] debconf_year_menu(page, year) elif page.mode == 'vdnet': vdnet_menu(page) elif page.mode == 'vdnet_year': year = page.args['year'][0] vdnet_year_menu(page, year) elif page.mode == 'vdnet_conf': conf = page.args['conf'][0] vdnet_conf_menu(page, conf) elif page.mode == 'vdnet_confyear': conf = page.args['conf'][0] year = page.args['year'][0] vdnet_confyear_menu(page, conf, year) elif page.mode == 'vdnet_video': conf = page.args['conf'][0] year = page.args['year'][0] name = page.args['name'][0] vdnet_video_menu(page, conf, year, name) else: page.placeholder_folder("no page for mode " + page.mode) if __name__ == '__main__': main()