Repository: sissbruecker/linkding

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


Discussion:

Patch:
diff --git a/bookmarks/services/singlefile.py b/bookmarks/services/singlefile.py
index 755cffcb..346ec632 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 @@ class SingeFileError(Exception):
 
 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 e5c9faef..688e42fb 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 f4337764..ad0bec30 100644
--- a/bookmarks/tests/test_singlefile_service.py
+++ b/bookmarks/tests/test_singlefile_service.py
@@ -51,6 +51,44 @@ def test_create_snapshot_failure(self):
             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