Repository: Flexget/Flexget

Issue:
Seen forget --tasks behaviour
### Expected behaviour:

`flexget seen forget --tasks "link-*` should remove everything from all tasks matching the glob. It does not run.

### Behaviour:

`flexget seen forget: error: the following arguments are required: <value>`

Also arg name `--tasks` does not match with exec which is not in plural.


Discussion:
`--tasks` is a flag for `flexget execute`, not `seen`. You just do `flexget seen forget task-name`, it doesn't take glob patterns though

EDIT: CLI built-in documentation and https://flexget.com/CLI/seen are conflicting
Look either `flexget seen forget -h` or code. It does have `--tasks`, it just doesn't seem to work properly.
I did, hence the edit before 😅


This issue is stale because it has been open 150 days with no activity. Remove stale label or comment or this will be closed in 30 days.
Hmm. `flexget seen forget [--help] [--tasks TASK [TASK ...]] <value>` is the help text, which means you need to specify value after the --tasks parameter. The problem is that you can't actually do that, because --tasks will consume multiple task names if specified. You can do it if you put <value> before --tasks, but the help text certainly doesn't help you figure that out.

I think we should change to --task, and make it only consume one value. Then you'd do `flexget seen forget --task task1 --task task2 some.title.here` to forget from multiple tasks.

It's also really confusing that `<value>` could be a task name, which is a totally separate thing than `--tasks`, and `<value>` doesn't take wildcards.

The thing you were trying to do, forget everything from many tasks matching a wildcard, isn't supported at all ATM, regardless of the deficiencies above. Somebody should really sort this out. 🤷‍♂️ 
This issue is stale because it has been open 150 days with no activity. Remove stale label or comment or this will be closed in 30 days.
This issue is stale because it has been open 150 days with no activity. Remove stale label or comment or this will be closed in 30 days.

Resolving patch:
diff --git a/flexget/components/seen/cli.py b/flexget/components/seen/cli.py
--- a/flexget/components/seen/cli.py
+++ b/flexget/components/seen/cli.py
@@ -1,3 +1,5 @@
+from rich.markup import escape
+
 from flexget import options, plugin
 from flexget.event import event
 from flexget.manager import Manager
@@ -30,14 +32,17 @@ def seen_forget(manager: Manager, options):
             forget_name = imdb_id
 
     tasks = None
-    if options.tasks:
+    if options.task:
         tasks = []
-        for task in options.tasks:
+        for task in options.task:
             try:
                 tasks.extend(m for m in manager.matching_tasks(task) if m not in tasks)
             except ValueError as e:
                 console(e)
                 continue
+        if not tasks:
+            console('None of the specified tasks exist.')
+            return
 
     # If tasks are specified it should use pattern matching as search
     if tasks:
@@ -61,21 +66,30 @@ def seen_add(manager: Manager, options):
         else:
             console("Could not parse IMDB ID")
 
-    task = DEFAULT_TASK
-    local = None
-    if options.task and options.task not in manager.tasks:
-        console(f"Task `{options.task}` not in config")
-        return
-    else:
-        task = options.task
+    if options.task:
+        tasks = []
         local = True
+        for task in options.task:
+            if task not in manager.tasks:
+                console(f"Task `{task}` not in config")
+                continue
+            tasks.append(task)
+        if not tasks:
+            console('No specified tasks in config.')
+            return
+    else:
+        tasks = [DEFAULT_TASK]
+        local = None
 
-    db.add(seen_name, task, {'cli_add': seen_name}, local=local)
+    for task in tasks:
+        db.add(seen_name, task, {'cli_add': seen_name}, local=local)
 
-    if task == DEFAULT_TASK:
+    if tasks == [DEFAULT_TASK]:
         console(f'Added `{seen_name}` as seen. This will affect all tasks.')
     else:
-        console(f'Added `{seen_name}` as seen. This will affect `{task}` task.')
+        console(
+            f'Added `{seen_name}` as seen. This will affect {", ".join(f"`{t}`" for t in tasks)} task(s).'
+        )
 
 
 @with_session
@@ -93,9 +107,9 @@ def seen_search(manager: Manager, options, session=None):
         search_term = search_term.replace("*", "%").replace("?", "_")
 
     tasks = None
-    if options.tasks:
+    if options.task:
         tasks = []
-        for task in options.tasks:
+        for task in options.task:
             try:
                 tasks.extend(m for m in manager.matching_tasks(task) if m not in tasks)
             except ValueError as e:
@@ -105,7 +119,7 @@ def seen_search(manager: Manager, options, session=None):
     seen_entries = db.search(value=search_term, status=None, tasks=tasks, session=session)
     table = TerminalTable('Field', 'Value', table_type=options.table_type)
     for se in seen_entries.all():
-        table.add_row('Title', se.title)
+        table.add_row('Title', escape(se.title))
         for sf in se.fields:
             if sf.field.lower() == 'title':
                 continue
@@ -130,8 +144,8 @@ def register_parser_arguments():
         'forget', help='Forget entry or entire task from seen plugin database'
     )
     forget_parser.add_argument(
-        '--tasks',
-        nargs='+',
+        '--task',
+        action='append',
         metavar='TASK',
         help='forget only in specified task(s), optionally using glob patterns ("tv-*"). '
         'matching is case-insensitive',
@@ -145,6 +159,7 @@ def register_parser_arguments():
     add_parser = subparsers.add_parser('add', help='Add a title or url to the seen database')
     add_parser.add_argument(
         '--task',
+        action='append',
         metavar='TASK',
         help='add in specified task'
         'matching is case-insensitive. Will make entry local to that task',
@@ -154,8 +169,8 @@ def register_parser_arguments():
         'search', help='Search text from the seen database', parents=[table_parser]
     )
     search_parser.add_argument(
-        '--tasks',
-        nargs='+',
+        '--task',
+        action='append',
         metavar='TASK',
         help='search only in specified task(s), optionally using glob patterns ("tv-*"). '
         'matching is case-insensitive',
diff --git a/flexget/components/seen/db.py b/flexget/components/seen/db.py
--- a/flexget/components/seen/db.py
+++ b/flexget/components/seen/db.py
@@ -36,7 +36,6 @@
 except ImportError:
     raise plugin.DependencyError(issued_by=__name__, missing='imdb')
 
-
 logger = logger.bind(name='seen.db')
 Base = db_schema.versioned_base('seen', 4)
 
@@ -170,7 +169,7 @@ def forget(value, tasks=None, test=False):
     """
     See module docstring
 
-    :param string value: Can be task name, entry title or field value
+    :param string value: Can be entry title or field value
     :return: count, field_count where count is number of entries removed and field_count number of fields
     """
     with Session() as session:
@@ -186,18 +185,15 @@ def forget(value, tasks=None, test=False):
             tasks = None
 
         if tasks:
-            query_se = (
-                session.query(SeenEntry)
-                .filter(SeenEntry.title.like(value, escape=ESCAPE_QUERY))
-                .filter(SeenEntry.task.in_(tasks))
-            )
+            query_se = session.query(SeenEntry).filter(SeenEntry.task.in_(tasks))
+            # If we just got a wildcard no need for further filtering
+            if value != '%':
+                query_se = query_se.filter(SeenEntry.title.like(value, escape=ESCAPE_QUERY))
             query_sf = session.query(SeenField).filter(
                 SeenField.value.like(value, escape=ESCAPE_QUERY)
             )
         else:
-            query_se = session.query(SeenEntry).filter(
-                or_(SeenEntry.title == value, SeenEntry.task == value)
-            )
+            query_se = session.query(SeenEntry).filter(SeenEntry.title == value)
             query_sf = session.query(SeenField).filter(SeenField.value == value)
 
         for se in query_se.all():
@@ -213,7 +209,7 @@ def forget(value, tasks=None, test=False):
                 session.delete(se)
 
         for sf in query_sf.all():
-            se = session.query(SeenEntry).filter(SeenEntry.id == sf.seen_entry_id).first()
+            se = sf.seen_entry
             if tasks and se.task not in tasks:
                 continue
             field_count += len(se.fields)


Test patch: