Showing with 105 additions and 102 deletions.
  1. +31 −28 flexget/plugins/api/series.py
  2. +16 −9 flexget/plugins/cli/series.py
  3. +6 −5 flexget/plugins/filter/seen.py
  4. +27 −32 flexget/plugins/filter/series.py
  5. +5 −5 flexget/plugins/output/series_forget.py
  6. +5 −5 tests/test_series.py
  7. +15 −18 tests/test_series_api.py
@@ -318,9 +318,9 @@ def get_series_details(show):
series_list_parser.add_argument('lookup', choices=('tvdb', 'tvmaze'), action='append',
help="Get lookup result for every show by sending another request to lookup API")


ep_identifier_doc = "'episode_identifier' should be one of SxxExx, integer or date formatted such as 2012-12-12"


@series_api.route('/')
class SeriesListAPI(APIResource):
@api.response(404, 'Page does not exist', default_error_schema)
@@ -463,6 +463,12 @@ def get(self, name, session):
})


delete_parser = api.parser()
delete_parser.add_argument('forget', type=inputs.boolean, default=False,
help="Enabling this will fire a 'forget' event that will delete the downloaded releases "
"from the entire DB, enabling to re-download them")


@series_api.route('/<int:show_id>')
@api.doc(params={'show_id': 'ID of the show'})
class SeriesShowAPI(APIResource):
@@ -484,7 +490,8 @@ def get(self, show_id, session):

@api.response(200, 'Removed series from DB', empty_response)
@api.response(404, 'Show ID not found', default_error_schema)
@api.doc(description='Delete a specific show using its ID')
@api.doc(description='Delete a specific show using its ID',
parser=delete_parser)
def delete(self, show_id, session):
""" Remove series from DB """
try:
@@ -495,9 +502,9 @@ def delete(self, show_id, session):
}, 404

name = show.name

args = delete_parser.parse_args()
try:
series.forget_series(name)
series.remove_series(name, forget=args.get('forget'))
except ValueError as e:
return {'status': 'error',
'message': e.args[0]
@@ -605,32 +612,27 @@ def get(self, show_id, session):

@api.response(500, 'Error when trying to forget episode', default_error_schema)
@api.response(200, 'Successfully forgotten all episodes from show', empty_response)
@api.doc(description='Delete all show episodes via its ID. Deleting an episode will mark it as wanted again')
@api.doc(description='Delete all show episodes via its ID. Deleting an episode will mark it as wanted again',
parser=delete_parser)
def delete(self, show_id, session):
""" Forgets all episodes of a show"""
""" Deletes all episodes of a show"""
try:
show = series.show_by_id(show_id, session=session)
except NoResultFound:
return {'status': 'error',
'message': 'Show with ID %s not found' % show_id
}, 404

for episode in show.episodes:
try:
series.forget_episodes_by_id(show.id, episode.id)
except ValueError as e:
return {'status': 'error',
'message': e.args[0]
}, 500
name = show.name
args = delete_parser.parse_args()
try:
series.remove_series(name, forget=args.get('forget'))
except ValueError as e:
return {'status': 'error',
'message': e.args[0]
}, 404
return {}


delete_parser = api.parser()
delete_parser.add_argument('delete_seen', type=inputs.boolean, default=False,
help="Enabling this will delete all the related releases from seen entries list as well, "
"enabling to re-download them")


@api.response(404, 'Show ID not found', default_error_schema)
@api.response(414, 'Episode ID not found', default_error_schema)
@api.response(400, 'Episode with ep_ids does not belong to show with show_id', default_error_schema)
@@ -685,21 +687,22 @@ def delete(self, show_id, ep_id, session):
'message': 'Episode with id %s does not belong to show %s' % (ep_id, show_id)}, 400

args = delete_parser.parse_args()
if args.get('delete_seen'):
for release in episode.releases:
fire_event('forget', release.title)

series.forget_episodes_by_id(show_id, ep_id)
try:
series.remove_series_episode(show.name, episode.identifier, args.get('forget'))
except ValueError as e:
return {'status': 'error',
'message': e.args[0]
}, 404
return {}


release_list_parser = api.parser()
release_list_parser.add_argument('downloaded', type=inputs.boolean, help='Filter between release status')

release_delete_parser = release_list_parser.copy()
release_delete_parser.add_argument('delete_seen', type=inputs.boolean, default=False,
help="Enabling this will delete all the related releases from seen entries list as well, "
"enabling to re-download them")
release_delete_parser.add_argument('forget', type=inputs.boolean, default=False,
help="Enabling this will for 'forget' event that will delete the downloaded"
" releases from the entire DB, enabling to re-download them")


