Repository: sissbruecker/linkding (python)

Issue:
[Bug] Videos attached to bookmarks no longer play when clicking “View” in Chrome

**Description**
Videos uploaded as attachments (e.g. mp4) previously opened in a new tab and played automatically in Chrome when clicking the "View" button. This worked I think recently (last week or week before I think).

Same videos now fail to play. Uploaded images and PDFs continue to open correctly. 

**Steps to reproduce**
1. Upload an MP4 video as a bookmark attachment.
2. Click "View" to open the video.
3. Video is "fetched" with a 200, but video fails to play

**Observed behavior**
- Network tab shows the browser able to get the asset with a 200, then a 404. It sends a `Range: bytes=0-` header.
- Linkding container logs show:
    ```
    200 OK for initial request
    WARNING Not Found: /assets/<id>
    404 for subsequent range requests
    ```
- Chrome (and I think Edge) won't play the video; Firefox plays it.
- I'm still able to right click View, Save Link As, and download the video 

**Expected behavior**
- Videos should open in a new tab and start playing automatically
- Seeking / partial playback should work.

**Environment**
- Linkding version: latest @ v1.44.2
- Chrome version: Version 137.0.7151.104 (Official Build) (arm64) -- doesn't play the video 
- Firefox: 146.0.1 -- plays the video

**Likely Cause** 
- I'm not too sure, but looks like it could be due to browsers now requiring range and content-range headers for video playback.


Patch:
diff --git a/bookmarks/views/assets.py b/bookmarks/views/assets.py
index 89ff1c6a..688ec051 100644
--- a/bookmarks/views/assets.py
+++ b/bookmarks/views/assets.py
@@ -33,7 +33,10 @@ def view(request, asset_id: int):
 
     response = HttpResponse(content, content_type=asset.content_type)
     response["Content-Disposition"] = f'inline; filename="{asset.download_name}"'
-    response["Content-Security-Policy"] = "sandbox allow-scripts"
+    if asset.content_type and asset.content_type.startswith("video/"):
+        response["Content-Security-Policy"] = "default-src 'none'; media-src 'self';"
+    else:
+        response["Content-Security-Policy"] = "sandbox allow-scripts"
     return response
 
 


Test patch:
diff --git a/bookmarks/tests/test_bookmark_asset_view.py b/bookmarks/tests/test_bookmark_asset_view.py
index f65937db..bca1aee5 100644
--- a/bookmarks/tests/test_bookmark_asset_view.py
+++ b/bookmarks/tests/test_bookmark_asset_view.py
@@ -27,14 +27,14 @@ def setup_asset_with_file(self, bookmark):
         )
         return asset
 
-    def setup_asset_with_uploaded_file(self, bookmark):
+    def setup_asset_with_uploaded_file(self, bookmark, content_type="image/png"):
         filename = f"temp_{bookmark.id}.png.gzip"
         self.setup_asset_file(filename)
         asset = self.setup_asset(
             bookmark=bookmark,
             file=filename,
             asset_type=BookmarkAsset.TYPE_UPLOAD,
-            content_type="image/png",
+            content_type=content_type,
             display_name=f"Uploaded file {bookmark.id}.png",
         )
         return asset
@@ -164,3 +164,17 @@ def test_uploaded_file_download_headers(self):
             f'inline; filename="{asset.display_name}"',
         )
         self.assertEqual(response["Content-Security-Policy"], "sandbox allow-scripts")
+
+    def test_uploaded_video_download_headers(self):
+        bookmark = self.setup_bookmark()
+        asset = self.setup_asset_with_uploaded_file(bookmark, content_type="video/mp4")
+        response = self.client.get(reverse("linkding:assets.view", args=[asset.id]))
+
+        self.assertEqual(response["Content-Type"], asset.content_type)
+        self.assertEqual(
+            response["Content-Disposition"],
+            f'inline; filename="{asset.display_name}"',
+        )
+        self.assertEqual(
+            response["Content-Security-Policy"], "default-src 'none'; media-src 'self';"
+        )