Repository: sissbruecker/linkding (python)

Issue:
User Agent, or, Plans for LD_SINGLEFILE_OPTIONS
I see you stubbed out this config variable `LD_SINGLEFILE_OPTIONS` but aren't using it. One use case I have for it is passing user agent:

Does not work:
`single-file "https://reddit.com/" r.html`

Yields (truncated output): 
```
<body>
 <h1>whoa there, pardner!</h1>
<p>Your request has been blocked due to a network policy.</p>
<p>Additionally make sure your User-Agent is not empty and is something unique and descriptive and try again.</p>
```

Works: 
`single-file "https://reddit.com/" --user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0" r2.html`

Before I start work on it for a pull request, do you have any design requirements? Any specific arguments you want promoted to their own environment variable? 

Ref https://github.com/sissbruecker/linkding/blob/edd958fff61a640cf02c7640b717e78f10322b8b/bookmarks/services/singlefile.py#L20-L23

Pull request description:
Add option for passing arguments to single-file command
Closes #690 

Needs some additional testing:
- base.py already looks for LD_SINGLEFILE_OPTIONS and defaults to "" when null; shlex.split("") returns [], so change should work when env variable unset
- Tested with `--user-agent <foo>` and it worked on reddit (see linked issue)
- Needs testing with multiple arguments, e.g. `--user-agent <foo> --block-videos=true --block-audios=true`

Patch:
diff --git a/bookmarks/frontend/components/TagAutocomplete.svelte b/bookmarks/frontend/components/TagAutocomplete.svelte
index 5e2d736..6a46bee 100644
--- a/bookmarks/frontend/components/TagAutocomplete.svelte
+++ b/bookmarks/frontend/components/TagAutocomplete.svelte
@@ -150,17 +150,27 @@
         display: block;
     }
 
+    .form-autocomplete-input {
+        box-sizing: border-box;
+        height: var(--control-size);
+        min-height: var(--control-size);
+        padding: 0;
+    }
+
+    .form-autocomplete-input input {
+        width: 100%;
+        height: 100%;
+        border: none;
+        margin: 0;
+    }
+
     .form-autocomplete.small .form-autocomplete-input {
         height: var(--control-size-sm);
         min-height: var(--control-size-sm);
-        padding: 0.05rem 0.3rem;
     }
 
     .form-autocomplete.small .form-autocomplete-input input {
-        width: 100%;
-        height: 100%;
-        margin: 0;
-        padding: 0;
+        padding: 0.05rem 0.3rem;
         font-size: var(--font-size-sm);
     }
 
diff --git a/bookmarks/services/singlefile.py b/bookmarks/services/singlefile.py
index 755cffc..346ec63 100644
--- a/bookmarks/services/singlefile.py
+++ b/bookmarks/services/singlefile.py
@@ -1,6 +1,7 @@
 import gzip
 import logging
 import os
+import shlex
 import shutil
 import signal
 import subprocess
@@ -17,10 +18,11 @@ logger = logging.getLogger(__name__)
 
 def create_snapshot(url: str, filepath: str):
     singlefile_path = settings.LD_SINGLEFILE_PATH
-    # singlefile_options = settings.LD_SINGLEFILE_OPTIONS
+    # parse string to list of arguments
+    singlefile_options = shlex.split(settings.LD_SINGLEFILE_OPTIONS)
     temp_filepath = filepath + ".tmp"
-
-    args = [singlefile_path, url, temp_filepath]
+    # concat lists
+    args = [singlefile_path] + singlefile_options + [url, temp_filepath]
     try:
         # Use start_new_session=True to create a new process group
         process = subprocess.Popen(args, start_new_session=True)
diff --git a/docs/Options.md b/docs/Options.md
index e5c9fae..688e42f 100644
--- a/docs/Options.md
+++ b/docs/Options.md
@@ -252,4 +252,14 @@ Alternative favicon providers:
 
 Values: `Float` | Default =  60.0
 
-When creating archive snapshots, control the timeout for how long to wait for `single-file` to complete, in `seconds`. Defaults to 60 seconds; on lower-powered hardware you may need to increase this value. 
\ No newline at end of file
+When creating HTML archive snapshots, control the timeout for how long to wait for the snapshot to complete, in `seconds`.
+Defaults to 60 seconds; on lower-powered hardware you may need to increase this value. 
+
+### `LD_SINGLEFILE_OPTIONS`
+
+Values: `String` | Default = None
+
+When creating HTML archive snapshots, pass additional options to the `single-file` application that is used to create snapshots.
+See `single-file --help` for complete list of arguments, or browse source: https://github.com/gildas-lormeau/single-file-cli/blob/master/options.js
+
+Example: `LD_SINGLEFILE_OPTIONS=--user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0"`


Test patch:
diff --git a/bookmarks/tests/test_singlefile_service.py b/bookmarks/tests/test_singlefile_service.py
index f433776..ad0bec3 100644
--- a/bookmarks/tests/test_singlefile_service.py
+++ b/bookmarks/tests/test_singlefile_service.py
@@ -51,6 +51,44 @@ class SingleFileServiceTestCase(TestCase):
             with self.assertRaises(singlefile.SingeFileError):
                 singlefile.create_snapshot("http://example.com", self.html_filepath)
 
+    def test_create_snapshot_empty_options(self):
+        mock_process = mock.Mock()
+        mock_process.wait.return_value = 0
+        self.create_test_file()
+
+        with mock.patch("subprocess.Popen") as mock_popen:
+            singlefile.create_snapshot("http://example.com", self.html_filepath)
+
+            expected_args = [
+                "single-file",
+                "http://example.com",
+                self.html_filepath + ".tmp",
+            ]
+            mock_popen.assert_called_with(expected_args, start_new_session=True)
+
+    @override_settings(
+        LD_SINGLEFILE_OPTIONS='--some-option "some value" --another-option "another value" --third-option="third value"'
+    )
+    def test_create_snapshot_custom_options(self):
+        mock_process = mock.Mock()
+        mock_process.wait.return_value = 0
+        self.create_test_file()
+
+        with mock.patch("subprocess.Popen") as mock_popen:
+            singlefile.create_snapshot("http://example.com", self.html_filepath)
+
+            expected_args = [
+                "single-file",
+                "--some-option",
+                "some value",
+                "--another-option",
+                "another value",
+                "--third-option=third value",
+                "http://example.com",
+                self.html_filepath + ".tmp",
+            ]
+            mock_popen.assert_called_with(expected_args, start_new_session=True)
+
     def test_create_snapshot_default_timeout_setting(self):
         mock_process = mock.Mock()
         mock_process.wait.return_value = 0