Repository: Flexget/Flexget

Issue:
season_packs doesn't reject episodes if in the same feed and with target and timeframe
### Expected behaviour:

According to the documentation, season_packs: yes, should accept seasons packs if no other episode is downloaded, and reject episodes if the season pack is downloaded.

### Actual behaviour:

This works if the season pack and the episode are not both in the same input feed and not with timeframe and target.

If both appear at the same time, **and the series contains timeframe and target**, they are both accepted. Flexget should only accept one of them.

### Steps to reproduce:
- Step 1: Test That Works with normal series configuration
`      flexget --debug execute --task "xTV_Test_season_pack_1" --inject "Lost S01 720p hdtv h264" --inject "Lost S01E01 720 hdtv h264"`
    
```
2021-02-11 16:49:22 DEBUG    series        xTV_Test_season_pack_1 current: <Season(id=54,identifier=S01,season=1,completed=False)>
2021-02-11 16:49:22 VERBOSE  task          xTV_Test_season_pack_1 ACCEPTED: `Lost S01 720p hdtv h264` by series plugin because matches quality
2021-02-11 16:49:22 DEBUG    series        xTV_Test_season_pack_1 adding season number `1` to accepted seasons for this task
2021-02-11 16:49:22 DEBUG    series        xTV_Test_season_pack_1 start with entities: ['Lost S01E01 720 hdtv h264']
2021-02-11 16:49:22 VERBOSE  task          xTV_Test_season_pack_1 REJECTED: `Lost S01E01 720 hdtv h264` by series plugin because already accepted season pack for season `1` in this task

```

- Step 2: Forget the Season
  flexget series forget "Lost (2004)"

- Step 3: Test with timeframe and target

`      flexget --debug execute --task "xTV_Test_season_pack_2" --inject "Lost S01 720p hdtv h264" --inject "Lost S01E01 720 hdtv h264"`

```
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 Accepted by series. `Lost S01 720p hdtv h264` meets quality requirement `720p webrip+ h264+`.
2021-02-11 16:50:50 VERBOSE  task          xTV_Test_season_pack_2 ACCEPTED: `Lost S01 720p hdtv h264` by series plugin because target quality
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 start with entities: ['Lost S01E01 720 hdtv h264']
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 current entities: ['Lost S01E01 720 hdtv h264']
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 quality req: 720p|1080p webrip+
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 -------------------- process_propers -->
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 propers - downloaded qualities: {}
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 continuing w. entities: ['Lost S01E01 720 hdtv h264']
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 best entity is: `Lost S01E01 720 hdtv h264`
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 -------------------- tracking -->
2021-02-11 16:50:50 DEBUG    series.db     xTV_Test_season_pack_2 no episodes found for series `Lost (2004)` with parameters season: None, downloaded: True
2021-02-11 16:50:50 DEBUG    series.db     xTV_Test_season_pack_2 no season packs found for series `Lost (2004)` with parameters season: None, downloaded: True
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 latest download: None
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 current: <Episode(id=6159,identifier=S01E01,season=1,number=1)>
2021-02-11 16:50:50 DEBUG    series        xTV_Test_season_pack_2 Accepted by series. `Lost S01E01 720 hdtv h264` meets quality requirement `720p webrip+ h264+`.
2021-02-11 16:50:50 VERBOSE  task          xTV_Test_season_pack_2 ACCEPTED: `Lost S01E01 720 hdtv h264` by series plugin because target quality
```



#### Config:
```yaml
  xTV_Test_season_pack_1:
    disable:
      - remember_rejected
      - seen
      - retry_failed
      - seen_info_hash
    series:
      - Lost (2004):
          identified_by: ep
          quality: 720p|1080p webrip+     
          season_packs: true
    thetvdb_lookup: true
    trakt_lookup: true



  xTV_Test_season_pack_2:
    disable:
      - remember_rejected
      - seen
      - retry_failed
      - seen_info_hash
    series:
      - Lost (2004):
          identified_by: ep
          quality: 720p|1080p webrip+
          timeframe: 4 hours
          target: 720p webrip+ h264+      
          season_packs: true
    thetvdb_lookup: true
    trakt_lookup: true
```
</details>

### Additional information:

- FlexGet version: 3.1.102
- Python version: Python 3.7.3
- Installation method: VENV
- Using daemon (yes/no): yes (but tested also without daemon)
- OS and version: Debian 10



