├── .gitignore ├── LICENSE ├── README.md ├── overcast-uploader.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea/ 107 | 108 | music/ 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paweł Kamiński 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # overcast-uploader 2 | Command line tool to upload mp3 files to [Overcast Podcast App](https://overcast.fm). 3 | 4 | ## Requirements 5 | * Python 3 6 | * Overcast Premium Account 7 | 8 | ## Installing dependencies 9 | ``` 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | ## Usage 14 | 15 | ``` 16 | usage: overcast-uploader.py [-h] [-e EMAIL] [-p PASSWORD] [-c] 17 | (-d DIR | -f FILE) 18 | 19 | optional arguments: 20 | -h, --help show this help message and exit 21 | -e EMAIL, --email EMAIL 22 | E-mail used to login to Overcast. 23 | -p PASSWORD, --password PASSWORD 24 | Password to Overcast account. 25 | -c, --clean Remove file after upload. 26 | -d DIR, --dir DIR Directory with files to upload. 27 | -f FILE, --file FILE File to upload. 28 | ``` 29 | 30 | ### Examples of usage 31 | #### Upload mp3 file 32 | ``` 33 | python overcast-uploader.py -f /path/to/file.mp3 34 | ``` 35 | 36 | #### Upload all mp3 files from directory 37 | ``` 38 | python overcast-uploader.py -d /path/to/path/to/directory-with-mp3-files 39 | ``` 40 | 41 | #### Upload mp3 file and delete it afterwards 42 | ``` 43 | python overcast-uploader.py -f /path/to/file.mp3 -c 44 | ``` 45 | 46 | #### Upload all mp3 files from directory and delete them afterwards 47 | ``` 48 | python overcast-uploader.py -d /path/to/directory-with-mp3-files -c 49 | ``` 50 | 51 | #### Upload mp3 file and provide Overcast credentials in program arguments 52 | ``` 53 | python overcast-uploader.py -f /path/to/file.mp3 -e abc@xyz.com -p password 54 | ``` 55 | -------------------------------------------------------------------------------- /overcast-uploader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import requests 3 | import bs4 as bs 4 | import argparse 5 | import glob, os 6 | import threading 7 | import datetime 8 | 9 | def send_file_to_overcast(filepath, login, password, clean=False): 10 | if not filepath.endswith("mp3"): 11 | raise Exception("Only mp3 files can be uploaded.") 12 | 13 | with open(filepath, 'rb') as f: 14 | file_body = f.read() 15 | 16 | print("Sending: " + filepath + " Size: " + str(len(file_body)) + " bytes") 17 | filename = os.path.basename(filepath) 18 | r = requests.Session() 19 | 20 | payload = {'email': login, 'password': password} 21 | r.post('https://overcast.fm/login', data=payload) 22 | data = r.get('https://overcast.fm/uploads').text 23 | 24 | soup = bs.BeautifulSoup(data, "html.parser") 25 | supa = soup.find('form', attrs={'id': 'upload_form'}) 26 | 27 | action = supa.get('action') 28 | data_key_prefix = supa.get('data-key-prefix') 29 | 30 | bucket = supa.find('input', attrs={'name': 'bucket'}).get('value') 31 | key = supa.find('input', attrs={'name': 'key'}).get('value') 32 | aws_access_key_id = supa.find('input', attrs={'name': 'AWSAccessKeyId'}).get('value') 33 | acl = supa.find('input', attrs={'name': 'acl'}).get('value') 34 | upload_policy = supa.find('input', attrs={'name': 'policy'}).get('value') 35 | upload_signature = supa.find('input', attrs={'name': 'signature'}).get('value') 36 | upload_ctype = supa.find('input', attrs={'name': 'Content-Type'}).get('value') 37 | 38 | data_key_prefix += filename 39 | 40 | 41 | form_params = { 42 | "bucket": (None, bucket), 43 | "key": (None, key), 44 | "AWSAccessKeyId": (None, aws_access_key_id), 45 | "acl": (None, acl), 46 | "policy": (None, upload_policy), 47 | "signature": (None, upload_signature), 48 | "Content-Type": (None, upload_ctype), 49 | "file": (filename, file_body) 50 | } 51 | 52 | r.post(action, files=form_params) 53 | 54 | r.post('https://overcast.fm/podcasts/upload_succeeded', data={"key": data_key_prefix}) 55 | 56 | print(filepath + " has been sent") 57 | 58 | if clean and os.path.exists(filepath): 59 | os.remove(filepath) 60 | 61 | 62 | def send_directory_to_overcast(dirpath, login, password, clean=False): 63 | os.chdir(dirpath) 64 | threads = [] 65 | for filename in glob.glob("*"): 66 | if not filename.endswith("mp3"): 67 | continue 68 | 69 | t = threading.Thread(target=send_file_to_overcast, args=(filename, login, password, clean)) 70 | threads.append(t) 71 | t.start() 72 | 73 | for t in threads: 74 | t.join() 75 | 76 | 77 | if __name__ == '__main__': 78 | parser = argparse.ArgumentParser() 79 | parser.add_argument('-e', '--email', help="E-mail used to login to Overcast.") 80 | parser.add_argument('-p', '--password', help="Password to Overcast account.") 81 | parser.add_argument('-c', '--clean', help="Remove file after upload.", action="store_true") 82 | 83 | group = parser.add_mutually_exclusive_group(required=True) 84 | group.add_argument('-d', '--dir', help="Directory with files to upload.") 85 | group.add_argument('-f', '--file', help="File to upload.") 86 | args = parser.parse_args() 87 | 88 | if args.dir is not None and not os.path.isdir(args.dir): 89 | print(args.dir + " is not a valid path") 90 | exit(1) 91 | 92 | if args.file is not None and not os.path.isfile(args.file): 93 | print(args.file + " is not a valid path") 94 | exit(1) 95 | 96 | if args.email: 97 | login = args.email 98 | else: 99 | login = input("Email:") 100 | 101 | if args.password: 102 | password = args.password 103 | else: 104 | password = input("Password:") 105 | 106 | begin_time = datetime.datetime.now() 107 | 108 | if args.file is not None: 109 | send_file_to_overcast(args.file, login, password, args.clean) 110 | elif args.dir is not None: 111 | send_directory_to_overcast(args.dir, login, password, args.clean) 112 | 113 | end_time = datetime.datetime.now() 114 | 115 | print("Begin time: " + str(begin_time)) 116 | print("End time: " + str(end_time)) 117 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.8.0 2 | certifi==2019.9.11 3 | chardet==3.0.4 4 | idna==2.8 5 | requests==2.22.0 6 | soupsieve==1.9.3 7 | urllib3==1.25.5 8 | --------------------------------------------------------------------------------