├── requirements.txt ├── pytest.ini ├── dev-requirements.txt ├── LICENSE ├── .gitignore ├── .travis.yml ├── README.md ├── automate_download_freesound.py └── test_automate_download_freesound.py /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==3.7.0 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v --cov automate_download_freesound --cov-report term-missing 3 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest==3.5.0 3 | coveralls==1.3.0 4 | pytest-cov==2.5.1 5 | pytest-xdist==1.22.2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Kevin Chuang] 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS related files 2 | .DS_Store 3 | 4 | # PyCharm IDE related files 5 | .idea 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: required 4 | 5 | notifications: 6 | email: never 7 | 8 | python: 9 | - '2.7' 10 | 11 | env: 12 | global: 13 | - secure: bu5U/Dpv3U9u6MeEPGUQ5hZCju5IiigO0yd++LtTu7ZNp8UITjgTciBOXQrTMWfvdrbhJ2r4TPXU7MUoOOzJM653QUNIA2BGe6Z3CUk6uZ1VpErtbaeR1U9SY+XO4HgtQ5OmrtaJrCBFrI30i4ag8kBadqiH73bMQq+tNL/jvw261Guf9mNBytYURJYG5OP7gkqV+RdCefR/noQGJyQsWythljUpYYB12Mq4UhzjVWAf+r4J9/CLfdBXBXx2UZn69ToqtyCLQOmlTnP6XdMlO1l/Xn62I3JXu23E43MQhpxtqjj8+IBOlZA/BSp4mrEktgmYthjyJ6zyZPa95gc674leJFid0/torvASchhZiFpPy3lfkFLdZBM+ciSukOulgAeBD3Ph/BdMepWCJelAT2p/gX1LVwkO3BIvZasJdztJLwKrH8qyjMhwVZtwlgreul0yNNQy03Fg/a8ntiqvib//DTYrjBtGM3jlDVTyF+SQZhVxHUy2P0gQJ0ALJstAKnf12mJDerAW4nPJ2OIuemgpOG50SPHCWIrV9IHuzP/r1LH+cKwh3WaONNiAi3GSmw6Bylfhydz6O28g+Lolm3bXytTFVekB9p37x6kCOd7kCDgq0JFWKeTyFoxzDzXhj1rJV0IaN4S+fA7FQA0e2RNzGk7TGiWw4ooBSVdkycE= 14 | - secure: NxfnnV8ovZC+s9LzJSJq2jURz+LqchOc+kV2fXTrIxFBVelDQdhrE1z9VgufveLuv84Jz5viSgY45kKcCivQ2Zt6KlweQnMXuoCNbaDmAAuXwhwEmnvbUX0yk6LiPIY6YX+faN8oYp+AnVY4JJ9XUdIbQgpdMSX4GhNxsEWXwyF4wMl2mTXl1+gmOY9lZW5tsRJK6CRYwgb2Z7kGrhrQUm4/f2lOAPfjN++02792j+BoiIAr2YyV62fAUVJMCOIGOLTGAD2uqJSNin/ChVOTankecJNFow7U7ujCvQcd+DvCzIxWv57/r0qrEQ6P6sCBANygqlj4vh8Nc2HcMfErFzFHgi/OFc5a7aJ2HiO6oXsnKDHZqbbxWYmKJTu7XSSKOpTA+e4yAFYvRT8pbUi9m1HKQ5WZDIwtpLzlhNj26wyKe9ZIqjpvzpD5dwD3VUNQTIUqvfL7fg2hzSEFgZCqhaj8wljuvUNddDaaVJNUU/I530TNYnOkNbS8+UtI/etH4IxUXQz8INiwZ9xicnxTNmQrNhk/tGUsPQ9/LD/TXxPFfmRuvHqR8imcbcoy3VdtmtHTf5IYbvhdlrTLZZJECIOKAciNXJgYxl9AUq62bZmTGHPGrw/+jwS/PD0rU4wPKHA6TlwA+oi4EvbErmFuujgFr1mI0nZ04/ds3UeTjRU= 15 | 16 | addons: 17 | chrome: stable 18 | 19 | before_install: 20 | - wget https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip 21 | - unzip chromedriver_linux64.zip -d /home/travis/virtualenv/python2.7.14/ 22 | - export PATH=$PATH:/home/travis/virtualenv/python2.7.14/ 23 | 24 | install: 25 | - pip install -r dev-requirements.txt 26 | 27 | before_script: 28 | - export DISPLAY=:99.0 29 | - sh -e /etc/init.d/xvfb start 30 | - sleep 3 31 | 32 | script: 33 | - py.test 34 | 35 | after_success: 36 | - coveralls 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automating the Menial Tasks using Python Pt.2 2 | [![Build Status](https://travis-ci.org/k-chuang/automate-download-freesound.svg?branch=master)](https://travis-ci.org/k-chuang/automate-download-freesound) 3 | [![Coverage Status](https://coveralls.io/repos/github/k-chuang/automate-download-freesound/badge.svg?branch=master)](https://coveralls.io/github/k-chuang/automate-download-freesound?branch=master) 4 | [![HitCount](http://hits.dwyl.io/k-chuang/automate-download-freesound.svg)](http://hits.dwyl.io/k-chuang/automate-download-freesound) 5 | 6 | 7 | A cool Python automation project that automates the menial task of downloading hundreds of audio files on 8 | [Freesound](freesound.org) using Selenium, a web-browser automation tool. 9 | 10 | # Description 11 | Using Selenium and the Google chromedriver, this project will automate downloading audio files 12 | via a command line interface (CLI) application. 13 | 14 | Inspired by [Automating the Boring Stuff](https://automatetheboringstuff.com/) by Al Sweigart. 15 | 16 | 17 | # Setup 18 | Install chromedriver via brew or from the [Google site](https://sites.google.com/a/chromium.org/chromedriver/downloads), 19 | and set the location of the chromedriver on your SYSTEM PATH. 20 | 21 | I installed the chromedriver via [brew](https://brew.sh/)(command is below), and naturally, brew installs packages to /usr/local/bin, which is already in your $PATH, so that is pretty convenient. You can always do an echo $PATH to make sure that /usr/local/bin is in it, and if it's not in your $PATH variable, then export it by editing either ~/.bash_profile or /etc/paths/. 22 | 23 | $ brew install chromedriver 24 | 25 | After installing the chromedriver, here is rest of the setup: 26 | 27 | $ git clone https://github.com/k-chuang/automate-download-freesound.git 28 | $ cd automate-download-freesound 29 | $ virtualenv -p /usr/bin/python2.7 venv 30 | $ source venv/bin/activate 31 | $ pip install -r dev-requirements.txt 32 | $ pytest 33 | 34 | # How to use 35 | 36 | Run the command below for more information regarding the arguments (positional or optional, default values, description, etc.) 37 | 38 | $ python automate_download_freesound.py --help 39 | 40 | There is only one required argument, and that is the desired sounds you wish to download from [Freesound](http://freesound.org). You may list more than one sound, but make sure to separate each one by commas, and if the sound you want to download includes spaces, please put quotation marks around it to ensure a smooth experience. 41 | 42 | $ python automate_download_freesound.py "dogs barking,cats purring" 43 | 44 | There are more features and arguments, including download path, file format, sample rate, and advanced filtering. These are all optional arguments, and can help with filtering for your specific needs. 45 | 46 | Here is another example, where all the optional arguments are specified: 47 | 48 | $ python automate_download_freesound.py "baby crying,smoke alarm" --download-dir /Users/KevinChuang/Desktop --file-format wav --sample-rate 48000 --advanced-filter True 49 | 50 | This command will download 'baby crying' and 'smoke alarm' wav files with a sampling rate of 48000 from Freesound.org. 51 | 52 | 53 | # Scripts 54 | [automate_download_freesound.py](https://github.com/k-chuang/automate-download-freesound/blob/master/automate_download_freesound.py) - the main CLI program that uses Selenium to automate downloading sound files from freesound. 55 | 56 | [test_automate_download_freesound.py](https://github.com/k-chuang/automate-download-freesound/blob/master/test_automate_download_freesound.py) - the tester program that runs through unit tests and test cases for the main CLI application. 57 | 58 | ## License 59 | 60 | See the [LICENSE](https://github.com/k-chuang/automate-download-freesound/blob/master/LICENSE) file for license rights and limitations (MIT). 61 | 62 | -------------------------------------------------------------------------------- /automate_download_freesound.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python2.7 2 | 3 | from selenium import webdriver 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.support.ui import WebDriverWait 6 | from selenium.webdriver.support import expected_conditions as EC 7 | from selenium.webdriver.common.keys import Keys 8 | from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException 9 | import getpass 10 | import re 11 | import glob 12 | import os 13 | import time 14 | from collections import namedtuple 15 | import argparse 16 | import sys 17 | 18 | 19 | def authenticate(): 20 | '''A function used to retrieve login credentials to authenticate for freesound.org. 21 | 22 | :return: a namedtuple containing user login credentials with two attributes, an email and a password 23 | ''' 24 | Credentials = namedtuple('Credentials', ['email', 'password']) 25 | user = raw_input("Please enter your email/username for freesound.org: \n") 26 | password = getpass.getpass() 27 | user_info = Credentials(email=user, password=password) 28 | return user_info 29 | 30 | 31 | def verify_authentication(user_info): 32 | '''A function to check if login credentials are valid (True) or not valid (False) 33 | :param user_info: a namedtuple with login credentials: user.email, user.password 34 | :return: boolean value depending on if login credentials are valid 35 | ''' 36 | driver = webdriver.Chrome() 37 | driver = login(driver, user_info.email, user_info.password) 38 | 39 | if re.match("https://freesound.org/home/login/", driver.current_url): 40 | driver.quit() 41 | return False 42 | else: 43 | print("Login successful!") 44 | driver.quit() 45 | return True 46 | 47 | 48 | def login(driver, user, pass_w): 49 | ''' Simulate logging into freesound.org 50 | 51 | :param driver: a chrome driver instance 52 | :param user: the user's email login 53 | :param pass_w: the user's password 54 | :return: the same chrome driver instance 55 | ''' 56 | driver.get("https://freesound.org/home/login/?next=/search/") 57 | username = driver.find_element_by_xpath('//*[@id="id_username"]') 58 | username.send_keys(user) 59 | password = driver.find_element_by_xpath('//*[@id="id_password"]') 60 | password.send_keys(pass_w) 61 | loginSelect = driver.find_element_by_xpath('//*[@id="content_full"]/form/input[2]') 62 | loginSelect.send_keys(Keys.RETURN) 63 | return driver 64 | 65 | 66 | def setup(full_path): 67 | '''Function to set up default download directory and Chrome Options 68 | 69 | :param full_path: absolute path to download to 70 | :return: a chrome driver instance 71 | ''' 72 | chromeOptions = webdriver.ChromeOptions() 73 | prefs = {"download.default_directory": full_path} 74 | chromeOptions.add_experimental_option("prefs", prefs) 75 | driver = webdriver.Chrome(chrome_options=chromeOptions) 76 | return driver 77 | 78 | 79 | def enter_search_subject(driver, search_subject): 80 | '''Function to go to the freesound site, and enter in the desired sound 81 | :param driver: a chrome driver instance 82 | :param search_subject: a string containing the desired sound 83 | :return: a chrome driver instance 84 | ''' 85 | driver.get("https://freesound.org") 86 | search_bar = driver.find_element_by_xpath('//*[@id="search"]/form/fieldset/input[1]') 87 | search_bar.send_keys(search_subject) 88 | search_bar.send_keys(Keys.RETURN) 89 | return driver 90 | 91 | 92 | def filter_by_attribute(driver, attribute_name, attribute_value): 93 | '''Filter by attribute depending on name and value 94 | 95 | :param driver: a chrome driver instance 96 | :param attribute_name: a string that is either samplerate or fileformat 97 | :param attribute_value: a string containing the attributes value 98 | :return: a chrome driver instance 99 | ''' 100 | if attribute_name == 'samplerate': 101 | message = "The sample rate you provided is not supported... Defaulting to sample rate of 48000." 102 | default_value = "48000" 103 | elif attribute_name == 'fileformat': 104 | message = "The file format you provided is not supported... Defaulting to the file format of wav." 105 | default_value = "wav" 106 | 107 | try: 108 | # Check if sample rate is valid search choice 109 | sampling_rate = driver.find_element_by_link_text(str(attribute_value)) 110 | sampling_rate.send_keys(Keys.RETURN) 111 | except NoSuchElementException: 112 | print(message) 113 | sampling_rate = driver.find_element_by_link_text(default_value) 114 | sampling_rate.send_keys(Keys.RETURN) 115 | return driver 116 | 117 | 118 | def advanced_filtering(driver): 119 | '''Function to do more advanced filtering of sound files. Advanced search for 120 | only search subject in tags, file name, or file description. 121 | 122 | :param driver: a chrome driver instance 123 | :return: a chrome driver instance 124 | ''' 125 | 126 | driver.find_element_by_css_selector('a[onclick*=showAdvancedSearchOption').click() 127 | tag_element = WebDriverWait(driver, 10).until( 128 | EC.visibility_of_element_located((By.NAME, 'a_tag'))) 129 | tag_element.click() 130 | 131 | file_element = WebDriverWait(driver, 10).until( 132 | EC.visibility_of_element_located((By.NAME, 'a_filename'))) 133 | file_element.click() 134 | 135 | description_element = WebDriverWait(driver, 10).until( 136 | EC.visibility_of_element_located((By.NAME, 'a_description'))) 137 | description_element.click() 138 | 139 | max_duration = WebDriverWait(driver, 10).until( 140 | EC.visibility_of_element_located((By.XPATH, '//*[@id="filter_duration_max"]'))) 141 | max_duration.clear() 142 | max_duration.send_keys("60") 143 | 144 | new_advanced_search = driver.find_elements_by_xpath('//*[@id="search_submit"]') 145 | new_advanced_search[1].send_keys(Keys.RETURN) 146 | return driver 147 | 148 | 149 | def find_next_page(driver): 150 | '''Function that determines whether or not there is a next page. 151 | 152 | :param driver: a chrome driver instance 153 | :return: a boolean value, True if there is a next page, and False if we are at the last page 154 | ''' 155 | try: 156 | # Try to find a next page button 157 | new_page = driver.find_element_by_xpath('//*[@id="content_full"]/div[2]/ul/li[2]/a') 158 | new_page.send_keys(Keys.RETURN) 159 | return True 160 | except NoSuchElementException: 161 | # This means we are at last page 162 | return False 163 | 164 | 165 | def wait_for_downloads(full_path): 166 | '''Function that waits for downloads, and will only return True 167 | when chrome downloads in full_path are finished. 168 | 169 | :param full_path: the absolute path to the folder where audio files are downloaded 170 | :return: a boolean value, True if downloads are done, False if not done 171 | ''' 172 | unfinished_files = glob.glob(full_path + "/*.crdownload") 173 | if unfinished_files: 174 | return False 175 | else: 176 | return True 177 | 178 | 179 | def simulate_download(sound, download_path, user, pass_w, args): 180 | '''A function used to automate downloading of sound files via Selenium. 181 | 182 | :param sound: a string of the desired sound to download 183 | :param download_path: a path of the desired download path 184 | :param args: a Namespace object with attributes such as file format, sample rate, and advanced filtering 185 | :return: count of number of downloads 186 | ''' 187 | full_path = os.path.join(download_path, sound) 188 | 189 | if not os.path.exists(full_path): 190 | # make a directory for the files to go to 191 | os.makedirs(full_path) 192 | 193 | download_count = 0 194 | driver = setup(full_path) 195 | search_subject = sound 196 | try: 197 | 198 | driver = login(driver, user, pass_w) 199 | 200 | driver = enter_search_subject(driver, search_subject) 201 | if args.samplerate is not None: 202 | driver = filter_by_attribute(driver, 'samplerate', args.samplerate) 203 | if args.file_format is not None: 204 | driver = filter_by_attribute(driver, 'fileformat', args.file_format) 205 | 206 | if args.advanced_filter: 207 | # Advanced search for only search subject in tags or file name 208 | driver = advanced_filtering(driver) 209 | driver.implicitly_wait(1) 210 | 211 | while True: 212 | # Gather all links on the page 213 | links = driver.find_elements_by_class_name("title") 214 | # Loop through all links on this page to download 215 | for j in range(len(links)): 216 | 217 | links[j].send_keys(Keys.RETURN) 218 | 219 | # Finding the download button 220 | download_link = driver.find_element_by_xpath('//*[@id="download_button"]') 221 | download_link.send_keys(Keys.RETURN) 222 | download_count += 1 223 | driver.back() 224 | # Redefine links to avoid stale element error 225 | links = driver.find_elements_by_class_name("title") 226 | if not find_next_page(driver): 227 | break 228 | 229 | # Wait for the rest of the downloads to finish 230 | while not wait_for_downloads(full_path): 231 | time.sleep(5) 232 | 233 | # Close all open browsers associated with driver instance and garbage collect driver instance 234 | driver.quit() 235 | 236 | except TimeoutException: 237 | print("Time out exception... Page took too long to load...") 238 | sys.exit(1) 239 | 240 | return download_count 241 | 242 | 243 | def list_of_sounds(arguments): 244 | '''Create a type for string arguments separated by commas, and generate a list from it. 245 | 246 | :param arguments: a string of arguments separated by commas from command line 247 | :return: a list of string arguments 248 | :raises ArgumentTypeError: an exception that comes from improper argument type 249 | ''' 250 | try: 251 | # Remove spaces between commas (if user happens to have spaces) 252 | no_space = re.sub(r'\s*,\s*', ',', arguments) 253 | sound_list = map(str, no_space.split(',')) 254 | sound_list = filter(None, sound_list) 255 | return sound_list 256 | except: 257 | raise argparse.ArgumentTypeError("List of sounds must be separated by commas (no spaces).") 258 | 259 | 260 | def parse_args(argv): 261 | ''' 262 | 263 | :param argv: string of sys arguments passed to command line 264 | :return: arguments parsed out of sys.argv 265 | ''' 266 | parser = argparse.ArgumentParser(description='Download audio files from Freesound.org!') 267 | parser.add_argument('sounds', 268 | type=list_of_sounds, 269 | default=None, 270 | help='Enter sound(s) you would like to download ' 271 | 'separated by commas and no spaces. If desired sound ' 272 | 'contains two or more words, please enclose sound list in parenthesis' 273 | 'to prevent errors. (i.e. "dog barking,cats purring,birds chirping")') 274 | 275 | parser.add_argument('--download-dir', 276 | dest='downloadpath', 277 | default=os.path.expanduser("~") + "/Downloads/", 278 | help='Optional argument to specify the download path where files will go. ' 279 | 'Default will be your standard Downloads folder. ' 280 | 'Works for both MacOS and Windows environments.') 281 | 282 | parser.add_argument('--file-format', 283 | dest='file_format', 284 | type=str, 285 | default=None, 286 | choices=[None, "wav", "flac", "aiff", "ogg", "mp3", "m4a"], 287 | help='Enter the desired audio file format. ' 288 | 'Default will be all available audio file formats with no filtering. ' 289 | 'The available options are wav, aiff, flac, ogg, m4a, and mp3.') 290 | 291 | parser.add_argument('--sample-rate', 292 | dest='samplerate', 293 | type=int, 294 | choices=[None, 11025, 16000, 22050, 44100, 48000, 88200, 96000], 295 | default=None, 296 | help='Enter the desired sample rate of the file. ' 297 | 'Default will be all of the available sample rates with no filtering. ' 298 | 'The available options are 11025, 22050, 44100, 48000, 88200, and 96000.') 299 | 300 | parser.add_argument('--advanced-filter', 301 | dest='advanced_filter', 302 | type=bool, 303 | default=False, 304 | help='Enter True if you want to initiate advanced filtering to limit audio files. ' 305 | 'Only audio files with tags, filenames, and descriptions ' 306 | 'containing your search item will be downloaded.') 307 | 308 | # If no arguments provided, return help message 309 | if len(argv) == 1: 310 | parser.print_help(sys.stderr) 311 | sys.exit(1) 312 | 313 | return parser.parse_args(argv[1:]) 314 | 315 | 316 | def main(argv): 317 | args = parse_args(argv) 318 | 319 | # To be clear: 320 | sounds = args.sounds 321 | download_path = args.downloadpath 322 | 323 | if not os.path.exists(download_path): 324 | print("The download destination directory specified does not exist... Defaulting to Downloads folder.") 325 | download_path = os.path.expanduser("~") + "/Downloads/" 326 | 327 | user_info = authenticate() 328 | credentials_flag = verify_authentication(user_info) 329 | 330 | if not credentials_flag: 331 | print("The credentials you entered were not correct. Please re-run the script. Exiting program...") 332 | sys.exit(1) 333 | 334 | for elem in sounds: 335 | download_count = simulate_download(elem, download_path, user_info.email, user_info.password, args) 336 | output_path = os.path.join(download_path, elem) 337 | print("Downloaded %d files of \"%s\" at %s" % 338 | (download_count, elem, output_path)) 339 | 340 | return 0 341 | 342 | 343 | if __name__ == "__main__": 344 | sys.exit(main(sys.argv)) 345 | -------------------------------------------------------------------------------- /test_automate_download_freesound.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for test_automate_download_freesound.py 3 | Run with: 4 | $ pytest 5 | """ 6 | 7 | import unittest 8 | import mock 9 | import automate_download_freesound 10 | from selenium import webdriver 11 | from selenium.webdriver.common.by import By 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | from selenium.webdriver.support import expected_conditions as EC 14 | from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException 15 | from collections import namedtuple 16 | import pytest 17 | import os 18 | import shutil 19 | 20 | 21 | class FreeSoundLoginElementsTest(unittest.TestCase): 22 | '''Using the setUpClass() and tearDownClass() methods along with the @classmethod decorator. 23 | These methods enable us to set the values at the class level rather than at method level. 24 | The values initialized at class level are shared between the test methods.''' 25 | @classmethod 26 | def setUpClass(cls): 27 | cls.driver = webdriver.Chrome() 28 | cls.driver.get("https://freesound.org/home/login/?next=/") 29 | 30 | def test_login_username(self): 31 | self.assertTrue(self.is_element_present(By.XPATH, '//*[@id="id_username"]')) 32 | 33 | def test_password(self): 34 | self.assertTrue(self.is_element_present(By.XPATH, '//*[@id="id_password"]')) 35 | 36 | @classmethod 37 | def tearDownClass(cls): 38 | cls.driver.quit() 39 | 40 | def is_element_present(self, how, what): 41 | ''' 42 | Helper method to confirm the presence of an element on page 43 | :params how: By locator type 44 | :params what: locator value 45 | ''' 46 | try: 47 | self.driver.find_element(by=how, value=what) 48 | except NoSuchElementException: 49 | return False 50 | return True 51 | 52 | 53 | class FreeSoundSearchByTextTest(unittest.TestCase): 54 | '''Using the setUpClass() and tearDownClass() methods along with the @classmethod decorator. 55 | These methods enable us to set the values at the class level rather than at method level. 56 | The values initialized at class level are shared between the test methods.''' 57 | @classmethod 58 | def setUpClass(cls): 59 | cls.driver = webdriver.Chrome() 60 | cls.driver.get("https://freesound.org/") 61 | 62 | def test_search_by_text(self): 63 | ''' 64 | Test to see that the number of elements within the first page of a search item is 15. 65 | ''' 66 | # get the search textbox 67 | self.search_field = self.driver.find_element_by_name("q") 68 | self.search_field.clear() 69 | # enter search keyword and submit 70 | self.search_field.send_keys("dogs barking") 71 | self.search_field.submit() 72 | # get the list of elements which are displayed after the search 73 | # currently on result page using find_elements_by_class_name method 74 | lists = self.driver.find_elements_by_class_name("title") 75 | self.assertEqual(15, len(lists)) 76 | 77 | @classmethod 78 | def tearDownClass(cls): 79 | cls.driver.quit() 80 | 81 | def is_element_present(self, how, what): 82 | ''' 83 | Helper method to confirm the presence of an element on page 84 | :params how: By locator type 85 | :params what: locator value 86 | ''' 87 | try: 88 | self.driver.find_element(by=how, value=what) 89 | except NoSuchElementException: 90 | return False 91 | return True 92 | 93 | 94 | class FreeSoundSearchTest(unittest.TestCase): 95 | '''Using the setUpClass() and tearDownClass() methods along with the @classmethod decorator. 96 | These methods enable us to set the values at the class level rather than at method level. 97 | The values initialized at class level are shared between the test methods.''' 98 | @classmethod 99 | def setUpClass(cls): 100 | cls.driver = webdriver.Chrome() 101 | cls.driver.get("https://freesound.org/search/?q=") 102 | 103 | def test_for_number_of_elements(self): 104 | # get the list of elements which are displayed after the search 105 | # currently on result page using find_elements_by_class_name method 106 | lists = self.driver.find_elements_by_class_name("title") 107 | self.assertEqual(15, len(lists)) 108 | 109 | def test_for_file_format(self): 110 | ''' 111 | Test to find attribute of file format 112 | ''' 113 | self.assertTrue(self.is_element_present(By.XPATH, '//*[@id="sidebar"]/h3[3]')) 114 | 115 | def test_for_wav_file_format(self): 116 | ''' 117 | Test to find specific wav format 118 | ''' 119 | self.assertTrue(self.is_element_present(By.LINK_TEXT, 'wav')) 120 | 121 | def test_for_samplerate(self): 122 | ''' 123 | Test to find attribute of samplerate 124 | ''' 125 | self.assertTrue(self.is_element_present(By.XPATH, '//*[@id="sidebar"]/h3[4]')) 126 | 127 | def test_for_specific_samplerate(self): 128 | ''' 129 | Test to find specific 48000 sample rate 130 | ''' 131 | self.assertTrue(self.is_element_present(By.LINK_TEXT, '48000')) 132 | 133 | def test_for_advanced_filter(self): 134 | ''' 135 | Test for advanced filter button 136 | ''' 137 | self.assertTrue(self.is_element_present( 138 | By.CSS_SELECTOR, 'a[onclick*=showAdvancedSearchOption')) 139 | 140 | @classmethod 141 | def tearDownClass(cls): 142 | cls.driver.quit() 143 | 144 | def is_element_present(self, how, what): 145 | ''' 146 | Helper method to confirm the presence of an element on page 147 | :params how: By locator type 148 | :params what: locator value 149 | ''' 150 | try: 151 | self.driver.find_element(by=how, value=what) 152 | except NoSuchElementException: 153 | return False 154 | return True 155 | 156 | 157 | class FreeSoundAdvancedFilter(unittest.TestCase): 158 | '''Using the setUpClass() and tearDownClass() methods along with the @classmethod decorator. 159 | These methods enable us to set the values at the class level rather than at method level. 160 | The values initialized at class level are shared between the test methods.''' 161 | @classmethod 162 | def setUpClass(cls): 163 | cls.driver = webdriver.Chrome() 164 | cls.driver.get("https://freesound.org/search/?q=") 165 | cls.driver.find_element_by_css_selector('a[onclick*=showAdvancedSearchOption').click() 166 | 167 | def test_fail_filter_item(self): 168 | with self.assertRaises(NoSuchElementException): 169 | self.driver.find_element_by_id('10000') 170 | 171 | def test_tags_filter_item(self): 172 | tag_element = WebDriverWait(self.driver, 10).until( 173 | EC.visibility_of_element_located((By.NAME, 'a_tag'))) 174 | tag_element.click() 175 | self.assertTrue(tag_element.is_selected()) 176 | 177 | def test_filenames_filter_item(self): 178 | file_element = WebDriverWait(self.driver, 10).until( 179 | EC.visibility_of_element_located((By.NAME, 'a_filename'))) 180 | file_element.click() 181 | self.assertTrue(file_element.is_selected()) 182 | 183 | def test_description_filter_item(self): 184 | description_element = WebDriverWait(self.driver, 10).until( 185 | EC.visibility_of_element_located((By.NAME, 'a_description'))) 186 | description_element.click() 187 | self.assertTrue(description_element.is_selected()) 188 | 189 | @classmethod 190 | def tearDownClass(cls): 191 | cls.driver.quit() 192 | 193 | def is_element_present(self, how, what): 194 | ''' 195 | Helper method to confirm the presence of an element on page 196 | :params how: By locator type 197 | :params what: locator value 198 | ''' 199 | try: 200 | self.driver.find_element(by=how, value=what) 201 | except NoSuchElementException: 202 | return False 203 | return True 204 | 205 | 206 | class SimulateDownloadIntegrationTest(unittest.TestCase): 207 | 208 | @classmethod 209 | def setUpClass(cls): 210 | cls.email = os.environ['FREESOUND_EMAIL'] 211 | cls.password = os.environ['FREESOUND_PASSWORD'] 212 | cls.download_path = os.path.expanduser("~") + "/Downloads/" 213 | shutil.rmtree(os.path.join(cls.download_path, 'toaster pop set'), ignore_errors=True) 214 | shutil.rmtree(os.path.join(cls.download_path, 'tiger'), ignore_errors=True) 215 | shutil.rmtree(os.path.join(cls.download_path, 'glass breaking'), ignore_errors=True) 216 | 217 | def test_simulate_download_basic(self): 218 | ''' 219 | Testing with no filters, just the one positional argument 220 | ''' 221 | 222 | args = automate_download_freesound.parse_args(['automate_download_freesound.py', 'toaster pop set']) 223 | download_count = automate_download_freesound.simulate_download( 224 | args.sounds[0], self.download_path, self.email, self.password, args) 225 | self.assertEqual(download_count, 2) 226 | update_download_path = os.path.join(self.download_path, 'toaster pop set') 227 | self.assertEqual(len(os.listdir(update_download_path)), download_count) 228 | 229 | def test_simulate_download_optional_arguments(self): 230 | ''' 231 | Testing with optional filter arguments 232 | ''' 233 | 234 | args = automate_download_freesound.parse_args( 235 | ['automate_download_freesound.py', 'tiger', 236 | '--sample-rate', '48000', '--file-format', 'mp3']) 237 | download_count = automate_download_freesound.simulate_download( 238 | args.sounds[0], self.download_path, self.email, self.password, args) 239 | self.assertEqual(download_count, 2) 240 | update_download_path = os.path.join(self.download_path, 'tiger') 241 | self.assertEqual(len(os.listdir(update_download_path)), download_count) 242 | 243 | def test_simulate_download_advanced_filters(self): 244 | ''' 245 | Testing with optional arguments and advanced filters 246 | ''' 247 | args = automate_download_freesound.parse_args( 248 | ['automate_download_freesound.py', 'glass breaking', 249 | '--sample-rate', '48000', '--file-format', 'flac', '--advanced-filter', 'True']) 250 | download_count = automate_download_freesound.simulate_download( 251 | args.sounds[0], self.download_path, self.email, self.password, args) 252 | self.assertEqual(download_count, 3) 253 | update_download_path = os.path.join(self.download_path, 'glass breaking') 254 | self.assertEqual(len(os.listdir(update_download_path)), download_count) 255 | 256 | @classmethod 257 | def tearDownClass(cls): 258 | shutil.rmtree(os.path.join(cls.download_path, 'toaster pop set'), ignore_errors=True) 259 | shutil.rmtree(os.path.join(cls.download_path, 'tiger'), ignore_errors=True) 260 | shutil.rmtree(os.path.join(cls.download_path, 'glass breaking'), ignore_errors=True) 261 | 262 | 263 | class FreeSoundLoginAuthenticationTest(unittest.TestCase): 264 | 265 | @mock.patch('getpass.getpass') 266 | @mock.patch('__builtin__.raw_input') 267 | def test_authenticate(self, input, getpass): 268 | input.return_value = 'example@gmail.com' 269 | getpass.return_value = 'MyPassword' 270 | Credentials = namedtuple('Credentials', ['email', 'password']) 271 | user_info = Credentials(email=input.return_value, password=getpass.return_value) 272 | self.assertEqual(automate_download_freesound.authenticate(), user_info) 273 | 274 | @mock.patch('getpass.getpass') 275 | @mock.patch('__builtin__.raw_input') 276 | def test_verify_authentication_fail(self, input, getpass): 277 | ''' 278 | Test verify authentication to make sure fail 279 | ''' 280 | input.return_value = 'example@gmail.com' 281 | getpass.return_value = 'MyPassword' 282 | Credentials = namedtuple('Credentials', ['email', 'password']) 283 | user_info = Credentials(email=input.return_value, password=getpass.return_value) 284 | self.assertFalse(automate_download_freesound.verify_authentication(user_info)) 285 | 286 | @mock.patch('getpass.getpass') 287 | @mock.patch('__builtin__.raw_input') 288 | def test_verify_authentication_pass(self, input, getpass): 289 | ''' 290 | Test verify authentication to make sure pass 291 | ''' 292 | input.return_value = os.environ['FREESOUND_EMAIL'] 293 | getpass.return_value = os.environ['FREESOUND_PASSWORD'] 294 | Credentials = namedtuple('Credentials', ['email', 'password']) 295 | user_info = Credentials(email=input.return_value, password=getpass.return_value) 296 | self.assertTrue(automate_download_freesound.verify_authentication(user_info)) 297 | 298 | 299 | class CommandLineArgumentsTests(unittest.TestCase): 300 | 301 | def test_parse_args_sound_one(self): 302 | ''' 303 | Basic test to test for the sound positional argument of parse_args() 304 | ''' 305 | args = automate_download_freesound.parse_args(['automate_download_freesound.py', 'dogs']) 306 | self.assertEqual(args.sounds, ['dogs']) 307 | self.assertEqual(args.downloadpath, os.path.expanduser("~") + "/Downloads/") 308 | self.assertEqual(args.file_format, None) 309 | self.assertEqual(args.samplerate, None) 310 | self.assertFalse(args.advanced_filter) 311 | 312 | def test_parse_args_sound_two(self): 313 | ''' 314 | Test for multiple arguments that include spaces, but separated by commas 315 | ''' 316 | args = automate_download_freesound.parse_args( 317 | ['automate_download_freesound.py', "dogs barking loud, birds chirping loud"]) 318 | self.assertEqual(args.sounds, ['dogs barking loud', 'birds chirping loud']) 319 | self.assertEqual(args.downloadpath, os.path.expanduser("~") + "/Downloads/") 320 | self.assertEqual(args.file_format, None) 321 | self.assertEqual(args.samplerate, None) 322 | self.assertFalse(args.advanced_filter) 323 | 324 | def test_parse_args_sound_three(self): 325 | ''' 326 | Test for leading/extra commas as input 327 | ''' 328 | args = automate_download_freesound.parse_args( 329 | ['automate_download_freesound.py', "dogs,cats,birds,"]) 330 | self.assertEqual(args.sounds, ['dogs', 'cats', 'birds']) 331 | self.assertEqual(args.downloadpath, os.path.expanduser("~") + "/Downloads/") 332 | self.assertEqual(args.file_format, None) 333 | self.assertEqual(args.samplerate, None) 334 | self.assertFalse(args.advanced_filter) 335 | 336 | def test_parse_args_format_pass(self): 337 | args = automate_download_freesound.parse_args( 338 | ['automate_download_freesound.py', "dogs,cats,birds,", "--file-format", "wav"]) 339 | assert args.file_format in [None, "wav", "flac", "aiff", "ogg", "mp3", "m4a"] 340 | 341 | def test_parse_args_format_fail(self): 342 | '''Test to see if there is a command line syntax error of wth error code 2 343 | ''' 344 | with self.assertRaises(SystemExit) as err: 345 | automate_download_freesound.parse_args( 346 | ['automate_download_freesound.py', "dogs,cats,birds,", "--file-format", "mp5"]) 347 | self.assertEqual(err.exception.code, 2) 348 | 349 | def test_parse_args_samplerate_pass(self): 350 | args = automate_download_freesound.parse_args( 351 | ['automate_download_freesound.py', "dogs,cats,birds,", "--sample-rate", "48000"]) 352 | assert int(args.samplerate) in [None, 11025, 16000, 22050, 44100, 48000, 88200, 96000] 353 | 354 | def test_parse_args_samplerate_fail(self): 355 | '''Test to see if there is a command line syntax error of wth error code 2 356 | ''' 357 | with self.assertRaises(SystemExit) as err: 358 | automate_download_freesound.parse_args( 359 | ['automate_download_freesound.py', "dogs,cats,birds,", "--sample-rate", "2500"]) 360 | self.assertEqual(err.exception.code, 2) 361 | 362 | def test_parse_args_download_path_pass(self): 363 | args = automate_download_freesound.parse_args( 364 | ['automate_download_freesound.py', "dogs,cats,birds,"]) 365 | self.assertEqual(args.downloadpath, os.path.expanduser("~") + "/Downloads/") 366 | 367 | def test_main(self): 368 | ''' 369 | Test for main function to exit with error code 1 and provide help if no arguments provided 370 | ''' 371 | with self.assertRaises(SystemExit) as err: 372 | automate_download_freesound.main(['automate_download_freesound.py']) 373 | self.assertEqual(err.exception.code, 1) 374 | --------------------------------------------------------------------------------