From 217479925fb937d8a2a29d580fd338ff3bffc02c Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 11:17:04 -0600 Subject: [PATCH 01/15] initial commit --- README.md | 15 +++ __init__.py | 49 +++++++++ config.py | 33 +++++++ plugin.py | 251 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + test.py | 15 +++ 6 files changed, 365 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 config.py create mode 100644 plugin.py create mode 100644 requirements.txt create mode 100644 test.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..993602d --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# TVMaze + +## Limnoria plugin to fetch TV show information and schedules from tvmaze.com API + +### Instructions + +#### This plugin requires Python 3 and Limnoria + +1. Clone this repository into your bot/plugins directory +```git clone --depth=1 https://gitlab.com/cottongin/TVMaze.git``` + +2. Install requirements for the plugin via pip +```pip install -r requirements.txt``` + +3. Load the plugin on your bot diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c628a28 --- /dev/null +++ b/__init__.py @@ -0,0 +1,49 @@ +### +# Copyright (c) 2019, cottongin +# All rights reserved. +# +# +### + +""" +TVMaze: Limnoria plugin to fetch TV show information and schedules from tvmaze.com API +""" + +import sys +import supybot +from supybot import world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.authors.unknown + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = '' + +from . import config +from . import plugin +if sys.version_info >= (3, 4): + from importlib import reload +else: + from imp import reload +# In case we're being reloaded. +reload(config) +reload(plugin) +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + from . import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/config.py b/config.py new file mode 100644 index 0000000..ece0813 --- /dev/null +++ b/config.py @@ -0,0 +1,33 @@ +### +# Copyright (c) 2019, cottongin +# All rights reserved. +# +# +### + +from supybot import conf, registry +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('TVMaze') +except: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified themself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('TVMaze', True) + + +TVMaze = conf.registerPlugin('TVMaze') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(TVMaze, 'someConfigVariableName', +# registry.Boolean(False, _("""Help for someConfigVariableName."""))) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..3400d07 --- /dev/null +++ b/plugin.py @@ -0,0 +1,251 @@ +# TVMaze v0.0.1 +### +# Copyright (c) 2019, cottongin +# All rights reserved. +# +# See LICENSE.txt +### + +import requests +import pendulum +import urllib.parse + +from supybot import utils, plugins, ircutils, callbacks +from supybot.commands import * +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('TVMaze') +except ImportError: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + + +class TVMaze(callbacks.Plugin): + """Limnoria plugin to fetch TV show information and schedules from tvmaze.com API""" + threaded = True + + def __init__(self, irc): + super().__init__(irc) + + def die(self): + super().die() + + #--------------------# + # Formatting helpers # + #--------------------# + + def _bold(self, string): + return ircutils.bold(string) + + def _ul(self, string): + return ircutils.underline(string) + + def _color(self, string, color): + return ircutils.mircColor(string, color) + + #--------------------# + # Internal functions # + #--------------------# + + def _get(self, mode, country='US', date=None, query=None, id_=None): + """wrapper for requests tailored to TVMaze API""" + + base_url = 'http://api.tvmaze.com' + + if mode == 'search': + if not query: + return + query = urllib.parse.quote_plus(query) + base_url += '/search/shows?q={}'.format(query) + try: + data = requests.get(base_url).json() + except: + data = None + elif mode == 'schedule': + if not date: + date = pendulum.now().format('YYYY-MM-DD') + base_url += '/schedule?country={}&date={}'.format(country, date) + try: + data = requests.get(base_url).json() + except: + data = None + elif mode == 'shows': + if not id_: + return + base_url += '/shows/{}?embed[]=previousepisode&embed[]=nextepisode'.format(id_) + try: + data = requests.get(base_url).json() + except: + data = None + else: + data = None + + return data + + #------------------# + # Public functions # + #------------------# + + @wrap([getopts({'country': 'somethingWithoutSpaces'}), 'text']) + def tvshow(self, irc, msg, args, options, query): + """[--country ] + Fetches information about provided TV Show from TVMaze.com. + Optionally include --country to find shows with the same name from another country + Ex: tvshow --country GB the office + """ + options = dict(options) + country = options.get('country') + + show_search = self._get('search', query=query) + if not show_search: + irc.reply('Nothing found for your query: {}'.format(query)) + return + + if country: + for show in show_search: + if show['show'].get('network'): + if show['show']['network']['country']['code'].upper() == country.upper(): + show_id = show['show']['id'] + break + else: + show_id = show_search[0]['show']['id'] + + show_info = self._get('shows', id_=show_id) + + urls = [] + urls.append(show_info['url']) + urls.append('https://imdb.com/title/{}/'.format(show_info['externals']['imdb'])) + urls.append(show_info['officialSite']) + + genres = '/'.join(g for g in show_info['genres']) + + name = self._bold(show_info['name']) + lang = show_info['language'] + status = show_info['status'] + if status == 'Ended': + status = self._color(status, 'red') + elif status == 'Running': + status = self._color(status, 'green') + runtime = "{}m".format(show_info['runtime']) + if show_info.get('premiered'): + premiered = show_info['premiered'][:4] + else: + premiered = "TBD" + name = "{} ({})".format(name, premiered) + network = show_info.get('network') + network = network['name'] if network else "" + + if show_info['_embedded']: + if show_info['_embedded'].get('previousepisode'): + try: + ep = "S{:02d}E{:02d}".format( + show_info['_embedded']['previousepisode']['season'], + show_info['_embedded']['previousepisode']['number'] + ) + except: + ep = "?" + ep = self._color(ep, 'orange') + previous = " | {}: {ep_name} [{ep}] ({ep_date}) | ".format( + self._bold('Prev'), + ep_name=show_info['_embedded']['previousepisode']['name'], + ep=ep, + ep_date=show_info['_embedded']['previousepisode']['airdate'] + ) + else: + previous = "" + + if show_info['_embedded'].get('nextepisode'): + try: + ep = "S{:02d}E{:02d}".format( + show_info['_embedded']['nextepisode']['season'], + show_info['_embedded']['nextepisode']['number'] + ) + except: + ep = "?" + ep = self._color(ep, 'orange') + next_ = " | {}: {ep_name} [{ep}] ({ep_date} {when})".format( + self._bold('Next'), + ep_name=show_info['_embedded']['nextepisode']['name'], + ep=ep, + ep_date=show_info['_embedded']['nextepisode']['airdate'], + when=pendulum.parse(show_info['_embedded']['nextepisode']['airstamp']).diff_for_humans() + ) + else: + next_ = "" + + reply = "{}{}{}{} | {} | {} | {} | {} | {}".format( + name, + next_, + previous, + status, + lang, + runtime, + network, + genres, + ' | '.join(u for u in urls) + ) + irc.reply(reply) + + + @wrap([getopts({'all': '', + 'tz': 'somethingWithoutSpaces', + 'network': 'somethingWithoutSpaces', + 'country': 'somethingWithoutSpaces'})]) + def schedule(self, irc, msg, args, options): + """[--all | --tz | --network | --country ] + Fetches upcoming TV schedule from TVMaze.com. + """ + options = dict(options) + tz = options.get('tz') or 'US/Eastern' + country = options.get('country') + if country: + country = country.upper() + if not options.get('tz'): + if country == 'GB': + tz = 'GMT' + elif country == 'AU': + tz = 'Australia/Sydney' + else: + tz = 'US/Eastern' + else: + country = 'US' + + schedule_data = self._get('schedule', country=country) + + if not schedule_data: + irc.reply('Something went wrong fetching TVMaze schedule data.') + return + + shows = [] + for show in schedule_data: + # TO-DO: implement custom --filter + tmp = "{show_name} [{ep}] ({show_time})" + name = "{1}: {0}".format(show['name'], show['show']['name']) + try: + ep_id = "S{:02d}E{:02d}".format(show['season'], show['number']) + except: + ep_id = '?' + time = pendulum.parse(show['airstamp']).in_tz(tz).format('h:mm A zz') + tmp = tmp.format(show_name=self._bold(name), + ep=self._color(ep_id, 'orange'), + show_time=time) + if options.get('all'): + shows.append(tmp) + elif options.get('network'): + if show['show'].get('network'): + if show['show']['network']['name'].lower() == options.get('network').lower(): + shows.append(tmp) + else: + if show['show']['type'] == 'Scripted': + shows.append(tmp) + + reply = "{}: {}".format(self._ul("Today's Shows"), ", ".join(s for s in shows)) + irc.reply(reply) + + + +Class = TVMaze + + +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b47cb15 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +pendulum \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..4ae931e --- /dev/null +++ b/test.py @@ -0,0 +1,15 @@ +### +# Copyright (c) 2019, cottongin +# All rights reserved. +# +# +### + +from supybot.test import * + + +class TVMazeTestCase(PluginTestCase): + plugins = ('TVMaze',) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From d548de01c519547935f0d74c92134331abbce11f Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 17:18:25 +0000 Subject: [PATCH 02/15] Add LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9464006 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 cottongin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 8abd94444bb9fecc18968a0d5175900e3bdab50c Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 11:26:18 -0600 Subject: [PATCH 03/15] update readme --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 993602d..17b164d 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,26 @@ #### This plugin requires Python 3 and Limnoria 1. Clone this repository into your bot/plugins directory -```git clone --depth=1 https://gitlab.com/cottongin/TVMaze.git``` +``` +git clone --depth=1 https://gitlab.com/cottongin/TVMaze.git +``` 2. Install requirements for the plugin via pip -```pip install -r requirements.txt``` +``` +pip install -r requirements.txt +``` 3. Load the plugin on your bot +``` +@load TVMaze +``` + +### Example Usage +``` + @schedule --country GB --tz GMT + Today's Shows: Cuckoo: Ivy Arrives [S05E01] (10:00 AM GMT), Cuckoo: Ivy Nanny [S05E02] (10:00 AM GMT), Cuckoo: Weed Farm [S05E03] (10:00 AM GMT), Cuckoo: Macbeth [S05E04] (10:00 AM GMT), Cuckoo: Divorce Party [S05E05] (10:00 AM GMT), Cuckoo: Two Engagements and a Funeral [S05E06] (10:00 AM GMT), Cuckoo: Election [S05E07] (10:00 AM GMT), The Dumping Ground: Rage [S08E01] (5:00 PM GMT), (1 more message) + + @tvshow the orville + The Orville (2017) | Next: Home [S02E03] (2019-01-10 in 6 days) | Prev: Primal Urges [S02E02] (2019-01-03) | Running | English | 60m | FOX | Comedy/Adventure/Science-Fiction | http://www.tvmaze.com/shows/20263/the-orville | https://imdb.com/title/tt5691552/ | http://www.fox.com/the-orville +``` +Use @help tvshow|schedule to see details on each command. \ No newline at end of file From d00088177429102af4b99707f0726f38e50df1ea Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 12:04:38 -0600 Subject: [PATCH 04/15] adds accountsdb.py --- accountsdb.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 accountsdb.py diff --git a/accountsdb.py b/accountsdb.py new file mode 100644 index 0000000..9cb1cec --- /dev/null +++ b/accountsdb.py @@ -0,0 +1,112 @@ +### +# Copyright (c) 2019, James Lu +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +accountsdb: Provides storage for user-specific data via Supybot accounts, ident@host, or nicks. +""" + +import pickle + +from supybot import ircdb, log, conf, registry + +MODES = ["accounts", "identhost", "nicks"] +DEFAULT_MODE = MODES[0] + +class _AccountsDBAddressConfig(registry.OnlySomeStrings): + validStrings = MODES + +CONFIG_OPTION_NAME = "DBAddressingMode" +CONFIG_OPTION = _AccountsDBAddressConfig(DEFAULT_MODE, """Sets the DB addressing mode. + This requires reloading the plugin to take effect. Valid settings include accounts + (save users by Supybot accounts and ident@host if not registered), identhost + (save users by ident@host), and nicks (save users by nicks). + When changing addressing modes, existing keys will be left intact, but migration between + addressing modes is NOT supported.""") + +class AccountsDB(): + """ + Abstraction to map users to third-party account names. + + This stores users by their bot account first, falling back to their + ident@host if they are not logged in. + """ + + def __init__(self, plugin_name, filename, addressing_mode=DEFAULT_MODE): + """ + Loads the existing database, creating a new one in memory if none + exists. + """ + self.db = {} + self._plugin_name = plugin_name + self.filename = conf.supybot.directories.data.dirize(filename) + + self.addressing_mode = addressing_mode + + try: + with open(self.filename, 'rb') as f: + self.db = pickle.load(f) + except Exception as e: + log.debug('%s: Unable to load database, creating ' + 'a new one: %s', self._plugin_name, e) + + def flush(self): + """Exports the database to a file.""" + try: + with open(self.filename, 'wb') as f: + pickle.dump(self.db, f, 2) + except Exception as e: + log.warning('%s: Unable to write database: %s', self._plugin_name, e) + + def _get_key(self, prefix): + nick, identhost = prefix.split('!', 1) + + if self.addressing_mode == "accounts": + try: # Try to first look up the caller as a bot account. + userobj = ircdb.users.getUser(prefix) + return userobj.name + except KeyError: # If that fails, store them by nick@host. + return identhost + elif self.addressing_mode == "identhost": + return identhost + elif self.addressing_mode == "nicks": + return nick + else: + raise ValueError("Unknown addressing mode %r" % self.addressing_mode) + + def set(self, prefix, newId): + """Sets a user ID given the user's prefix.""" + user = self._get_key(prefix) + self.db[user] = newId + + def get(self, prefix): + """Sets a user ID given the user's prefix.""" + user = self._get_key(prefix) + + # Automatically returns None if entry does not exist + return self.db.get(user) From 10ca1e94d19a88494a00eb172c06e6e12460e028 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 12:52:34 -0600 Subject: [PATCH 05/15] implements accountsdb and adds settvmazeoptions --- __init__.py | 7 +++++-- accountsdb.py | 2 +- config.py | 3 +++ plugin.py | 42 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index c628a28..17d4fd1 100644 --- a/__init__.py +++ b/__init__.py @@ -18,14 +18,15 @@ from supybot import world __version__ = "" # XXX Replace this with an appropriate author or supybot.Author instance. -__author__ = supybot.authors.unknown +__author__ = supybot.Author('cottongin', 'cottongin', + 'cottongin@cottongin.club') # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} # This is a url where the most recent plugin package can be downloaded. -__url__ = '' +__url__ = 'https://gitlab.com/cottongin/TVMaze' from . import config from . import plugin @@ -38,6 +39,8 @@ reload(config) reload(plugin) # Add more reloads here if you add third-party modules and want them to be # reloaded when this plugin is reloaded. Don't forget to import them as well! +from . import accountsdb +reload(accountsdb) if world.testing: from . import test diff --git a/accountsdb.py b/accountsdb.py index 9cb1cec..371c098 100644 --- a/accountsdb.py +++ b/accountsdb.py @@ -36,7 +36,7 @@ import pickle from supybot import ircdb, log, conf, registry MODES = ["accounts", "identhost", "nicks"] -DEFAULT_MODE = MODES[0] +DEFAULT_MODE = MODES[2] class _AccountsDBAddressConfig(registry.OnlySomeStrings): validStrings = MODES diff --git a/config.py b/config.py index ece0813..774f1e8 100644 --- a/config.py +++ b/config.py @@ -13,6 +13,8 @@ except: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x + +from . import accountsdb def configure(advanced): @@ -28,6 +30,7 @@ TVMaze = conf.registerPlugin('TVMaze') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(TVMaze, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) +conf.registerGlobalValue(TVMaze, accountsdb.CONFIG_OPTION_NAME, accountsdb.CONFIG_OPTION) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugin.py b/plugin.py index 3400d07..ae312b6 100644 --- a/plugin.py +++ b/plugin.py @@ -10,7 +10,9 @@ import requests import pendulum import urllib.parse -from supybot import utils, plugins, ircutils, callbacks +from . import accountsdb + +from supybot import utils, plugins, ircutils, callbacks, world from supybot.commands import * try: from supybot.i18n import PluginInternationalization @@ -27,8 +29,12 @@ class TVMaze(callbacks.Plugin): def __init__(self, irc): super().__init__(irc) + self.db = accountsdb.AccountsDB("TVMaze", 'TVMaze.db', self.registryValue(accountsdb.CONFIG_OPTION_NAME)) + world.flushers.append(self.db.flush) def die(self): + world.flushers.remove(self.db.flush) + self.db.flush() super().die() #--------------------# @@ -94,7 +100,11 @@ class TVMaze(callbacks.Plugin): Optionally include --country to find shows with the same name from another country Ex: tvshow --country GB the office """ - options = dict(options) + # prefer manually passed options, then saved user options + # this merges the two possible dictionaries, prefering manually passed + # options if they already exist + options = {**self.db.get(msg.prefix), **dict(options)} + country = options.get('country') show_search = self._get('search', query=query) @@ -196,7 +206,11 @@ class TVMaze(callbacks.Plugin): """[--all | --tz | --network | --country ] Fetches upcoming TV schedule from TVMaze.com. """ - options = dict(options) + # prefer manually passed options, then saved user options + # this merges the two possible dictionaries, prefering manually passed + # options if they already exist + options = {**self.db.get(msg.prefix), **dict(options)} + tz = options.get('tz') or 'US/Eastern' country = options.get('country') if country: @@ -243,6 +257,28 @@ class TVMaze(callbacks.Plugin): reply = "{}: {}".format(self._ul("Today's Shows"), ", ".join(s for s in shows)) irc.reply(reply) + @wrap([getopts({'country': 'somethingWithoutSpaces', + 'tz': 'somethingWithoutSpaces', + 'clear': ''})]) + def settvmazeoptions(self, irc, msg, args, options): + """--country | --tz + Allows user to set options for easier use of TVMaze commands. + Use --clear to reset all options. + """ + + options = dict(options) + if options.get('clear'): + self.db.set(msg.prefix, {}) + irc.replySuccess() + return + if not options: + irc.reply('You must give me some options!') + return + + self.db.set(msg.prefix, options) + irc.replySuccess() + + Class = TVMaze From 135bf667a94f24ff0d9f68d86e1b1b1e7dd9bd93 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 12:57:18 -0600 Subject: [PATCH 06/15] fixes settvmazeoptions --- plugin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugin.py b/plugin.py index ae312b6..43db8d3 100644 --- a/plugin.py +++ b/plugin.py @@ -265,15 +265,19 @@ class TVMaze(callbacks.Plugin): Allows user to set options for easier use of TVMaze commands. Use --clear to reset all options. """ + if not options: + irc.reply('You must give me some options!') + return + + # prefer manually passed options, then saved user options + # this merges the two possible dictionaries, prefering manually passed + # options if they already exist + options = {**self.db.get(msg.prefix), **dict(options)} - options = dict(options) if options.get('clear'): self.db.set(msg.prefix, {}) irc.replySuccess() return - if not options: - irc.reply('You must give me some options!') - return self.db.set(msg.prefix, options) irc.replySuccess() From a6686aebd132ba869d22fb89ed2fb90e7865c492 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 13:02:10 -0600 Subject: [PATCH 07/15] update README to include instructions for saving user options --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17b164d..8e100b9 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,15 @@ pip install -r requirements.txt @tvshow the orville The Orville (2017) | Next: Home [S02E03] (2019-01-10 in 6 days) | Prev: Primal Urges [S02E02] (2019-01-03) | Running | English | 60m | FOX | Comedy/Adventure/Science-Fiction | http://www.tvmaze.com/shows/20263/the-orville | https://imdb.com/title/tt5691552/ | http://www.fox.com/the-orville ``` -Use @help tvshow|schedule to see details on each command. \ No newline at end of file +Use @help tvshow|schedule to see details on each command. +--- +You can use @settvmazeoptions to save common command options to make using commands easier: +``` +@settvmazeoptions --country GB +@settvmazeoptions --tz US/Central +@settvmazeoptions --country AU --tz US/Pacific +``` +This stores settings per nick, you can clear them via --clear: +``` +@settvmazeoptions --clear +``` \ No newline at end of file From a55f313180bb4c10158417c1c2a7b1c205e6dece Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 13:02:53 -0600 Subject: [PATCH 08/15] update README to include instructions for saving user options --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8e100b9..0adb685 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ pip install -r requirements.txt The Orville (2017) | Next: Home [S02E03] (2019-01-10 in 6 days) | Prev: Primal Urges [S02E02] (2019-01-03) | Running | English | 60m | FOX | Comedy/Adventure/Science-Fiction | http://www.tvmaze.com/shows/20263/the-orville | https://imdb.com/title/tt5691552/ | http://www.fox.com/the-orville ``` Use @help tvshow|schedule to see details on each command. + --- + You can use @settvmazeoptions to save common command options to make using commands easier: ``` @settvmazeoptions --country GB From 69d08aa4fc4128580118e0d6e0d92c26b0631890 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 15:27:53 -0600 Subject: [PATCH 09/15] adds --detail option to tvshow, cleanup schedule command, comments --- config.py | 3 ++ plugin.py | 120 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/config.py b/config.py index 774f1e8..191b1ea 100644 --- a/config.py +++ b/config.py @@ -32,5 +32,8 @@ TVMaze = conf.registerPlugin('TVMaze') # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerGlobalValue(TVMaze, accountsdb.CONFIG_OPTION_NAME, accountsdb.CONFIG_OPTION) +conf.registerChannelValue(TVMaze, 'showEpisodeTitle', + registry.Boolean(True, _("""Determines whether the episode title will be displayed in the schedule output."""))) + # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugin.py b/plugin.py index 43db8d3..45fec7a 100644 --- a/plugin.py +++ b/plugin.py @@ -93,11 +93,14 @@ class TVMaze(callbacks.Plugin): # Public functions # #------------------# - @wrap([getopts({'country': 'somethingWithoutSpaces'}), 'text']) + @wrap([getopts({'country': 'somethingWithoutSpaces', + 'detail': '', + 'd': ''}), 'text']) def tvshow(self, irc, msg, args, options, query): - """[--country ] + """[--country | --detail|--d] Fetches information about provided TV Show from TVMaze.com. - Optionally include --country to find shows with the same name from another country + Optionally include --country to find shows with the same name from another country. + Optionally include --detail (or --d) to show additional details. Ex: tvshow --country GB the office """ # prefer manually passed options, then saved user options @@ -105,48 +108,93 @@ class TVMaze(callbacks.Plugin): # options if they already exist options = {**self.db.get(msg.prefix), **dict(options)} + # filter out any manually passed options country = options.get('country') + show_detail = options.get('d') or options.get('detail') + # search for the queried TV show show_search = self._get('search', query=query) if not show_search: irc.reply('Nothing found for your query: {}'.format(query)) return + # if we have a country, look for that first instead of the first result if country: for show in show_search: if show['show'].get('network'): if show['show']['network']['country']['code'].upper() == country.upper(): show_id = show['show']['id'] break + # if we can't find it, default to the first result anyway + if not show_id: + show_id = show_search[0]['show']['id'] else: show_id = show_search[0]['show']['id'] + # fetch the show information show_info = self._get('shows', id_=show_id) + # grab the included URLs and generate an imdb one urls = [] urls.append(show_info['url']) urls.append('https://imdb.com/title/{}/'.format(show_info['externals']['imdb'])) urls.append(show_info['officialSite']) - genres = '/'.join(g for g in show_info['genres']) + # grab the genres + genres = '{}: {}'.format( + self._bold('Genre(s)'), + '/'.join(show_info['genres']) + ) + # show name name = self._bold(show_info['name']) - lang = show_info['language'] + + # show language + lang = "{}: {}".format( + self._bold('Language'), + show_info['language'] + ) + + # show status status = show_info['status'] if status == 'Ended': status = self._color(status, 'red') elif status == 'Running': status = self._color(status, 'green') - runtime = "{}m".format(show_info['runtime']) + + # show duration + runtime = "{}: {}m".format( + self._bold('Duration'), + show_info['runtime'] + ) + + # show premiere date, stripped to year and added to name if show_info.get('premiered'): premiered = show_info['premiered'][:4] else: premiered = "TBD" name = "{} ({})".format(name, premiered) - network = show_info.get('network') - network = network['name'] if network else "" + # is the show on television or web (netflix, amazon, etc) + if show_info.get('network'): + # we use this if --detail/--d is asked for + network = show_info['network']['name'] + schedule = "{}: {} at {} on {}".format( + self._bold('Schedule'), + ', '.join(show_info['schedule']['days']), + show_info['schedule']['time'], + network + ) + elif show_info.get('webChannel'): + # we use this if --detail/--d is asked for + network = show_info['webChannel']['name'] + schedule = "Watch on {}".format( + network + ) + + # try to get previous and/or next episode details if show_info['_embedded']: + # previous episode if show_info['_embedded'].get('previousepisode'): try: ep = "S{:02d}E{:02d}".format( @@ -156,7 +204,7 @@ class TVMaze(callbacks.Plugin): except: ep = "?" ep = self._color(ep, 'orange') - previous = " | {}: {ep_name} [{ep}] ({ep_date}) | ".format( + previous = " | {}: {ep_name} [{ep}] ({ep_date})".format( self._bold('Prev'), ep_name=show_info['_embedded']['previousepisode']['name'], ep=ep, @@ -164,7 +212,7 @@ class TVMaze(callbacks.Plugin): ) else: previous = "" - + # next episode if show_info['_embedded'].get('nextepisode'): try: ep = "S{:02d}E{:02d}".format( @@ -183,25 +231,33 @@ class TVMaze(callbacks.Plugin): ) else: next_ = "" - - reply = "{}{}{}{} | {} | {} | {} | {} | {}".format( + + # now finally put it all together and reply + reply = "{0} ({3}){1}{2} | {4}".format( name, next_, previous, status, - lang, - runtime, - network, - genres, - ' | '.join(u for u in urls) + ' | '.join(urls) ) irc.reply(reply) + # add a second line for details if requested + if show_detail: + reply = "{} | {} | {} | {}".format( + schedule, + runtime, + lang, + genres + ) + irc.reply(reply) + @wrap([getopts({'all': '', 'tz': 'somethingWithoutSpaces', 'network': 'somethingWithoutSpaces', - 'country': 'somethingWithoutSpaces'})]) + 'country': 'somethingWithoutSpaces', + 'showEpisodeTitle': ''})]) def schedule(self, irc, msg, args, options): """[--all | --tz | --network | --country ] Fetches upcoming TV schedule from TVMaze.com. @@ -211,10 +267,14 @@ class TVMaze(callbacks.Plugin): # options if they already exist options = {**self.db.get(msg.prefix), **dict(options)} + # parse manually passed options, if any tz = options.get('tz') or 'US/Eastern' country = options.get('country') + # TO-DO: add a --filter option(s) if country: country = country.upper() + # if user isn't asking for a specific timezone, + # default to some sane ones given the country if not options.get('tz'): if country == 'GB': tz = 'GMT' @@ -224,26 +284,37 @@ class TVMaze(callbacks.Plugin): tz = 'US/Eastern' else: country = 'US' + # we don't need to default tz here because it's already set + # fetch the schedule schedule_data = self._get('schedule', country=country) if not schedule_data: irc.reply('Something went wrong fetching TVMaze schedule data.') return + # parse schedule shows = [] for show in schedule_data: - # TO-DO: implement custom --filter tmp = "{show_name} [{ep}] ({show_time})" - name = "{1}: {0}".format(show['name'], show['show']['name']) + # by default we show the episode title, there is a channel config option to disable this + # and users can override with --showEpisodeTitle flag + show_title = options.get('showEpisodeTitle') or self.registryValue('showEpisodeTitle', msg.args[0]) + if show_title: + name = "{1}: {0}".format(show['name'], show['show']['name']) + else: + name = "{0}".format(show['show']['name']) + # try to build some season/episode information try: ep_id = "S{:02d}E{:02d}".format(show['season'], show['number']) except: ep_id = '?' time = pendulum.parse(show['airstamp']).in_tz(tz).format('h:mm A zz') + # put it all together tmp = tmp.format(show_name=self._bold(name), ep=self._color(ep_id, 'orange'), show_time=time) + # depending on any options, append to list if options.get('all'): shows.append(tmp) elif options.get('network'): @@ -251,17 +322,20 @@ class TVMaze(callbacks.Plugin): if show['show']['network']['name'].lower() == options.get('network').lower(): shows.append(tmp) else: + # for now, defaults to only 'Scripted' shows if show['show']['type'] == 'Scripted': shows.append(tmp) - - reply = "{}: {}".format(self._ul("Today's Shows"), ", ".join(s for s in shows)) + + # finally reply + reply = "{}: {}".format(self._ul("Today's Shows"), ", ".join(shows)) irc.reply(reply) @wrap([getopts({'country': 'somethingWithoutSpaces', 'tz': 'somethingWithoutSpaces', + 'showEpisodeTitle': 'boolean', 'clear': ''})]) def settvmazeoptions(self, irc, msg, args, options): - """--country | --tz + """--country | --tz | --showEpisodeTitle (True|False) Allows user to set options for easier use of TVMaze commands. Use --clear to reset all options. """ From 1073c1303f9b02b06b03448cb2844b893ca9ef2d Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 15:34:08 -0600 Subject: [PATCH 10/15] changes schedule output to only upcoming shows --- plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.py b/plugin.py index 45fec7a..3779d34 100644 --- a/plugin.py +++ b/plugin.py @@ -309,11 +309,11 @@ class TVMaze(callbacks.Plugin): ep_id = "S{:02d}E{:02d}".format(show['season'], show['number']) except: ep_id = '?' - time = pendulum.parse(show['airstamp']).in_tz(tz).format('h:mm A zz') + time = pendulum.parse(show['airstamp']).in_tz(tz) # put it all together tmp = tmp.format(show_name=self._bold(name), ep=self._color(ep_id, 'orange'), - show_time=time) + show_time=time.format('h:mm A zz')) # depending on any options, append to list if options.get('all'): shows.append(tmp) @@ -322,8 +322,8 @@ class TVMaze(callbacks.Plugin): if show['show']['network']['name'].lower() == options.get('network').lower(): shows.append(tmp) else: - # for now, defaults to only 'Scripted' shows - if show['show']['type'] == 'Scripted': + # for now, defaults to only upcoming 'Scripted' shows + if show['show']['type'] == 'Scripted' and pendulum.now(tz) <= time: shows.append(tmp) # finally reply From e027fea75f5fb7432eb8b77bf4dd1f2918e32a3d Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 15:43:44 -0600 Subject: [PATCH 11/15] fixes bug with --country in tvshow command --- plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin.py b/plugin.py index 3779d34..2e68e62 100644 --- a/plugin.py +++ b/plugin.py @@ -120,6 +120,7 @@ class TVMaze(callbacks.Plugin): # if we have a country, look for that first instead of the first result if country: + show_id = None for show in show_search: if show['show'].get('network'): if show['show']['network']['country']['code'].upper() == country.upper(): From fc7f7b4fbe9ab3af8c645ea8329d077e2a98ff34 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 16:12:10 -0600 Subject: [PATCH 12/15] fix URL grabbing in tvshow, default user options when user doesn't exist in db --- plugin.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugin.py b/plugin.py index 2e68e62..2824ad7 100644 --- a/plugin.py +++ b/plugin.py @@ -106,7 +106,8 @@ class TVMaze(callbacks.Plugin): # prefer manually passed options, then saved user options # this merges the two possible dictionaries, prefering manually passed # options if they already exist - options = {**self.db.get(msg.prefix), **dict(options)} + user_options = self.db.get(msg.prefix) or dict() + options = {**user_options, **dict(options)} # filter out any manually passed options country = options.get('country') @@ -139,7 +140,8 @@ class TVMaze(callbacks.Plugin): urls = [] urls.append(show_info['url']) urls.append('https://imdb.com/title/{}/'.format(show_info['externals']['imdb'])) - urls.append(show_info['officialSite']) + if show_info['officialSite']: + urls.append(show_info['officialSite']) # grab the genres genres = '{}: {}'.format( @@ -266,7 +268,8 @@ class TVMaze(callbacks.Plugin): # prefer manually passed options, then saved user options # this merges the two possible dictionaries, prefering manually passed # options if they already exist - options = {**self.db.get(msg.prefix), **dict(options)} + user_options = self.db.get(msg.prefix) or dict() + options = {**user_options, **dict(options)} # parse manually passed options, if any tz = options.get('tz') or 'US/Eastern' @@ -334,9 +337,11 @@ class TVMaze(callbacks.Plugin): @wrap([getopts({'country': 'somethingWithoutSpaces', 'tz': 'somethingWithoutSpaces', 'showEpisodeTitle': 'boolean', + 'detail': 'boolean', + 'd': 'boolean', 'clear': ''})]) def settvmazeoptions(self, irc, msg, args, options): - """--country | --tz | --showEpisodeTitle (True|False) + """--country | --tz | --showEpisodeTitle (True/False) | --detail/--d (True/False) Allows user to set options for easier use of TVMaze commands. Use --clear to reset all options. """ @@ -347,7 +352,8 @@ class TVMaze(callbacks.Plugin): # prefer manually passed options, then saved user options # this merges the two possible dictionaries, prefering manually passed # options if they already exist - options = {**self.db.get(msg.prefix), **dict(options)} + user_options = self.db.get(msg.prefix) or dict() + options = {**user_options, **dict(options)} if options.get('clear'): self.db.set(msg.prefix, {}) From 5bd131294f1068c5489bcbf7dfbb2f4b628dab6a Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 4 Jan 2019 17:11:23 -0600 Subject: [PATCH 13/15] schedule cleanup and added --date option --- plugin.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/plugin.py b/plugin.py index 2824ad7..0f62f77 100644 --- a/plugin.py +++ b/plugin.py @@ -260,9 +260,10 @@ class TVMaze(callbacks.Plugin): 'tz': 'somethingWithoutSpaces', 'network': 'somethingWithoutSpaces', 'country': 'somethingWithoutSpaces', + 'date': 'somethingWithoutSpaces', 'showEpisodeTitle': ''})]) def schedule(self, irc, msg, args, options): - """[--all | --tz | --network | --country ] + """[--all | --tz | --network | --country | --date ] Fetches upcoming TV schedule from TVMaze.com. """ # prefer manually passed options, then saved user options @@ -274,6 +275,7 @@ class TVMaze(callbacks.Plugin): # parse manually passed options, if any tz = options.get('tz') or 'US/Eastern' country = options.get('country') + date = options.get('date') # TO-DO: add a --filter option(s) if country: country = country.upper() @@ -290,8 +292,14 @@ class TVMaze(callbacks.Plugin): country = 'US' # we don't need to default tz here because it's already set + # parse date input + if date: + date = pendulum.parse(date, strict=False).format('YYYY-MM-DD') + else: + date = pendulum.now(tz).format('YYYY-MM-DD') + # fetch the schedule - schedule_data = self._get('schedule', country=country) + schedule_data = self._get('schedule', country=country, date=date) if not schedule_data: irc.reply('Something went wrong fetching TVMaze schedule data.') @@ -329,11 +337,16 @@ class TVMaze(callbacks.Plugin): # for now, defaults to only upcoming 'Scripted' shows if show['show']['type'] == 'Scripted' and pendulum.now(tz) <= time: shows.append(tmp) - + + # set a default message if no shows were found + if not shows: + shows.append('No upcoming shows found') + # finally reply reply = "{}: {}".format(self._ul("Today's Shows"), ", ".join(shows)) irc.reply(reply) + @wrap([getopts({'country': 'somethingWithoutSpaces', 'tz': 'somethingWithoutSpaces', 'showEpisodeTitle': 'boolean', From 8aceab73f1893479cd2bfd78e71e59e0d216a7da Mon Sep 17 00:00:00 2001 From: cottongin Date: Sat, 5 Jan 2019 10:17:15 -0600 Subject: [PATCH 14/15] adds --search and --record options to tvshow --- plugin.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/plugin.py b/plugin.py index 0f62f77..2c4c7e9 100644 --- a/plugin.py +++ b/plugin.py @@ -95,7 +95,9 @@ class TVMaze(callbacks.Plugin): @wrap([getopts({'country': 'somethingWithoutSpaces', 'detail': '', - 'd': ''}), 'text']) + 'd': '', + 'search': '', + 'record': 'positiveInt'}), 'text']) def tvshow(self, irc, msg, args, options, query): """[--country | --detail|--d] Fetches information about provided TV Show from TVMaze.com. @@ -119,6 +121,32 @@ class TVMaze(callbacks.Plugin): irc.reply('Nothing found for your query: {}'.format(query)) return + # if the user is using --search let's just output the results + if options.get('search'): + results = [] + for idx, show in enumerate(show_search): + # try to pin the year of release to the show name + if show['show'].get('premiered'): + premiered = show['show']['premiered'][:4] + else: + premiered = "TBD" + name = "{} ({})".format(show['show']['name'], premiered) + results.append("{}. {}".format( + idx+1, + self._bold(name) + )) + irc.reply("Results: {}".format(" | ".join(results))) + return + + # pull a specific show from --search results + if options.get('record'): + if options.get('record') > len(show_search): + irc.reply('Invalid record!') + return + result_to_show = options.get('record') - 1 + else: + result_to_show = 0 + # if we have a country, look for that first instead of the first result if country: show_id = None @@ -129,9 +157,9 @@ class TVMaze(callbacks.Plugin): break # if we can't find it, default to the first result anyway if not show_id: - show_id = show_search[0]['show']['id'] + show_id = show_search[result_to_show]['show']['id'] else: - show_id = show_search[0]['show']['id'] + show_id = show_search[result_to_show]['show']['id'] # fetch the show information show_info = self._get('shows', id_=show_id) @@ -261,7 +289,8 @@ class TVMaze(callbacks.Plugin): 'network': 'somethingWithoutSpaces', 'country': 'somethingWithoutSpaces', 'date': 'somethingWithoutSpaces', - 'showEpisodeTitle': ''})]) + 'showEpisodeTitle': '', + 'debug': ''})]) def schedule(self, irc, msg, args, options): """[--all | --tz | --network | --country | --date ] Fetches upcoming TV schedule from TVMaze.com. @@ -344,6 +373,9 @@ class TVMaze(callbacks.Plugin): # finally reply reply = "{}: {}".format(self._ul("Today's Shows"), ", ".join(shows)) + if options.get('debug'): + #irc.reply(repr(reply)) + print(repr(reply)) irc.reply(reply) From 9417cc1aa858f408afbd92ffef8139388cbedf7a Mon Sep 17 00:00:00 2001 From: oddluck Date: Thu, 5 Dec 2019 07:18:17 +0000 Subject: [PATCH 15/15] Initial commit. --- LICENSE => TVMaze/LICENSE | 0 README.md => TVMaze/README.md | 7 ++----- __init__.py => TVMaze/__init__.py | 4 +++- accountsdb.py => TVMaze/accountsdb.py | 0 config.py => TVMaze/config.py | 0 plugin.py => TVMaze/plugin.py | 0 requirements.txt => TVMaze/requirements.txt | 0 test.py => TVMaze/test.py | 0 8 files changed, 5 insertions(+), 6 deletions(-) rename LICENSE => TVMaze/LICENSE (100%) rename README.md => TVMaze/README.md (92%) rename __init__.py => TVMaze/__init__.py (86%) rename accountsdb.py => TVMaze/accountsdb.py (100%) rename config.py => TVMaze/config.py (100%) rename plugin.py => TVMaze/plugin.py (100%) rename requirements.txt => TVMaze/requirements.txt (100%) rename test.py => TVMaze/test.py (100%) diff --git a/LICENSE b/TVMaze/LICENSE similarity index 100% rename from LICENSE rename to TVMaze/LICENSE diff --git a/README.md b/TVMaze/README.md similarity index 92% rename from README.md rename to TVMaze/README.md index 0adb685..01a3aee 100644 --- a/README.md +++ b/TVMaze/README.md @@ -6,10 +6,7 @@ #### This plugin requires Python 3 and Limnoria -1. Clone this repository into your bot/plugins directory -``` -git clone --depth=1 https://gitlab.com/cottongin/TVMaze.git -``` +1. Install with PluginDownloader @install oddluck TVMaze 2. Install requirements for the plugin via pip ``` @@ -42,4 +39,4 @@ You can use @settvmazeoptions to save common command options to make using comma This stores settings per nick, you can clear them via --clear: ``` @settvmazeoptions --clear -``` \ No newline at end of file +``` diff --git a/__init__.py b/TVMaze/__init__.py similarity index 86% rename from __init__.py rename to TVMaze/__init__.py index 17d4fd1..ec5fd2e 100644 --- a/__init__.py +++ b/TVMaze/__init__.py @@ -20,13 +20,15 @@ __version__ = "" # XXX Replace this with an appropriate author or supybot.Author instance. __author__ = supybot.Author('cottongin', 'cottongin', 'cottongin@cottongin.club') +__maintainer__ = getattr(supybot.authors, 'oddluck', + supybot.Author('oddluck', 'oddluck', 'oddluck@riseup.net')) # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} # This is a url where the most recent plugin package can be downloaded. -__url__ = 'https://gitlab.com/cottongin/TVMaze' +__url__ = 'https://github.com/oddluck/limnoria-plugins/' from . import config from . import plugin diff --git a/accountsdb.py b/TVMaze/accountsdb.py similarity index 100% rename from accountsdb.py rename to TVMaze/accountsdb.py diff --git a/config.py b/TVMaze/config.py similarity index 100% rename from config.py rename to TVMaze/config.py diff --git a/plugin.py b/TVMaze/plugin.py similarity index 100% rename from plugin.py rename to TVMaze/plugin.py diff --git a/requirements.txt b/TVMaze/requirements.txt similarity index 100% rename from requirements.txt rename to TVMaze/requirements.txt diff --git a/test.py b/TVMaze/test.py similarity index 100% rename from test.py rename to TVMaze/test.py