Repository: Flexget/Flexget

Issue:
Search plugins storing BeautifulSoup objects in Entry cause excessive recursion in Entry::take_snapshot
### Expected behaviour:

Flexget doesn't crash.
### Actual behaviour:

Flexget crashes due to excessive recursion depth while taking the snapshot of an entry produced by the TorrentLeech search plugin.
### Steps to reproduce:

Use the TorrentLeech search plugin in a task.
#### Config (snippet):

```
tasks:
  tv-discover:
    interval: 12 hours
    discover:
      what:
        - next_series_episodes: yes
      from:
        - torrentleech:
            rss_key: "{{ secrets.torrentleech.rsskey }}"
            username: "{{ secrets.torrentleech.username }}"
            password: "{{ secrets.torrentleech.password }}"
            category: Episodes HD
    transmission: yes
```
### Additional information:
- Flexget Version: 2.2.1
- Python Version: 2.7.12
- Installation method: pip
- OS and version: Fedora 24 x86_64
- Link to crash log: https://gist.github.com/danielkza/d6e8c98b31037de731c5b5ac76733b39
### Debugging information

I'm not familiar with the Flexget codebase, but some quick debugging shows the problem is caused by the TL plugin inserting some text directly from the HTML tree produced by BeautifulSoup. That text is actually a `bs4.element.NavigableString` that contains references to the whole tree, which Flexget attempts to serialize when saving a snapshot of the entry.

The plugin should convert any such usages to `unicode` before storing it in the entry. It is possible other search plugins have the same issue, so a generic solution might be desirable. 



Discussion:
Do you know which entry field that gets the navigablestring or is it just all of them?

It seems to be the `title` field. Applying the change I sent in #1302 after cleaning the database fixed the issue for me. 


Resolving patch:
diff --git a/flexget/entry.py b/flexget/entry.py
--- a/flexget/entry.py
+++ b/flexget/entry.py
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals, division, absolute_import
 from builtins import *  # pylint: disable=unused-import, redefined-builtin
-from future.utils import PY2, native_str
+from future.utils import PY2, native_str, text_type
 
 import copy
 import functools
@@ -195,6 +195,10 @@ def __setitem__(self, key, value):
                 raise EntryUnicodeError(key, value)
         elif isinstance(value, bytes):
             raise EntryUnicodeError(key, value)
+        # Coerce any enriched strings (such as those returned by BeautifulSoup) to plain strings to avoid serialization
+        # troubles.
+        elif isinstance(value, text_type) and type(value) != text_type:  # pylint: disable=unidiomatic-typecheck
+            value = text_type(value)
 
         # url and original_url handling
         if key == 'url':


Test patch:
diff --git a/flexget/tests/test_misc.py b/flexget/tests/test_misc.py
--- a/flexget/tests/test_misc.py
+++ b/flexget/tests/test_misc.py
@@ -1,5 +1,8 @@
+# pylint: disable=no-self-use
+
 from __future__ import unicode_literals, division, absolute_import
 from builtins import *  # pylint: disable=unused-import, redefined-builtin
+from future.utils import text_type
 
 import os
 import stat
@@ -150,6 +153,16 @@ def test_encoding(self):
             e['invalid'] = b'\x8e'
 
 
+class TestEntryStringCoercion(object):
+    def test_coercion(self):
+        class EnrichedString(text_type):
+            pass
+
+        e = Entry('title', 'url')
+        e['test'] = EnrichedString("test")
+        assert type(e['test']) == text_type  # pylint: disable=unidiomatic-typecheck
+
+
 class TestFilterRequireField(object):
     config = """
         tasks: