├── .github └── workflows │ ├── python-publish.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── google.jpeg ├── google_images_search ├── __init__.py ├── cli.py ├── fetch_resize_save.py ├── google_api.py └── meta.py ├── setup.cfg ├── setup.py └── tests ├── test_fetch_resize_save.py └── test_google_api.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | push: 13 | branches: 14 | - master 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | deploy: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.9' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install build 34 | - name: Build package 35 | run: python -m build 36 | - name: Publish package 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.PYPI_USER_TOKEN }} 41 | skip-existing: true 42 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "develop" 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | python-version: [3.8, 3.9] 15 | 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Set Up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Display Python version 26 | run: python -c "import sys; print(sys.version)" 27 | 28 | - name: Install package 29 | run: python setup.py install 30 | 31 | - name: Install pytest 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install pytest 35 | 36 | - name: Run tests 37 | run: pytest -vvv tests/test* 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /Google_Images_Search.egg-info/ 3 | /build/ 4 | /dist/ 5 | /tests/__pycache__/ 6 | /.DS_Store 7 | .venv 8 | /tests/test_my.py 9 | /google_images_search/__pycache__/ 10 | /dwnld/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.4.6 4 | 5 | ### Fixes in 1.4.6 6 | - custom name bug for multiple files (discovered and fixed by [@Hussainity](https://github.com/Hussainity)) 7 | 8 | ## 1.4.5 9 | 10 | ### Fixes in 1.4.5 11 | - multiple search with different data without instantiating GIS object again (discovered by [@notchum](https://github.com/notchum)) 12 | 13 | ## 1.4.4 14 | 15 | ### Fixes in 1.4.4 16 | - Dependency version for Click 17 | 18 | ## 1.4.3 19 | 20 | ### Added in 1.4.3 21 | - Travis switched with GitHub Actions 22 | 23 | ### Fixed in 1.4.2 24 | 25 | - Updated version of google-api-python-client lib 26 | 27 | ## 1.4.2 28 | 29 | ### Fixed in 1.4.2 30 | 31 | - Bad referrer link fix 32 | - Image validation fix (discovered by [@SiewLinYap](https://github.com/SiewLinYap)) 33 | 34 | ### Added in 1.4.2 35 | 36 | - Requests browser simulation 37 | 38 | ## 1.4.1 39 | 40 | ### Fixed in 1.4.1 41 | 42 | - Search parameters aligned with Google's definitions (pull request by [@SiewLinYap](https://github.com/SiewLinYap)) 43 | 44 | ### Added in 1.4.1 45 | 46 | - Support for transparent images 47 | 48 | ## 1.4.0 49 | 50 | ### Added in 1.4.0 51 | 52 | - Image object now has a referrer url (source) as well 53 | 54 | ## 1.3.10 55 | 56 | ### Fixed in 1.3.10 57 | 58 | - Improved error handling when trying to fetch images 59 | 60 | ## 1.3.9 61 | 62 | ### Fixed in 1.3.9 63 | 64 | - Added "imgColorType" param for Google API search (pull request by [@SantaHey](https://github.com/SantaHey)) 65 | 66 | ## 1.3.8 67 | 68 | ### Fixed in 1.3.8 69 | 70 | - Security vulnerability found in Pillow. Update to version to 8.1.1. 71 | 72 | ## 1.3.7 73 | 74 | ### Fixed in 1.3.7 75 | 76 | - Handling CLI exception when api key and cx are not provided 77 | - Handling PIL open and rgb convert exception 78 | 79 | ## Added in 1.3.7 80 | 81 | - Curses terminal progress is now started and ended using context (with statement) 82 | - CLI also uses contextual progress 83 | - Better progress output in CLI overall 84 | 85 | ## 1.3.6 86 | 87 | ### Fixed in 1.3.6 88 | 89 | - CLI used non-null default params (discovered by [@itoche](https://github.com/itoche)) 90 | 91 | ## 1.3.5 92 | 93 | ### Fixed in 1.3.5 94 | 95 | - Pilow version updated (pull request by [@eladavron](https://github.com/eladavron)) 96 | - Fixed x-raw-image://urls (pull request by [@reteps](https://github.com/reteps)) 97 | 98 | ## 1.3.4 99 | 100 | ### Fixed in 1.3.4 101 | 102 | - Number of images limit would produce error if number was 20 or 30 or etc (issue discovered by [@gaarsmu](https://github.com/gaarsmu)) 103 | 104 | ## 1.3.3 105 | 106 | ### Fixed in 1.3.3 107 | 108 | - SSL check is back 109 | 110 | ## 1.3.2 111 | 112 | ### Fixed in 1.3.2 113 | 114 | - Some images failed to download (issue discovered by [@techguytechtips](https://github.com/techguytechtips)) 115 | 116 | ## 1.3.1 117 | 118 | ### Added in 1.3.1 119 | 120 | - Option to specify images usage rights (change made by [@bradleyfowler123](https://github.com/bradleyfowler123)) 121 | 122 | ## 1.3.0 123 | 124 | ### Added in 1.3.0 125 | 126 | - Removed Python 2.7 support 127 | 128 | ### Fixed in 1.3.0 129 | 130 | - Upgrade from Pillow 6.0 to 7.1.0 131 | - Fixed issue with downloading images with custom name 132 | 133 | ## 1.2.1 134 | 135 | ### Fixed in 1.2.1 136 | 137 | - If Google returns zero results, don't loop to get desired number of images. 138 | 139 | ## 1.2.0 140 | 141 | ### Added in 1.2.0 142 | 143 | - Ability to save save images with custom file name (change suggested by [@otsir](https://github.com/otsir)) 144 | 145 | ## 1.1.4 146 | 147 | ### Fixed in 1.1.4 148 | 149 | - Sometimes the lib would return more images then user would request. 150 | 151 | ## 1.1.3 152 | 153 | ### Fixed in 1.1.3 154 | 155 | - CLI was broken, so I fixed it. 156 | 157 | ## 1.1.2 158 | 159 | ### Added in 1.1.2 160 | 161 | - Due to the image validation, non-valid images are ignored, so is triggered again and again until desired number of images is reached (change suggested by [@Uskompuf](https://github.com/Uskompuf) 162 | 163 | ## 1.1.1 164 | 165 | ### Added in 1.1.1 166 | 167 | - Automatic paging (change suggested by [@Uskompuf](https://github.com/Uskompuf) 168 | - Image validation enable/disable 169 | 170 | ### Fixed in 1.1.1 171 | 172 | - Better exception handling during image check timeout. 173 | 174 | ## 1.1.0 175 | 176 | ### Added in 1.1.0 177 | 178 | - Google api search apgination using next searxh parameter (change suggested by [@Uskompuf](https://github.com/Uskompuf) 179 | 180 | ## 1.0.1 181 | 182 | ### Fixed in 1.0.1 183 | 184 | - Sometimes google api desn't return 'items' in response and code breaks. 185 | 186 | ## 1.0.0 187 | 188 | ### Added in 1.0.0 189 | - Multithreaded images downloading 190 | - Download progress bars 191 | - External progress bar insertion 192 | 193 | ## 0.3.8 194 | 195 | ### Fixed in 0.3.8 196 | - Non-alphanumeric characters removed from file names which are not valid characters in windows file names (change made by [@sebastianchr](https://github.com/sebastianchr) 197 | 198 | ## 0.3.7 199 | 200 | ### Fixed in 0.3.7 201 | - Code formatted. 202 | 203 | ## 0.3.6 204 | 205 | ### Added in 0.3.6 206 | - Cache_discovery option forward fix. 207 | 208 | ## 0.3.5 209 | 210 | ### Added in 0.3.5 211 | - Cache_discovery option for search method to control file_cache (change made by [@maredov](https://github.com/marodev)). 212 | 213 | ## 0.3.4 214 | 215 | ### Fixed added in 0.3.4 216 | - Dependencies versions updated (change made by [@maredov](https://github.com/marodev)) 217 | 218 | ## 0.3.3 219 | 220 | ### Fixed in 0.3.3 221 | - Travis CI definition for PyPi upload. 222 | 223 | ## 0.3.2 224 | 225 | ### Fixed in 0.3.2 226 | - API call default parameter changed from specific to blank (change made by [@mateusrangel](https://github.com/mateusrangel)). 227 | 228 | ## 0.3.1 229 | 230 | ### Added in 0.3.1 231 | - Class docstrings. 232 | 233 | ## 0.3.0 234 | 235 | ### Added in 0.3.0 236 | - Tests added. 237 | 238 | ## 0.2.0 239 | 240 | ### Added in 0.2.0 241 | - Saving to a BytesIO object (change made by [@fuchsia80](https://github.com/fuchsia80)). 242 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ivan Arar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the README 2 | include README.md 3 | 4 | # Include the license file 5 | include LICENSE 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Google Images Search](google.jpeg) 2 | 3 | # Google Images Search 4 | 5 | [![PyPI version](https://badge.fury.io/py/Google-Images-Search.svg)](https://badge.fury.io/py/Google-Images-Search) 6 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b3d5259c67ca48a7bfe844b9721b6c19)](https://www.codacy.com/app/arrrlo/Google-Images-Search?utm_source=github.com&utm_medium=referral&utm_content=arrrlo/Google-Images-Search&utm_campaign=Badge_Grade) 7 | 8 | ![GitHub issues](https://img.shields.io/github/issues/arrrlo/Google-Images-Search.svg) 9 | ![GitHub closed issues](https://img.shields.io/github/issues-closed/arrrlo/Google-Images-Search.svg) 10 | ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/arrrlo/Google-Images-Search.svg) 11 | 12 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/Google-Images-Search.svg) 13 | ![GitHub](https://img.shields.io/github/license/arrrlo/Google-Images-Search.svg?color=blue) 14 | ![GitHub last commit](https://img.shields.io/github/last-commit/arrrlo/Google-Images-Search.svg?color=blue) 15 | 16 | ## [Installation](#installation) 17 | 18 | To be able to use this library, you need to enable Google Custom Search API, generate API key credentials and set a project: 19 | 20 | - Visit [https://console.developers.google.com](https://console.developers.google.com) and create a project. 21 | 22 | - Visit [https://console.developers.google.com/apis/library/customsearch.googleapis.com](https://console.developers.google.com/apis/library/customsearch.googleapis.com) and enable "Custom Search API" for your project. 23 | 24 | - Visit [https://console.developers.google.com/apis/credentials](https://console.developers.google.com/apis/credentials) and generate API key credentials for your project. 25 | 26 | - Visit [https://cse.google.com/cse/all](https://cse.google.com/cse/all) and in the web form where you create/edit your custom search engine enable "Image search" option and for "Sites to search" option select "Search the entire web but emphasize included sites". 27 | 28 | After setting up your Google developers account and project you should have been provided with developers API key and project CX. 29 | 30 | Install package from pypi.org: 31 | 32 | ```bash 33 | > pip install Google-Images-Search 34 | ``` 35 | 36 | ## [CLI usage](#cli-usage) 37 | 38 | ```bash 39 | # without environment variables: 40 | 41 | > gimages -k __your_dev_api_key__ -c __your_project_cx__ search -q puppies 42 | ``` 43 | 44 | ```bash 45 | # with environment variables: 46 | 47 | > export GCS_DEVELOPER_KEY=__your_dev_api_key__ 48 | > export GCS_CX=__your_project_cx__ 49 | > 50 | > gimages search -q puppies 51 | ``` 52 | 53 | ```bash 54 | # search only (no download and resize): 55 | 56 | > gimages search -q puppies 57 | ``` 58 | 59 | ```bash 60 | # search and download only (no resize): 61 | 62 | > gimages search -q puppies -d /path/on/your/drive/where/images/should/be/downloaded 63 | ``` 64 | 65 | ```bash 66 | # search, download and resize: 67 | 68 | > gimages search -q puppies -d /path/ -w 500 -h 500 69 | ``` 70 | 71 | ## [Programmatic usage](#programmatic-usage) 72 | 73 | ```python 74 | from google_images_search import GoogleImagesSearch 75 | 76 | # you can provide API key and CX using arguments, 77 | # or you can set environment variables: GCS_DEVELOPER_KEY, GCS_CX 78 | gis = GoogleImagesSearch('your_dev_api_key', 'your_project_cx') 79 | 80 | # define search params 81 | # option for commonly used search param are shown below for easy reference. 82 | # For param marked with '##': 83 | # - Multiselect is currently not feasible. Choose ONE option only 84 | # - This param can also be omitted from _search_params if you do not wish to define any value 85 | _search_params = { 86 | 'q': '...', 87 | 'num': 10, 88 | 'fileType': 'jpg|gif|png', 89 | 'rights': 'cc_publicdomain|cc_attribute|cc_sharealike|cc_noncommercial|cc_nonderived', 90 | 'safe': 'active|high|medium|off|safeUndefined', ## 91 | 'imgType': 'clipart|face|lineart|stock|photo|animated|imgTypeUndefined', ## 92 | 'imgSize': 'huge|icon|large|medium|small|xlarge|xxlarge|imgSizeUndefined', ## 93 | 'imgDominantColor': 'black|blue|brown|gray|green|orange|pink|purple|red|teal|white|yellow|imgDominantColorUndefined', ## 94 | 'imgColorType': 'color|gray|mono|trans|imgColorTypeUndefined' ## 95 | } 96 | 97 | # this will only search for images: 98 | gis.search(search_params=_search_params) 99 | 100 | # this will search and download: 101 | gis.search(search_params=_search_params, path_to_dir='/path/') 102 | 103 | # this will search, download and resize: 104 | gis.search(search_params=_search_params, path_to_dir='/path/', width=500, height=500) 105 | 106 | # search first, then download and resize afterwards: 107 | gis.search(search_params=_search_params) 108 | for image in gis.results(): 109 | image.url # image direct url 110 | image.referrer_url # image referrer url (source) 111 | 112 | image.download('/path/') # download image 113 | image.resize(500, 500) # resize downloaded image 114 | 115 | image.path # downloaded local file path 116 | ``` 117 | 118 | ## [Custom file name](#custom-file-name) 119 | 120 | Sometimes you would want to save images with file name of your choice. 121 | 122 | ```python 123 | from google_images_search import GoogleImagesSearch 124 | 125 | gis = GoogleImagesSearch('your_dev_api_key', 'your_project_cx') 126 | 127 | _search_params = { ... } 128 | 129 | gis.search(search_params=_search_params, path_to_dir='...', 130 | custom_image_name='my_image') 131 | ``` 132 | 133 | ## [Paging](#paging) 134 | 135 | Google's API limit is 10 images per request. 136 | That means if you want 123 images, it will be divided internally into 13 requests. 137 | Keep in mind that getting 123 images will take a bit more time if the image validation is enabled. 138 | 139 | ```python 140 | from google_images_search import GoogleImagesSearch 141 | 142 | gis = GoogleImagesSearch('your_dev_api_key', 'your_project_cx') 143 | _search_params = { 144 | 'q': '...', 145 | 'num': 123, 146 | } 147 | 148 | # get first 123 images: 149 | gis.search(search_params=_search_params) 150 | 151 | # take next 123 images from Google images search: 152 | gis.next_page() 153 | for image in gis.results(): 154 | ... 155 | ``` 156 | 157 | ## [Image validation](#image-validation) 158 | 159 | Every image URL is validated by default. 160 | That means that every image URL will be checked if the headers can be fetched and validated. 161 | With that you don't need to wary about which image URL is actually downloadable or not. 162 | The downside is the time needed to validate. 163 | If you prefer, you can turn it off. 164 | 165 | ```python 166 | from google_images_search import GoogleImagesSearch 167 | 168 | # turn the validation off with "validate_images" agrument 169 | gis = GoogleImagesSearch('your_dev_api_key', 'your_project_cx', validate_images=False) 170 | ``` 171 | 172 | ## [Inserting custom progressbar function](#progressbar) 173 | 174 | By default, progressbar is not enabled. 175 | Only in CLI progressbar is enabled by default using [Curses library](https://docs.python.org/3/howto/curses.html). 176 | In a programmatic mode it can be enabled in two ways: 177 | - using contextual mode (Curses) 178 | - using your custom progressbar function 179 | 180 | ```python 181 | from google_images_search import GoogleImagesSearch 182 | 183 | # using your custom progressbar function 184 | def my_progressbar(url, progress): 185 | print(url + ' ' + progress + '%') 186 | gis = GoogleImagesSearch( 187 | 'your_dev_api_key', 'your_project_cx', progressbar_fn=my_progressbar 188 | ) 189 | _search_params = {...} 190 | gis.search(search_params=_search_params) 191 | 192 | # using contextual mode (Curses) 193 | with GoogleImagesSearch('your_dev_api_key', 'your_project_cx') as gis: 194 | _search_params = {...} 195 | gis.search(search_params=_search_params) 196 | ... 197 | ``` 198 | 199 | ## [Saving to a BytesIO object](#bytes-io) 200 | 201 | ```python 202 | from google_images_search import GoogleImagesSearch 203 | from io import BytesIO 204 | from PIL import Image 205 | 206 | # in this case we're using PIL to keep the BytesIO as an image object 207 | # that way we don't have to wait for disk save / write times 208 | # the image is simply kept in memory 209 | # this example should display 3 pictures of puppies! 210 | 211 | gis = GoogleImagesSearch('your_dev_api_key', 'your_project_cx') 212 | 213 | my_bytes_io = BytesIO() 214 | 215 | gis.search({'q': 'puppies', 'num': 3}) 216 | for image in gis.results(): 217 | # here we tell the BytesIO object to go back to address 0 218 | my_bytes_io.seek(0) 219 | 220 | # take raw image data 221 | raw_image_data = image.get_raw_data() 222 | 223 | # this function writes the raw image data to the object 224 | image.copy_to(my_bytes_io, raw_image_data) 225 | 226 | # or without the raw data which will be automatically taken 227 | # inside the copy_to() method 228 | image.copy_to(my_bytes_io) 229 | 230 | # we go back to address 0 again so PIL can read it from start to finish 231 | my_bytes_io.seek(0) 232 | 233 | # create a temporary image object 234 | temp_img = Image.open(my_bytes_io) 235 | 236 | # show it in the default system photo viewer 237 | temp_img.show() 238 | ``` 239 | -------------------------------------------------------------------------------- /google.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arrrlo/Google-Images-Search/37631698c28e5b4c0574060ad67408a503f9cbef/google.jpeg -------------------------------------------------------------------------------- /google_images_search/__init__.py: -------------------------------------------------------------------------------- 1 | from google_images_search.fetch_resize_save import \ 2 | FetchResizeSave as GoogleImagesSearch 3 | from google_images_search.google_api import GoogleBackendException 4 | -------------------------------------------------------------------------------- /google_images_search/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | import googleapiclient 3 | 4 | from .google_api import GoogleBackendException 5 | from .fetch_resize_save import FetchResizeSave, __version__ 6 | 7 | 8 | @click.group() 9 | @click.pass_context 10 | @click.option('-k', '--developer_key', help='Developer API key') 11 | @click.option('-c', '--custom_search_cx', help='Custom Search CX') 12 | def cli(ctx, developer_key, custom_search_cx): 13 | click.echo() 14 | click.secho(f'GOOGLE IMAGES SEARCH {__version__}', fg='yellow') 15 | click.echo() 16 | 17 | ctx.obj = { 18 | 'object': FetchResizeSave( 19 | developer_key, custom_search_cx 20 | ) 21 | } 22 | 23 | 24 | IMAGE_TYPES = ('clipart', 'face', 'lineart', 'stock', 'photo', 25 | 'animated', 'imgTypeUndefined') 26 | IMAGE_SIZES = ('huge', 'icon', 'large', 'medium', 'small', 27 | 'xlarge', 'xxlarge', 'imgSizeUndefined') 28 | FILE_TYPES = ('jpg', 'gif', 'png') 29 | DOMINANT_COLORS = ('black', 'blue', 'brown', 'gray', 'green', 'pink', 'purple', 30 | 'teal', 'white', 'yellow') 31 | SAFE_SEARCH = ('active', 'high', 'medium', 'off', 'safeUndefined') 32 | USAGE_RIGHTS = ('cc_publicdomain', 'cc_attribute', 'cc_sharealike', 33 | 'cc_noncommercial', 'cc_nonderived') 34 | 35 | 36 | @cli.command() 37 | @click.pass_context 38 | @click.option('-q', '--query', help='Search query') 39 | @click.option('-n', '--num', default=1, help='Number of images in response') 40 | @click.option('-s', '--safe', type=click.Choice(SAFE_SEARCH), 41 | default='off', help='Search safety level') 42 | @click.option('-f', '--filetype', type=click.Choice(FILE_TYPES), 43 | help='Images file type') 44 | @click.option('-i', '--imagetype', type=click.Choice(IMAGE_TYPES), 45 | help='Image type') 46 | @click.option('-s', '--imagesize', type=click.Choice(IMAGE_SIZES), 47 | help='Image size') 48 | @click.option('-c', '--dominantcolor', type=click.Choice(DOMINANT_COLORS), 49 | help='Dominant color in images') 50 | @click.option('-r', '--usagerights', type=click.Choice(USAGE_RIGHTS), 51 | multiple=True, help='Usage rights of images') 52 | @click.option('-d', '--download_path', type=click.Path(dir_okay=True), 53 | help='Download images') 54 | @click.option('-w', '--width', help='Image crop width') 55 | @click.option('-h', '--height', help='Image crop height') 56 | @click.option('-m', '--custom_file_name', help='Custom file name') 57 | def search(ctx, query, num, safe, filetype, imagetype, imagesize, 58 | dominantcolor, usagerights, download_path, width, height, 59 | custom_file_name): 60 | 61 | usagerights = '|'.join(usagerights) 62 | if imagesize: 63 | imagesize = imagesize.upper() 64 | search_params = { 65 | 'q': query, 66 | 'num': num, 67 | 'safe': safe, 68 | 'fileType': filetype, 69 | 'imgType': imagetype, 70 | 'rights': usagerights, 71 | 'imgSize': imagesize, 72 | 'imgDominantColor': dominantcolor 73 | } 74 | 75 | try: 76 | gis = ctx.obj['object'] 77 | 78 | with gis: 79 | gis.search(search_params, download_path, 80 | width, height, custom_file_name) 81 | 82 | results = ctx.obj['object'].results() 83 | 84 | if results: 85 | for image in results: 86 | click.echo(image.url) 87 | if image.path: 88 | click.secho(image.path, fg='blue') 89 | if not image.resized: 90 | click.secho('[image is not resized]', fg='red') 91 | else: 92 | click.secho('[image is not downloaded]', fg='red') 93 | click.echo() 94 | else: 95 | click.secho('No images found!', fg='red') 96 | 97 | except GoogleBackendException: 98 | click.secho('Error occurred trying to fetch ' 99 | 'images from Google. Please try again.', fg='red') 100 | 101 | except googleapiclient.errors.HttpError as e: 102 | click.secho(f'Google reported an error: {str(e)}', fg='red') 103 | return 104 | -------------------------------------------------------------------------------- /google_images_search/fetch_resize_save.py: -------------------------------------------------------------------------------- 1 | import os 2 | import curses 3 | import requests 4 | import threading 5 | from PIL import Image, UnidentifiedImageError 6 | from resizeimage import resizeimage, imageexceptions 7 | 8 | from .meta import __version__ 9 | from .google_api import GoogleCustomSearch 10 | 11 | 12 | IMAGES_NUM_LIMIT = 10 13 | 14 | class FetchResizeSave(object): 15 | """Class with resizing and downloading logic""" 16 | 17 | def __init__(self, developer_key, custom_search_cx, 18 | progressbar_fn=None, validate_images=True): 19 | 20 | # initialise google api 21 | self._google_custom_search = GoogleCustomSearch( 22 | developer_key, custom_search_cx, self) 23 | 24 | self._search_result = [] 25 | self.validate_images = validate_images 26 | 27 | self._stdscr = None 28 | self._progress = False 29 | self._chunk_sizes = {} 30 | self.zero_return = False 31 | self._terminal_lines = {} 32 | self._download_progress = {} 33 | self._search_for_more = False 34 | self._report_progress = progressbar_fn 35 | 36 | self._set_data() 37 | 38 | self._page = 1 39 | self._number_of_images = None 40 | 41 | if progressbar_fn: 42 | # user inserted progressbar fn 43 | self._progress = True 44 | 45 | def __enter__(self): 46 | """Entering a terminal window setup 47 | :return: self 48 | """ 49 | 50 | self._report_progress = self.__report_progress 51 | self._progress = True 52 | 53 | # set terminal screen 54 | self._stdscr = curses.initscr() 55 | self._stdscr.keypad(True) 56 | curses.cbreak() 57 | curses.noecho() 58 | 59 | # show terminal header information 60 | self._stdscr.addstr(0, 0, f'GOOGLE IMAGES SEARCH {__version__}') 61 | 62 | return self 63 | 64 | def __exit__(self, exc_type, exc_val, exc_tb): 65 | """Exiting terminal window and putting all back as it was 66 | :param exc_type: 67 | :param exc_val: 68 | :param exc_tb: 69 | :return: 70 | """ 71 | 72 | self._progress = False 73 | 74 | # reverse all as it was 75 | self._stdscr.keypad(False) 76 | curses.nocbreak() 77 | curses.echo() 78 | curses.endwin() 79 | 80 | def _set_data(self, search_params=None, path_to_dir=False, 81 | width=None, height=None, custom_image_name=None, cache_discovery=True): 82 | """Set data for Google api search, save and resize 83 | :param search_params: parameters for Google API Search 84 | :param path_to_dir: path where the images should be downloaded 85 | :param width: crop width of the images 86 | :param height: crop height of the images 87 | :param custom_image_name: define custom filename 88 | :param cache_discovery: whether or not to cache the discovery doc 89 | :return: None 90 | """ 91 | 92 | self._width = width 93 | self._height = height 94 | self._path_to_dir = path_to_dir 95 | self._search_params = search_params 96 | self._custom_image_name = custom_image_name 97 | self._cache_discovery = cache_discovery 98 | 99 | def _get_data(self): 100 | """Get data for Google api search, save and resize 101 | :return: tuple 102 | """ 103 | 104 | return self._search_params, \ 105 | self._path_to_dir, \ 106 | self._width,\ 107 | self._height,\ 108 | self._custom_image_name, \ 109 | self._cache_discovery 110 | 111 | def search(self, search_params, path_to_dir=False, width=None, 112 | height=None, custom_image_name=None, cache_discovery=False): 113 | """Fetched images using Google API and does the download and resize 114 | if path_to_dir and width and height variables are provided. 115 | :param search_params: parameters for Google API Search 116 | :param path_to_dir: path where the images should be downloaded 117 | :param width: crop width of the images 118 | :param height: crop height of the images 119 | :param custom_image_name: define custom filename 120 | :param cache_discovery: whether or not to cache the discovery doc 121 | :return: None 122 | """ 123 | 124 | if not self._search_for_more: 125 | self._set_data( 126 | search_params, path_to_dir, width, height, custom_image_name, 127 | cache_discovery 128 | ) 129 | self._search_result = [] 130 | 131 | # number of images required from lib user is important 132 | # save it only when searching for the first time 133 | if not self._number_of_images: 134 | self._number_of_images = search_params.get('num') or 1 135 | 136 | start = self._number_of_images * (self._page - 1) 137 | end = self._number_of_images * self._page 138 | 139 | for i, page in enumerate(range(start, end, IMAGES_NUM_LIMIT)): 140 | start = page+1 141 | 142 | if self._number_of_images >= IMAGES_NUM_LIMIT*(i+1): 143 | num = IMAGES_NUM_LIMIT 144 | else: 145 | num = (self._number_of_images % IMAGES_NUM_LIMIT) or \ 146 | self._number_of_images 147 | 148 | self._search_params['start'] = start 149 | self._search_params['num'] = num 150 | 151 | self._search_images(*self._get_data()) 152 | 153 | if len(self._search_result) >= self._number_of_images \ 154 | or self.zero_return: 155 | break 156 | else: 157 | # run search again if validation removed some images 158 | # and desired number of images haven't been reached 159 | self._next_page() 160 | 161 | self._search_result = self._search_result[:self._number_of_images] 162 | 163 | def _search_images(self, search_params, path_to_dir=False, width=None, 164 | height=None, _custom_image_name=None, cache_discovery=False): 165 | """Fetched images using Google API and does the download and resize 166 | if path_to_dir and width and height variables are provided. 167 | :param search_params: parameters for Google API Search 168 | :param path_to_dir: path where the images should be downloaded 169 | :param width: crop width of the images 170 | :param height: crop height of the images 171 | :param _custom_image_name: define custom filename 172 | :param cache_discovery: whether or not to cache the discovery doc 173 | :return: None 174 | """ 175 | 176 | i = 0 177 | threads = [] 178 | for url, referrer_url in self._google_custom_search.search( 179 | search_params, cache_discovery 180 | ): 181 | # initialise image object 182 | image = GSImage(self) 183 | image.url = url 184 | image.referrer_url = referrer_url 185 | 186 | # set thread safe variables 187 | self._download_progress[url] = 0 188 | self._terminal_lines[url] = i 189 | i += 2 190 | 191 | # set thread with function and arguments 192 | thread = threading.Thread( 193 | target=self._download_and_resize, 194 | args=(path_to_dir, image, width, height) 195 | ) 196 | 197 | # start thread 198 | thread.start() 199 | 200 | # register thread 201 | threads.append(thread) 202 | 203 | # wait for all threads to end here 204 | for thread in threads: 205 | thread.join() 206 | 207 | if self._progress: 208 | if self._stdscr: 209 | curses.endwin() 210 | 211 | def _next_page(self): 212 | """Run search again if validation removed some images 213 | and desired number of images haven't been reached 214 | :return: None 215 | """ 216 | 217 | # don't reset the data 218 | self._search_for_more = True 219 | 220 | # get new images 221 | self.next_page() 222 | 223 | # set reset flag 224 | self._search_for_more = False 225 | 226 | def next_page(self): 227 | """Get next batch of images. 228 | Number of images is defined with num search parameter. 229 | :return: None 230 | """ 231 | 232 | self._page += 1 233 | self.search(*self._get_data()) 234 | 235 | def set_chunk_size(self, url, content_size): 236 | """Set images chunk size according to its size 237 | :param url: image url 238 | :param content_size: image size 239 | :return: None 240 | """ 241 | 242 | self._chunk_sizes[url] = int(int(content_size) / 100) + 1 243 | 244 | def _download_and_resize(self, path_to_dir, image, width, height): 245 | """Method used for threading 246 | :param path_to_dir: path to download dir 247 | :param image: image object 248 | :param width: crop width 249 | :param height: crop height 250 | :return: None 251 | """ 252 | 253 | if path_to_dir: 254 | image.download(path_to_dir) 255 | if width and height: 256 | try: 257 | image.resize(width, height) 258 | except imageexceptions.ImageSizeError: 259 | pass 260 | 261 | self._search_result.append(image) 262 | 263 | def results(self): 264 | """Returns objects of downloaded images 265 | :return: list 266 | """ 267 | 268 | return self._search_result 269 | 270 | def download(self, url, path_to_dir): 271 | """Downloads image from url to path dir 272 | Used only by GSImage class 273 | :param url: image url 274 | :param path_to_dir: path to directory where image should be saved 275 | :return: path to image 276 | """ 277 | 278 | if not os.path.exists(path_to_dir): 279 | os.makedirs(path_to_dir) 280 | 281 | raw_filename = url.split('/')[-1].split('?')[0] 282 | basename, ext = os.path.splitext(raw_filename) 283 | 284 | if not ext: 285 | ext = '.jpg' 286 | 287 | if self._custom_image_name: 288 | def increment_naming(dir_list, name, number=0): 289 | if number: 290 | file_name = ''.join([name, '(', str(number), ')', ext]) 291 | else: 292 | file_name = ''.join([name, ext]) 293 | 294 | if file_name in dir_list: 295 | return increment_naming(dir_list, name, number+1) 296 | else: 297 | return file_name 298 | 299 | basename = increment_naming( 300 | os.listdir(path_to_dir), self._custom_image_name) 301 | else: 302 | basename = basename + ext 303 | 304 | path_to_image = os.path.join(path_to_dir, basename) 305 | 306 | with open(path_to_image, 'wb') as f: 307 | for chunk in self.get_raw_data(url): 308 | f.write(chunk) 309 | 310 | try: 311 | Image.open(path_to_image).convert('RGBA')\ 312 | .save(path_to_image, 'png') 313 | except UnidentifiedImageError: 314 | pass 315 | 316 | return path_to_image 317 | 318 | def get_raw_data(self, url): 319 | """Generator method for downloading images in chunks 320 | :param url: url to image 321 | :return: raw image data 322 | """ 323 | 324 | # simulate browser request 325 | headers = { 326 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64) ' 327 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 328 | 'Chrome/27.0.1453.94 ' 329 | 'Safari/537.36' 330 | } 331 | with requests.get(url, stream=True, headers=headers) as req: 332 | for chunk in req.iter_content(chunk_size=self._chunk_sizes.get(url)): 333 | 334 | # filter out keep-alive new chunks 335 | if chunk: 336 | # report progress 337 | if self._progress: 338 | self._download_progress[url] += 1 339 | if self._download_progress[url] <= 100: 340 | self._report_progress( 341 | url, self._download_progress[url]) 342 | 343 | yield chunk 344 | 345 | @staticmethod 346 | def resize(path_to_image, width, height): 347 | """Resize the image and save it again. 348 | :param path_to_image: os.path 349 | :param width: int 350 | :param height: int 351 | :return: None 352 | """ 353 | 354 | fd_img = open(path_to_image, 'rb') 355 | img = Image.open(fd_img) 356 | 357 | try: 358 | img = resizeimage.resize_cover(img, [int(width), int(height)]) 359 | except resizeimage.ImageSizeError: 360 | # error resizing an image 361 | # image is probably too small 362 | pass 363 | 364 | img.save(path_to_image, img.format) 365 | fd_img.close() 366 | 367 | def __report_progress(self, url, progress): 368 | """Prints a progress bar in terminal 369 | :param url: 370 | :param progress: 371 | :return: 372 | """ 373 | 374 | self._stdscr.addstr( 375 | self._terminal_lines[url] + 2, 0, "Downloading file: {0}".format(url) 376 | ) 377 | self._stdscr.addstr( 378 | self._terminal_lines[url] + 3, 0, 379 | "Progress: [{1:100}] {0}%".format(progress, "#" * progress) 380 | ) 381 | self._stdscr.refresh() 382 | 383 | 384 | class GSImage(object): 385 | """Class for handling one image""" 386 | 387 | def __init__(self, fetch_resize_save): 388 | self._fetch_resize_save = fetch_resize_save 389 | 390 | self._url = None 391 | self._path = None 392 | self._referrer_url = None 393 | 394 | self.resized = False 395 | 396 | @property 397 | def url(self): 398 | """Returns the image url 399 | :return: url 400 | """ 401 | 402 | return self._url 403 | 404 | @url.setter 405 | def url(self, image_url): 406 | """Sets the image url 407 | :param image_url: url 408 | :return: None 409 | """ 410 | 411 | self._url = image_url 412 | 413 | @property 414 | def path(self): 415 | """Returns image path 416 | :return: path 417 | """ 418 | 419 | return self._path 420 | 421 | @path.setter 422 | def path(self, image_path): 423 | """Sets image path 424 | :param image_path: path 425 | :return: None 426 | """ 427 | 428 | self._path = image_path 429 | 430 | @property 431 | def referrer_url(self): 432 | """Returns image referrer url 433 | :return: referrer_url 434 | """ 435 | 436 | return self._referrer_url 437 | 438 | @referrer_url.setter 439 | def referrer_url(self, referrer_url): 440 | """Sets image referrer url 441 | :param referrer_url: referrer url 442 | :return: None 443 | """ 444 | 445 | self._referrer_url = referrer_url 446 | 447 | def download(self, path_to_dir): 448 | """Downloads image from url to path 449 | :param path_to_dir: path 450 | :return: None 451 | """ 452 | 453 | self._path = self._fetch_resize_save.download(self._url, path_to_dir) 454 | 455 | def get_raw_data(self): 456 | """Gets images raw data 457 | :return: raw data 458 | """ 459 | 460 | return b''.join(list(self._fetch_resize_save.get_raw_data(self._url))) 461 | 462 | def copy_to(self, obj, raw_data=None): 463 | """Copies raw image data to another object, preferably BytesIO 464 | :param obj: BytesIO 465 | :param raw_data: raw data 466 | :return: None 467 | """ 468 | 469 | if not raw_data: 470 | raw_data = self.get_raw_data() 471 | 472 | obj.write(raw_data) 473 | 474 | def resize(self, width, height): 475 | """Resize the image 476 | :param width: int 477 | :param height: int 478 | :return: None 479 | """ 480 | 481 | self._fetch_resize_save.__class__.resize(self._path, width, height) 482 | self.resized = True 483 | -------------------------------------------------------------------------------- /google_images_search/google_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from apiclient import discovery 4 | 5 | 6 | class GoogleCustomSearch(object): 7 | """Wrapper class for Google images search api""" 8 | 9 | def __init__(self, developer_key=None, 10 | custom_search_cx=None, 11 | fetch_resize_save=None): 12 | 13 | self._developer_key = developer_key or \ 14 | os.environ.get('GCS_DEVELOPER_KEY') 15 | self._custom_search_cx = custom_search_cx or \ 16 | os.environ.get('GCS_CX') 17 | 18 | self._google_build = None 19 | self._fetch_resize_save = fetch_resize_save 20 | 21 | self._search_params_keys = { 22 | 'q': None, 23 | 'searchType': 'image', 24 | 'num': 1, 25 | 'start': 1, 26 | 'imgType': None, 27 | 'imgSize': None, 28 | 'fileType': None, 29 | 'safe': 'off', 30 | 'imgDominantColor': None, 31 | 'imgColorType': None, 32 | 'rights': None 33 | } 34 | 35 | def _query_google_api(self, search_params, cache_discovery=True): 36 | """Queries Google api 37 | :param search_params: dict of params 38 | :param cache_discovery whether or not to cache the discovery doc 39 | :return: search result object 40 | """ 41 | 42 | if not self._google_build: 43 | self._google_build = discovery.build( 44 | "customsearch", "v1", 45 | developerKey=self._developer_key, 46 | cache_discovery=cache_discovery) 47 | 48 | return self._google_build.cse().list( 49 | cx=self._custom_search_cx, **search_params).execute() 50 | 51 | def _search_params(self, params): 52 | """Received a dict of params and merges 53 | it with default params dict 54 | :param params: dict 55 | :return: dict 56 | """ 57 | 58 | search_params = {} 59 | 60 | for key, value in self._search_params_keys.items(): 61 | params_value = params.get(key) 62 | 63 | if params_value: 64 | if key == "imgSize" and params_value != "imgSizeUndefined": 65 | params_value = params_value.upper() 66 | # take user defined param value if defined 67 | search_params[key] = params_value 68 | elif value: 69 | # take default param value if defined 70 | search_params[key] = value 71 | 72 | return search_params 73 | 74 | def search(self, params, cache_discovery=False): 75 | """Search for images and returns 76 | them using generator object 77 | :param params: search params 78 | :param cache_discovery whether or not to cache the discovery doc 79 | :return: image list 80 | """ 81 | 82 | search_params = self._search_params(params) 83 | res = self._query_google_api(search_params, cache_discovery) 84 | 85 | results = res.get('items', []) 86 | if not results: 87 | self._fetch_resize_save.zero_return = True 88 | 89 | for image in results: 90 | if len(self._fetch_resize_save._search_result) >= \ 91 | self._fetch_resize_save._number_of_images: 92 | break 93 | 94 | if self._fetch_resize_save.validate_images: 95 | try: 96 | # simulate browser request 97 | headers = { 98 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64) ' 99 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 100 | 'Chrome/27.0.1453.94 ' 101 | 'Safari/537.36' 102 | } 103 | response = requests.head( 104 | image['link'], timeout=5, allow_redirects=False, 105 | headers=headers 106 | ) 107 | content_length = response.headers.get('Content-Length') 108 | content_type = response.headers.get('Content-Type', '') 109 | 110 | # check if the url is valid 111 | if response.status_code == 200 and \ 112 | 'image' in content_type and content_length: 113 | 114 | # calculate download chunk size based on image size 115 | self._fetch_resize_save.set_chunk_size( 116 | image['link'], content_length 117 | ) 118 | else: 119 | continue 120 | except requests.exceptions.RequestException: 121 | continue 122 | 123 | yield image['link'], image['image']['contextLink'] 124 | 125 | 126 | class GoogleBackendException(Exception): 127 | """Exception handler for search api""" 128 | -------------------------------------------------------------------------------- /google_images_search/meta.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.4.7' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | 4 | [bdist_wheel] 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | def readme(): 6 | with open('README.md') as f: 7 | return f.read() 8 | 9 | 10 | def version(): 11 | with open(os.path.join('.', 'google_images_search', 'meta.py')) as f: 12 | contents = f.read() 13 | return contents.split('__version__ = ')[1].strip()[1:-1] 14 | 15 | 16 | setup( 17 | name='Google Images Search', 18 | version=version(), 19 | 20 | description='Search for image using Google Custom Search ' 21 | 'API and resize & crop the image afterwords', 22 | long_description=readme(), 23 | long_description_content_type='text/markdown', 24 | 25 | url='https://github.com/arrrlo/Google-Images-Search', 26 | licence='MIT', 27 | 28 | author='Ivan Arar', 29 | author_email='ivan.arar@gmail.com', 30 | 31 | classifiers=[ 32 | 'Development Status :: 5 - Production/Stable', 33 | 'Intended Audience :: Developers', 34 | 'Topic :: Software Development :: Build Tools', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Programming Language :: Python :: 3.7', 37 | 'Programming Language :: Python :: 3.8', 38 | 'Programming Language :: Python :: 3.9', 39 | ], 40 | keywords='google images, resize, crop', 41 | 42 | packages=['google_images_search'], 43 | install_requires=[ 44 | 'colorama~=0.4', 45 | 'pyfiglet~=0.8', 46 | 'termcolor~=1.1', 47 | 'click>=7.0, <=8.2', 48 | 'six~=1.12', 49 | 'requests~=2.21', 50 | 'Pillow>=8.1.1', 51 | 'python-resize-image~=1.1', 52 | 'google-api-python-client~=2.48.0', 53 | ], 54 | 55 | entry_points={ 56 | 'console_scripts': [ 57 | 'gimages=google_images_search.cli:cli' 58 | ], 59 | }, 60 | 61 | project_urls={ 62 | 'Source': 'https://github.com/arrrlo/Google-Images-Search', 63 | }, 64 | ) 65 | -------------------------------------------------------------------------------- /tests/test_fetch_resize_save.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from six import BytesIO 5 | 6 | from google_images_search.google_api import GoogleCustomSearch 7 | from google_images_search.fetch_resize_save import FetchResizeSave 8 | 9 | items = { 10 | 'items': [ 11 | { 12 | 'link': 'https://www.gstatic.com/webp/gallery3/1.png', 13 | 'image': { 14 | 'contextLink': 'https://www.gstatic.com' 15 | } 16 | }, 17 | { 18 | 'link': 'https://www.gstatic.com/webp/gallery3/2.png', 19 | 'image': { 20 | 'contextLink': 'https://www.gstatic.com' 21 | } 22 | } 23 | ] 24 | } 25 | 26 | GoogleCustomSearch._query_google_api = \ 27 | lambda self, search_params, cache_discovery: items 28 | 29 | 30 | class TestFetchResizeSave(unittest.TestCase): 31 | 32 | def setUp(self): 33 | self._api_key = '__api_key__' 34 | self._api_cx = '__api_cx__' 35 | self._frs = FetchResizeSave(self._api_key, self._api_cx) 36 | 37 | self._base_dir = os.path.join( 38 | os.path.dirname( 39 | os.path.dirname(os.path.abspath(__file__)) 40 | ), 'tests' 41 | ) 42 | self._file_paths = [ 43 | os.path.join(self._base_dir, '1.png'), 44 | os.path.join(self._base_dir, '2.png'), 45 | ] 46 | 47 | def tearDown(self): 48 | self._frs = None 49 | for path in self._file_paths: 50 | try: 51 | os.remove(path) 52 | except OSError: 53 | pass 54 | 55 | def test_init(self): 56 | self.assertTrue(isinstance( 57 | self._frs._google_custom_search, GoogleCustomSearch 58 | )) 59 | self.assertEqual(self._frs._search_result, []) 60 | self.assertEqual(self._frs._progress, False) 61 | 62 | frs = FetchResizeSave(self._api_key, self._api_cx, 63 | progressbar_fn=lambda x, y: None) 64 | 65 | self.assertEqual(frs._chunk_sizes, {}) 66 | self.assertEqual(frs._terminal_lines, {}) 67 | self.assertEqual(frs._download_progress, {}) 68 | self.assertNotEqual(frs._report_progress, None) 69 | 70 | self.assertEqual(frs._width, None) 71 | self.assertEqual(frs._height, None) 72 | self.assertEqual(frs._path_to_dir, False) 73 | self.assertEqual(frs._search_params, None) 74 | self.assertEqual(frs._cache_discovery, True) 75 | 76 | self.assertEqual(frs._page, 1) 77 | self.assertEqual(frs._number_of_images, None) 78 | 79 | def test_search_url(self): 80 | self._frs.search({'num': 2}) 81 | for i, item in enumerate(self._frs.results()): 82 | self.assertEqual(item.url, items['items'][i]['link']) 83 | 84 | def test_search_referrer_url(self): 85 | self._frs.search({'num': 2}) 86 | for i, item in enumerate(self._frs.results()): 87 | self.assertEqual(item.referrer_url, 88 | items['items'][i]['image']['contextLink']) 89 | 90 | def test_search_path(self): 91 | self._frs.search({}, path_to_dir=self._base_dir, width=100, height=100) 92 | for item in self._frs.results(): 93 | self.assertTrue(item.path in self._file_paths) 94 | 95 | def test_progressbar(self): 96 | progress_data = [] 97 | 98 | def pbar(url, progress): 99 | progress_data.append((url, progress)) 100 | 101 | frs = FetchResizeSave(self._api_key, self._api_cx, progressbar_fn=pbar) 102 | frs.search({'num': 2}, path_to_dir=self._base_dir) 103 | 104 | test_progress_data = \ 105 | list(zip([items['items'][0]['link']] * 100, list(range(1, 101)))) +\ 106 | list(zip([items['items'][1]['link']] * 100, list(range(1, 101)))) 107 | 108 | for progress_item in progress_data: 109 | self.assertTrue(progress_item in test_progress_data) 110 | 111 | def test_bytes_io(self): 112 | my_bytes_io = BytesIO() 113 | 114 | self._frs.search({'num': 2}) 115 | for image in self._frs.results(): 116 | my_bytes_io.seek(0) 117 | raw_image_data = image.get_raw_data() 118 | image.copy_to(my_bytes_io, raw_image_data) 119 | image.copy_to(my_bytes_io) 120 | my_bytes_io.seek(0) 121 | 122 | def test_paging(self): 123 | self._frs.search({'num': 2}) 124 | self.assertEqual(self._frs._search_params, {'num': 2, 'start': 1}) 125 | self.assertEqual(self._frs._page, 1) 126 | self.assertEqual(self._frs._number_of_images, 2) 127 | 128 | self._frs.next_page() 129 | self.assertEqual(self._frs._search_params, {'num': 2, 'start': 3}) 130 | self.assertEqual(self._frs._page, 2) 131 | self.assertEqual(self._frs._number_of_images, 2) 132 | 133 | 134 | if __name__ == '__main__': 135 | unittest.main() 136 | -------------------------------------------------------------------------------- /tests/test_google_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from google_images_search.google_api import GoogleCustomSearch 4 | 5 | 6 | class TestGoogleApi(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self._api_key = '__api_key__' 10 | self._api_cx = '__api_cx__' 11 | self._api = GoogleCustomSearch(self._api_key, self._api_cx) 12 | 13 | def test_init(self): 14 | self.assertEqual(self._api._developer_key, self._api_key) 15 | self.assertEqual(self._api._custom_search_cx, self._api_cx) 16 | self.assertEqual(self._api._google_build, None) 17 | self.assertEqual(self._api._search_params_keys, { 18 | 'q': None, 19 | 'searchType': 'image', 20 | 'num': 1, 21 | 'start': 1, 22 | 'imgType': None, 23 | 'imgSize': None, 24 | 'fileType': None, 25 | 'safe': 'off', 26 | 'imgDominantColor': None, 27 | 'imgColorType': None, 28 | 'rights': None 29 | }) 30 | 31 | def test_search_params(self): 32 | params = { 33 | 'q': 'test', 34 | } 35 | assert_params = { 36 | 'q': 'test', 37 | 'num': 1, 38 | 'start': 1, 39 | 'safe': 'off', 40 | 'searchType': 'image' 41 | } 42 | self.assertEqual(self._api._search_params(params), assert_params) 43 | 44 | params = { 45 | 'q': 'test', 46 | 'num': 12, 47 | 'imgDominantColor': 'black' 48 | } 49 | assert_params = { 50 | 'q': 'test', 51 | 'num': 12, 52 | 'start': 1, 53 | 'safe': 'off', 54 | 'searchType': 'image', 55 | 'imgDominantColor': 'black' 56 | } 57 | self.assertEqual(self._api._search_params(params), assert_params) 58 | 59 | params = { 60 | 'q': 'test', 61 | 'num': 1, 62 | 'start': 1, 63 | 'safe': 'high', 64 | 'fileType': 'jpg', 65 | 'imgType': 'clipart', 66 | 'imgSize': 'huge', 67 | 'searchType': 'image', 68 | 'imgDominantColor': 'black' 69 | } 70 | assert_params = { 71 | 'q': 'test', 72 | 'num': 1, 73 | 'start': 1, 74 | 'safe': 'high', 75 | 'fileType': 'jpg', 76 | 'imgType': 'clipart', 77 | 'imgSize': 'HUGE', 78 | 'searchType': 'image', 79 | 'imgDominantColor': 'black' 80 | } 81 | self.assertEqual(self._api._search_params(params), assert_params) 82 | 83 | 84 | if __name__ == '__main__': 85 | unittest.main() 86 | --------------------------------------------------------------------------------