@api.response(404, 'Show ID not found', default_error_schema)
@@ -9,7 +9,7 @@
from flexget.manager import Session

try:
from flexget.plugins.filter.series import (Series, forget_series, forget_series_episode, set_series_begin,
from flexget.plugins.filter.series import (Series, remove_series, remove_series_episode, set_series_begin,
normalize_series_name, new_eps_after, get_latest_release,
get_series_summary, shows_by_name, show_episodes, shows_by_exact_name)
except ImportError:
@@ -22,8 +22,10 @@ def do_cli(manager, options):
display_summary(options)
elif options.series_action == 'show':
display_details(options.series_name)
elif options.series_action == 'remove':
remove(manager, options)
elif options.series_action == 'forget':
forget(manager, options)
remove(manager, options, forget=True)
elif options.series_action == 'begin':
begin(manager, options)

@@ -119,29 +121,30 @@ def begin(manager, options):
manager.config_changed()


def forget(manager, options):
def remove(manager, options, forget=False):
name = options.series_name

if options.episode_id:
# remove by id
identifier = options.episode_id
try:
forget_series_episode(name, identifier)
remove_series_episode(name, identifier, forget)
console('Removed episode `%s` from series `%s`.' % (identifier, name.capitalize()))
except ValueError:
# Try upper casing identifier if we fail at first
try:
forget_series_episode(name, identifier.upper())
remove_series_episode(name, identifier.upper(), forget)
console('Removed episode `%s` from series `%s`.' % (identifier, name.capitalize()))
except ValueError as e:
console(e.message)
console(e.args[0])

else:
# remove whole series
try:
forget_series(name)
remove_series(name, forget)
console('Removed series `%s` from database.' % name.capitalize())
except ValueError as e:
console(e.message)
console(e.args[0])

manager.config_changed()

@@ -254,5 +257,9 @@ def register_parser_arguments():
help='episode ID to start getting the series from (e.g. S02E01, 2013-12-11, or 9, '
'depending on how the series is numbered)')
forget_parser = subparsers.add_parser('forget', parents=[series_parser],
help='removes episodes or whole series from the series database')
help='removes episodes or whole series from the entire database '
'(including seen plugin)')
forget_parser.add_argument('episode_id', nargs='?', default=None, help='episode ID to forget (optional)')
delete_parser = subparsers.add_parser('remove', parents=[series_parser],
help='removes episodes or whole series from the series database only')
delete_parser.add_argument('episode_id', nargs='?', default=None, help='episode ID to forget (optional)')
@@ -132,23 +132,23 @@ def forget(value):
:param string value: Can be task name, entry title or field value
:return: count, field_count where count is number of entries removed and field_count number of fields
"""
log.debug('forget called with %s' % value)
with Session() as session:
log.debug('forget called with %s', value)
count = 0
field_count = 0
for se in session.query(SeenEntry).filter(or_(SeenEntry.title == value, SeenEntry.task == value)).all():
field_count += len(se.fields)
count += 1
log.debug('forgetting %s' % se)
log.debug('forgetting %s', se)
session.delete(se)

for sf in session.query(SeenField).filter(SeenField.value == value).all():
se = session.query(SeenEntry).filter(SeenEntry.id == sf.seen_entry_id).first()
field_count += len(se.fields)
count += 1
log.debug('forgetting %s' % se)
log.debug('forgetting %s', se)
session.delete(se)
return count, field_count
return count, field_count


@with_session
@@ -338,7 +338,8 @@ def add(title, task_name, fields, reason=None, local=None, session=None):


@with_session
def search(value=None, status=None, start=None, stop=None, count=False, order_by='added', descending=False, session=None):
def search(value=None, status=None, start=None, stop=None, count=False, order_by='added', descending=False,
session=None):
query = session.query(SeenEntry)
if descending:
query = query.order_by(getattr(SeenEntry, order_by).desc())
@@ -15,7 +15,7 @@

from flexget import db_schema, options, plugin
from flexget.config_schema import one_or_more
from flexget.event import event
from flexget.event import event, fire_event
from flexget.manager import Session
from flexget.plugin import get_plugin_by_name
from flexget.plugins.parsers import SERIES_ID_TYPES
@@ -716,60 +716,55 @@ def set_series_begin(series, ep_id):
series.begin = episode


def forget_series(name):
"""Remove a whole series `name` from database."""
session = Session()
try:
def remove_series(name, forget=False):
"""
Remove a whole series `name` from database.
:param name: Name of series to be removed
:param forget: Indication whether or not to fire a 'forget' event
"""
downloaded_releases = []
with Session() as session:
series = session.query(Series).filter(Series.name == name).all()
if series:
for s in series:
if forget:
for episode in s.episodes:
downloaded_releases = [release.title for release in episode.downloaded_releases]
session.delete(s)
session.commit()
log.debug('Removed series %s from database.', name)
else:
raise ValueError('Unknown series %s' % name)
finally:
session.close()
for downloaded_release in downloaded_releases:
fire_event('forget', downloaded_release)


def forget_series_episode(name, identifier):
"""Remove all episodes by `identifier` from series `name` from database."""
session = Session()
try:
def remove_series_episode(name, identifier, forget=False):
"""
Remove all episodes by `identifier` from series `name` from database.
:param name: Name of series to be removed
:param identifier: Series identifier to be deleted
:param forget: Indication whether or not to fire a 'forget' event
"""
downloaded_releases = []
with Session() as session:
series = session.query(Series).filter(Series.name == name).first()
if series:
episode = session.query(Episode).filter(Episode.identifier == identifier). \
filter(Episode.series_id == series.id).first()
if episode:
if not series.begin:
series.identified_by = '' # reset identified_by flag so that it will be recalculated
if forget:
downloaded_releases = [release.title for release in episode.downloaded_releases]
session.delete(episode)
session.commit()
log.debug('Episode %s from series %s removed from database.', identifier, name)
else:
raise ValueError('Unknown identifier %s for series %s' % (identifier, name.capitalize()))
else:
raise ValueError('Unknown series %s' % name)
finally:
session.close()


def forget_episodes_by_id(series_id, episode_id):
""" Removes a specific episode using `series_id` and `episode_id`."""
with Session() as session:
series = session.query(Series).filter(Series.id == series_id).first()
if series:
episode = session.query(Episode).filter(Episode.id == episode_id).first()
if episode:
if not series.begin:
series.identified_by = '' # reset identified_by flag so that it will be recalculated
session.delete(episode)
session.commit()
log.debug('Episode %s from series %s removed from database.', episode_id, series_id)
else:
raise ValueError('Unknown identifier %s for series %s' % (episode_id, series_id))
else:
raise ValueError('Unknown series %s' % series_id)
for downloaded_release in downloaded_releases:
fire_event('forget', downloaded_release)


def delete_release_by_id(release_id):
@@ -6,15 +6,15 @@
from flexget.event import event

try:
from flexget.plugins.filter.series import forget_series_episode
from flexget.plugins.filter.series import remove_series_episode
except ImportError:
raise plugin.DependencyError(issued_by='series_forget', missing='series',
raise plugin.DependencyError(issued_by='series_remove', missing='series',
message='series_forget plugin need series plugin to work')

log = logging.getLogger('series_forget')


class OutputSeriesForget(object):
class OutputSeriesRemove(object):
schema = {'type': 'boolean'}

def on_task_output(self, task, config):
@@ -23,7 +23,7 @@ def on_task_output(self, task, config):
for entry in task.accepted:
if 'series_name' in entry and 'series_id' in entry:
try:
forget_series_episode(entry['series_name'], entry['series_id'])
remove_series_episode(entry['series_name'], entry['series_id'])
log.info('Removed episode `%s` from series `%s` download history.' %
(entry['series_id'], entry['series_name']))
except ValueError:
@@ -32,4 +32,4 @@ def on_task_output(self, task, config):

@event('plugin.register')
def register_plugin():
plugin.register(OutputSeriesForget, 'series_forget', api_ver=2)
plugin.register(OutputSeriesRemove, 'series_remove', api_ver=2)
@@ -2008,7 +2008,7 @@ def test_series_list(self, manager, execute_task):
assert all(any(line.lstrip().startswith(series) for line in lines) for series in ['Some Show', 'Other Show'])


class TestSeriesForget(object):
class TestSeriesRemove(object):
config = """
templates:
global:
@@ -2022,23 +2022,23 @@ class TestSeriesForget(object):
mock:
- title: My Show S01E01 1080p
- title: My Show S01E01 720p
forget_episode:
remove_episode:
seen: no
mock:
- title: My Show S01E01
series_name: My Show
series_id: S01E01
accept_all: yes
series_forget: yes
series_remove: yes
"""

def test_forget_episode(self, execute_task):
def test_remove_episode(self, execute_task):
task = execute_task('get_episode')
assert len(task.accepted) == 1
first_rls = task.accepted[0]
task = execute_task('get_episode')
assert not task.accepted, 'series plugin duplicate blocking not working?'
task = execute_task('forget_episode')
task = execute_task('remove_episode')
task = execute_task('get_episode')
assert len(task.accepted) == 1, 'new release not accepted after forgetting ep'
assert task.accepted[0] != first_rls, 'same release accepted on second run'