Discussion:
 Nice catch on this, I see the root cause. Accepting the entity using the timeframe accept the entry at a separate code path, one that doesn't add its season to accepted season list.

Resolving patch:
diff --git a/flexget/components/series/series.py b/flexget/components/series/series.py
--- a/flexget/components/series/series.py
+++ b/flexget/components/series/series.py
@@ -5,6 +5,7 @@
 from collections import defaultdict
 from copy import copy
 from datetime import datetime
+from typing import List, Union
 
 from loguru import logger
 from sqlalchemy import not_
@@ -622,7 +623,22 @@ def process_series(self, task, series_entries, config):
         :param series_entries: dict mapping Episodes or Seasons to entries for that episode or season_pack
         :param config: Series configuration
         """
-        accepted_seasons = []
+        accepted_seasons: List[int] = []
+
+        def _exclude_season_on_accept(
+            *args,
+            series_entity: Union[db.Season, db.Episode],
+            accepted_seasons_list: List[int],
+            **kwargs,
+        ) -> None:
+            # need to reject all other episode/season packs for an accepted season during the task,
+            # can't wait for task learn phase
+            if series_entity.is_season:
+                logger.debug(
+                    'adding season number `{}` to accepted seasons for this task',
+                    series_entity.season,
+                )
+                accepted_seasons_list.append(series_entity.season)
 
         # sort for season packs first, order by season number ascending. Uses -1 in case entity does not return a
         # season number or sort will crash
@@ -632,6 +648,15 @@ def process_series(self, task, series_entries, config):
             if not entries:
                 continue
 
+            # Add season exclude hook to all entries so it will get added to list in all code paths of entry acceptance
+            for entry in entries:
+                entry.add_hook(
+                    action='accept',
+                    func=_exclude_season_on_accept,
+                    series_entity=entity,
+                    accepted_seasons_list=accepted_seasons,
+                )
+
             reason = None
 
             logger.debug('start with entities: {}', [e['title'] for e in entries])
@@ -652,7 +677,7 @@ def process_series(self, task, series_entries, config):
             # Determine episode threshold for season pack
             ep_threshold = season_packs['threshold'] if season_packs else 0
 
-            # check that a season ack for this season wasn't already accepted in this task run
+            # check that a season pack for this season wasn't already accepted in this task run
             if entity.season in accepted_seasons:
                 for entry in entries:
                     entry.reject(
@@ -777,14 +802,6 @@ def process_series(self, task, series_entries, config):
             reason = reason or 'choosing first acceptable match'
             best.accept(reason)
 
-            # need to reject all other episode/season packs for an accepted season during the task,
-            # can't wait for task learn phase
-            if entity.is_season:
-                logger.debug(
-                    'adding season number `{}` to accepted seasons for this task', entity.season
-                )
-                accepted_seasons.append(entity.season)
-
     def process_propers(self, config, episode, entries):
         """
         Accepts needed propers. Nukes episodes from which there exists proper.


Test patch:
diff --git a/flexget/tests/test_series.py b/flexget/tests/test_series.py
--- a/flexget/tests/test_series.py
+++ b/flexget/tests/test_series.py
@@ -2495,6 +2495,35 @@ def test_advanced_config(self, execute_task):
         assert task.find_entry('accepted', title='bro.s02.720p.HDTV-Flexget')
 
 
+class TestSeriesSeasonPackAdvanced:
+    _config = """
+    tasks:
+      timeframe_and_target:
+        parsing:
+          series: internal
+        mock:
+          - title: "foo S01 720p hdtv h264"
+          - title: "foo S01E01 720p hdtv h264"
+        series:
+          - foo:
+              identified_by: ep
+              quality: 720p|1080p webrip+
+              timeframe: 4 hours
+              target: 720p webrip+ h264+      
+              season_packs: true
+    """
+
+    @pytest.fixture()
+    def config(self):
+        """Overrides outer config fixture since season pack support does not work with guessit parser"""
+        return self._config
+
+    def test_season_pack_with_timeframe_and_target(self, execute_task):
+        task = execute_task('timeframe_and_target')
+        assert task.find_entry('accepted', title="foo S01 720p hdtv h264")
+        assert not task.find_entry('accepted', title="foo S01E01 720p hdtv h264")
+
+
 class TestSeriesDDAudio:
     _config = """
       templates: