Repository: scrapy/scrapy

Issue:
ImagesPipeline.thumb_path should allow access to item
## Summary

ImagesPipeline should pass on item to thumb_path function as additional argument.

## Motivation

The ImagesPipeline.file_path function allows access to item. So when an user overwrites this function and makes the file name
depended on the item itself, it makes sense to have the thumb path to be depended on the item as well.

This way main image file and thumbnail image file can have the same name and it is easily possible to jump between different sizes of the image.

Example of how a file and thumbnail could be build:
```python
file_path = f"full/{item['ID']}.jpg"
thumb_path = f"thumbs/small/{item['ID']}.jpg"
```

So I suggest to change the signature of ImagesPipeline.thumb_path to the following:
```python
def thumb_path(self, request, thumb_id, response=None, info=None, *, item=None):
```
which makes the functions thumb_path and file_path more consistent.

## Describe alternatives you've considered

As a workaround when I overwrite `file_path` I set `request._item = item` which I can then access in `thumb_path` function.

## Additional context

In case this is of interest I am happy to work on a pull request.

ImagesPipeline.thumb_path should allow access to item
## Summary

ImagesPipeline should pass on item to thumb_path function as additional argument.

## Motivation

The ImagesPipeline.file_path function allows access to item. So when an user overwrites this function and makes the file name
depended on the item itself, it makes sense to have the thumb path to be depended on the item as well.

This way main image file and thumbnail image file can have the same name and it is easily possible to jump between different sizes of the image.

Example of how a file and thumbnail could be build:
```python
file_path = f"full/{item['ID']}.jpg"
thumb_path = f"thumbs/small/{item['ID']}.jpg"
```

So I suggest to change the signature of ImagesPipeline.thumb_path to the following:
```python
def thumb_path(self, request, thumb_id, response=None, info=None, *, item=None):
```
which makes the functions thumb_path and file_path more consistent.

## Describe alternatives you've considered

As a workaround when I overwrite `file_path` I set `request._item = item` which I can then access in `thumb_path` function.

## Additional context

In case this is of interest I am happy to work on a pull request.



Discussion:

Resolving patch:
diff --git a/scrapy/pipelines/images.py b/scrapy/pipelines/images.py
--- a/scrapy/pipelines/images.py
+++ b/scrapy/pipelines/images.py
@@ -141,7 +141,7 @@ def get_images(self, response, request, info, *, item=None):
         yield path, image, buf
 
         for thumb_id, size in self.thumbs.items():
-            thumb_path = self.thumb_path(request, thumb_id, response=response, info=info)
+            thumb_path = self.thumb_path(request, thumb_id, response=response, info=info, item=item)
             thumb_image, thumb_buf = self.convert_image(image, size)
             yield thumb_path, thumb_image, thumb_buf
 
@@ -179,6 +179,6 @@ def file_path(self, request, response=None, info=None, *, item=None):
         image_guid = hashlib.sha1(to_bytes(request.url)).hexdigest()
         return f'full/{image_guid}.jpg'
 
-    def thumb_path(self, request, thumb_id, response=None, info=None):
+    def thumb_path(self, request, thumb_id, response=None, info=None, *, item=None):
         thumb_guid = hashlib.sha1(to_bytes(request.url)).hexdigest()
         return f'thumbs/{thumb_id}/{thumb_guid}.jpg'
diff --git a/scrapy/pipelines/media.py b/scrapy/pipelines/media.py
--- a/scrapy/pipelines/media.py
+++ b/scrapy/pipelines/media.py
@@ -121,7 +121,7 @@ def _process_request(self, request, info, item):
     def _make_compatible(self):
         """Make overridable methods of MediaPipeline and subclasses backwards compatible"""
         methods = [
-            "file_path", "media_to_download", "media_downloaded",
+            "file_path", "thumb_path", "media_to_download", "media_downloaded",
             "file_downloaded", "image_downloaded", "get_images"
         ]
 


Test patch:
diff --git a/tests/test_pipeline_images.py b/tests/test_pipeline_images.py
--- a/tests/test_pipeline_images.py
+++ b/tests/test_pipeline_images.py
@@ -93,6 +93,22 @@ def test_thumbnail_name(self):
                                     info=object()),
                          'thumbs/50/850233df65a5b83361798f532f1fc549cd13cbe9.jpg')
 
+    def test_thumbnail_name_from_item(self):
+        """
+        Custom thumbnail name based on item data, overriding default implementation
+        """
+
+        class CustomImagesPipeline(ImagesPipeline):
+            def thumb_path(self, request, thumb_id, response=None, info=None, item=None):
+                return f"thumb/{thumb_id}/{item.get('path')}"
+
+        thumb_path = CustomImagesPipeline.from_settings(Settings(
+            {'IMAGES_STORE': self.tempdir}
+        )).thumb_path
+        item = dict(path='path-to-store-file')
+        request = Request("http://example.com")
+        self.assertEqual(thumb_path(request, 'small', item=item), 'thumb/small/path-to-store-file')
+
     def test_convert_image(self):
         SIZE = (100, 100)
         # straigh forward case: RGB and JPEG
diff --git a/tests/test_pipeline_media.py b/tests/test_pipeline_media.py
--- a/tests/test_pipeline_media.py
+++ b/tests/test_pipeline_media.py
@@ -1,4 +1,5 @@
 from typing import Optional
+import io
 
 from testfixtures import LogCapture
 from twisted.trial import unittest
@@ -355,9 +356,12 @@ def __init__(self, *args, **kwargs):
 
     def get_media_requests(self, item, info):
         item_url = item['image_urls'][0]
+        output_img = io.BytesIO()
+        img = Image.new('RGB', (60, 30), color='red')
+        img.save(output_img, format='JPEG')
         return Request(
             item_url,
-            meta={'response': Response(item_url, status=200, body=b'data')}
+            meta={'response': Response(item_url, status=200, body=output_img.getvalue())}
         )
 
     def inc_stats(self, *args, **kwargs):
@@ -379,9 +383,13 @@ def file_path(self, request, response=None, info=None):
         self._mockcalled.append('file_path')
         return super(MockedMediaPipelineDeprecatedMethods, self).file_path(request, response, info)
 
+    def thumb_path(self, request, thumb_id, response=None, info=None):
+        self._mockcalled.append('thumb_path')
+        return super(MockedMediaPipelineDeprecatedMethods, self).thumb_path(request, thumb_id, response, info)
+
     def get_images(self, response, request, info):
         self._mockcalled.append('get_images')
-        return []
+        return super(MockedMediaPipelineDeprecatedMethods, self).get_images(response, request, info)
 
     def image_downloaded(self, response, request, info):
         self._mockcalled.append('image_downloaded')
@@ -392,7 +400,11 @@ class MediaPipelineDeprecatedMethodsTestCase(unittest.TestCase):
     skip = skip_pillow
 
     def setUp(self):
-        self.pipe = MockedMediaPipelineDeprecatedMethods(store_uri='store-uri', download_func=_mocked_download_func)
+        self.pipe = MockedMediaPipelineDeprecatedMethods(
+            store_uri='store-uri',
+            download_func=_mocked_download_func,
+            settings=Settings({"IMAGES_THUMBS": {'small': (50, 50)}})
+        )
         self.pipe.open_spider(None)
         self.item = dict(image_urls=['http://picsum.photos/id/1014/200/300'], images=[])
 
@@ -444,6 +456,16 @@ def test_file_path_called(self):
         )
         self._assert_method_called_with_warnings('file_path', message, warnings)
 
+    @inlineCallbacks
+    def test_thumb_path_called(self):
+        yield self.pipe.process_item(self.item, None)
+        warnings = self.flushWarnings([MediaPipeline._compatible])
+        message = (
+            'thumb_path(self, request, thumb_id, response=None, info=None) is deprecated, '
+            'please use thumb_path(self, request, thumb_id, response=None, info=None, *, item=None)'
+        )
+        self._assert_method_called_with_warnings('thumb_path', message, warnings)
+
     @inlineCallbacks
     def test_get_images_called(self):
         yield self.pipe.process_item(self.item, None)