import re
from datetime import datetime
from loguru import logger
from sqlalchemy import Column, DateTime, Integer, String
from flexget import plugin
from flexget.db_schema import versioned_base
from flexget.event import event
from flexget.utils import requests
logger = logger.bind(name='myepisodes')
Base = versioned_base('myepisodes', 0)
[docs]
class MyEpisodesInfo(Base):
__tablename__ = 'myepisodes'
id = Column(Integer, primary_key=True)
series_name = Column(String, unique=True)
myepisodes_id = Column(Integer, unique=True)
updated = Column(DateTime)
def __init__(self, series_name, myepisodes_id):
self.series_name = series_name
self.myepisodes_id = myepisodes_id
self.updated = datetime.now()
def __repr__(self):
return (
f'<MyEpisodesInfo(series_name={self.series_name}, myepisodes_id={self.myepisodes_id})>'
)
[docs]
class MyEpisodes:
"""Marks a series episode as acquired in your myepisodes.com account.
Simple Example:
Most shows are recognized automatically from their TVDBname.
And of course the plugin needs to know your MyEpisodes.com account details.
tasks:
tvshows:
myepisodes:
username: <username>
password: <password>
series:
- human target
- chuck
Advanced Example:
In some cases, the TVDB name is either not unique or won't even be discovered.
In that case you need to specify the MyEpisodes id manually using the set plugin.
.. code:: yaml
tasks:
tvshows:
myepisodes:
username: <username>
password: <password>
series:
- human target:
set:
myepisodes_id: 5111
- chuck
How to find the MyEpisodes id: http://matrixagents.org/screencasts/myep_example-20110507-131555.png
"""
schema = {
'type': 'object',
'properties': {'username': {'type': 'string'}, 'password': {'type': 'string'}},
'required': ['username', 'password'],
'additionalProperties': False,
}
def __init__(self):
self.plugin_config = None
self.db_session = None
self.test_mode = None
self.http_session = None
[docs]
@plugin.priority(plugin.PRIORITY_LAST)
def on_task_output(self, task, config):
"""Mark all accepted episodes as acquired on MyEpisodes."""
if not task.accepted:
# Nothing accepted, don't do anything
return
try:
self.plugin_config = config
self.db_session = task.session
self.test_mode = task.options.test
# attempt authentication
self.http_session = self._login(config)
except plugin.PluginWarning as w:
logger.warning(w)
return
except plugin.PluginError as e:
logger.error(e)
return
for entry in task.accepted:
# mark the accepted entries as acquired
try:
self._validate_entry(entry)
entry['myepisodes_id'] = self._lookup_myepisodes_id(entry)
self._mark_episode_acquired(entry)
except plugin.PluginWarning as w:
logger.warning(w)
[docs]
def _validate_entry(self, entry):
"""Check an entry for all of the fields needed to communicate with myepidoes.
Return: boolean
"""
if (
'series_season' not in entry
or 'series_episode' not in entry
or 'series_name' not in entry
):
raise plugin.PluginWarning(
"Can't mark entry `{}` in myepisodes without series_season, series_episode and series_name "
'fields'.format(entry['title']),
logger,
)
[docs]
def _lookup_myepisodes_id(self, entry):
"""Attempt to find the myepisodes id for the series.
Return: myepisode id or None
"""
# Do we already have the id?
myepisodes_id = entry.get('myepisodes_id')
if myepisodes_id:
return myepisodes_id
# have we previously recorded the id for this series?
myepisodes_id = self._retrieve_id_from_database(entry)
if myepisodes_id:
return myepisodes_id
# We don't know the id for this series, so it's time to search myepisodes.com for it
myepisodes_id = self._retrieve_id_from_website(entry)
if myepisodes_id:
return myepisodes_id
raise plugin.PluginWarning(
'Unable to determine the myepisodes id for: `{}`'.format(entry['title']), logger
)
[docs]
def _retrieve_id_from_database(self, entry):
"""Attempt to find the myepisodes id in the database.
Return: myepisode id or None
"""
lc_series_name = entry['series_name'].lower()
info = (
self.db_session
.query(MyEpisodesInfo)
.filter(MyEpisodesInfo.series_name == lc_series_name)
.first()
)
if info:
return info.myepisodes_id
return None
[docs]
def _retrieve_id_from_website(self, entry):
"""Attempt to find the myepisodes id for the series for the website itself.
Return: myepisode id or None
"""
myepisodes_id = None
baseurl = 'http://www.myepisodes.com/search/'
search_value = self._generate_search_value(entry)
payload = {'tvshow': search_value, 'action': 'Search'}
try:
response = self.http_session.post(baseurl, data=payload)
regex = r'"/epsbyshow\/([0-9]*)\/.*">' + search_value + '</a>'
match_obj = re.search(regex, response.text, re.MULTILINE | re.IGNORECASE)
if match_obj:
myepisodes_id = match_obj.group(1)
self._save_id(search_value, myepisodes_id)
except requests.RequestException as e:
raise plugin.PluginError(f'Error searching for myepisodes id: {e}')
return myepisodes_id
[docs]
def _generate_search_value(self, entry):
"""Find the TVDB name for searching myepisodes with.
myepisodes.com is backed by tvrage, so this will not be perfect.
Return: myepisode id or None
"""
search_value = entry['series_name']
# Get the series name from thetvdb to increase match chance on myepisodes
if entry.get('tvdb_series_name'):
search_value = entry['tvdb_series_name']
else:
try:
series = plugin.get('api_tvdb', self).lookup_series(
name=entry['series_name'], tvdb_id=entry.get('tvdb_id')
)
search_value = series.name
except LookupError:
logger.warning(
'Unable to lookup series `{}` from tvdb, using raw name.', entry['series_name']
)
return search_value
[docs]
def _save_id(self, series_name, myepisodes_id):
"""Save the myepisodes id in the database.
This will help prevent unecceary communication with the website
"""
# if we already have the a record for that id, update the name so that we find it next time
db_item = (
self.db_session
.query(MyEpisodesInfo)
.filter(MyEpisodesInfo.myepisodes_id == myepisodes_id)
.first()
)
if db_item:
logger.info(
'Changing name to `{}` for series with myepisodes_id {}',
series_name.lower(),
myepisodes_id,
)
db_item.series_name = series_name.lower()
else:
self.db_session.add(MyEpisodesInfo(series_name.lower(), myepisodes_id))
[docs]
def _mark_episode_acquired(self, entry):
"""Mark episode as acquired.
Required entry fields:
- series_name
- series_season
- series_episode
Raises:
PluginWarning if operation fails
"""
url = 'http://www.myepisodes.com/ajax/service.php?mode=eps_update'
myepisodes_id = entry['myepisodes_id']
season = entry['series_season']
episode = entry['series_episode']
super_secret_code = f'A{myepisodes_id!s}-{season!s}-{episode!s}'
payload = {super_secret_code: 'true'}
if self.test_mode:
logger.info(
'Would mark {} of `{}` as acquired.', entry['series_id'], entry['series_name']
)
return
try:
self.http_session.post(url, data=payload)
except requests.RequestException:
raise plugin.PluginError(
'Failed to mark {} of `{}` as acquired.'.format(
entry['series_id'], entry['series_name']
)
)
logger.info('Marked {} of `{}` as acquired.', entry['series_id'], entry['series_name'])
[docs]
def _login(self, config):
"""Authenticate with the myepisodes service and return a requests session.
Return:
requests session
Raises:
PluginWarning if login fails
PluginError if http communication fails
"""
url = 'https://www.myepisodes.com/login.php'
session = requests.Session()
payload = {
'username': config['username'],
'password': config['password'],
'action': 'Login',
}
try:
response = session.post(url, data=payload)
if 'login' in response.url:
raise plugin.PluginWarning(
(
'Login to myepisodes.com failed, please see if the site is down and verify '
'your credentials.'
),
logger,
)
except requests.RequestException as e:
raise plugin.PluginError(f'Error logging in to myepisodes: {e}')
return session
[docs]
@event('plugin.register')
def register_plugin():
plugin.register(MyEpisodes, 'myepisodes', api_ver=2)